commit 0e2bba237a4ada27ebb9167cec95f847ee1c674b Author: Loki Verloren Date: Mon May 3 10:43:10 2021 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39c4a4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +pod +.idea diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..8a91528 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +pod.parallelcoin.io \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 0000000..943cfb8 --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,56 @@ + +### Contribution Guidelines + +Loki, the primary author of this code has somewhat unconventional ideas about everything including Go, developed out of the process of building this application and the use of compact logging syntax with visual recognisability as a key debugging technique for mostly multi-threaded code, and so, tools have been developed and conventions are found throughout the code and increasingly as all the dusty parts get looked at again. + +So, there's a few things that you should do, which Loki thinks you will anyway realise that should be the default anyway. He will stop speaking in the third person now. + +1. e is error + + being a verb, `err` is a stolen name. But it also is 3 times as long. It is nearly universal among programmers that i, usually also j and k are iterator variables, and commonly also x, y, z, most cogently relevant when they are coordinates. So use e, not err + +2. Use the logger, E.Chk for serious conditions or while debugging, non-blocking failures use D.Chk + + It's not difficult, literally just copy any log.go file out of this repo, change the package name and then you can use the logger, which includes these handy check functions that some certain Go Authors use in several of their lectures and texts, and actually probably the origin of the whole idea of making the error type a pointer to string, which led to the Wrap/Unwrap interface which I don't use. + +4. Use if statements with ALL calls that return errors or booleans + + And declare their variables with var statements, unless there is no more uses of the `e` again before all paths of execution return. + +5. Prefer to return early + + The `.Chk` functions return true if not nil, and so without the `!` in front, the most common case, the content of the following `{}` are the error handling code. + + In general, and especially when the process is not idempotent (changed order breaks the process), which will be most of the time with several processes in a sequence, especially in Gio code which is naturally a bit wide if you write it to make it easily sliced, you want to keep the success line of execution as far left as possible. + + Sometimes the negative condition is ignored, as there is a retry or it is not critical, and for these cases use `!E.Chk` and put the success path inside its if block. + +6. In the Gio code of the wallet, take advantage of the fact that you can break a line after the dot `.` operator and as you will see amply throughout the code, as it allows items to be repositioned and added with minimal fuss. + + Deep levels of closures and stacked variables of any kind tend to lead quickly to a lot of nasty red lines in one's IDE. The fluent method chaining pattern is used because it is far more concise than the raw type definition followed by closure attached by a dot operator, but since it would be valid and pass `gofmt` unchanged to put the whole chain in there (assuming it somehow had no anonymous functions in it). + +7. Use the logger + + This is being partly repeated as it's very important. Regardless of programmer opinions about whether a debugger is a better tool than a log viewer, note that while it is not fully implemented, `pod` already contains a protocol to aggregate child process logs invisibly from the terminal through an IPC, and logging is one means to enabling auditability of code. + + So long as logs concern themselves primarily with metadata information and only expose data in `trace` level (with the `T` error type) and put the really heavy stuff like printing tree walks over thousands of nodes or other similarly very short operations, put them inside closures with `T.C(func()string{})` + +8. `gofmt` sort the imports, and avoid whenever possible a package name different from a folder, unless you can't avoid an import name conflict. A notable example is uber/atomic. + +9. 120 characters wide + + We are living in the 21st century. It was already common as this century started that monitors were big enough to show 120 or more characters wide, 80 characters is just too narrow. + + Well, it would be even better if I figured out a way to make the Gio code not stack out to the right so deep but it's comfortable in 120 unless I have put too many things into one closure. + +10. Always put doc comments on exported functions. Put them on struct fields and variables also in the case that detailed information isn't already available elsewhere related to the item. Keep the doc comments to one line unless you really need to explain something that needs to be visible in the documentation. For the most part this means libraries and for the most part a lot of that is in independent repositories. + +11. Avoid pointers for other than structs and arrays (fixed length). Pointers require mutexes with concurrent code, which is the rule rather than the exception, and it is incredibly easy to put a lock inside a downstream function that has just locked it and the application freezes. + + Instead, as a default option, use atomics and if necessary further processing or hook-calling from getters or setters. If it's a big struct, so much the better. + + Atomics are perfectly suited for independent threads with narrow responsibilities, and they also don't lock out threads from accessing other fields of the struct concurrently, which can become a bottleneck that invisibly creeps up on you. + +12. `if e = function(); E.Chk(e){}` is the prototype of how all functions handling errors should be invoked. It is more compact and doesn't noise up the call so much as the alternative fills up your screen. + +13. When considering whether to use an interface, ask whether a generator might be easier (and perhaps faster). The generics-loving vocal usually sub-2 years programmers who came from generics abusing languages like c#, java or c++, can just go away, Go14life. Automation > abbreviation. diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..ab24099 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# TODO: write the build for minimal Go environment and headless build +FROM golang:1.14 as builder +WORKDIR /pod +COPY .git /pod/.git +COPY app /pod/app +COPY cmd /pod/cmd +COPY pkg /pod/pkg +COPY stroy /pod/stroy +COPY version /pod/version +COPY go.??? /pod/ +COPY ./*.go /pod/ +RUN ls +ENV GOBIN "/bin" +ENV PATH "$GOBIN:$PATH" +RUN cd /pod && go install ./cmd/build/. +RUN cd /pod && build build +RUN cd /pod && stroy docker +RUN stroy teststopnode +FROM ubuntu:20.04 +ENV GOBIN "/bin" +ENV PATH "$GOBIN:$PATH" +#RUN /usr/bin/which sh +COPY --from=builder /bin/stroy /bin/ +COPY --from=builder /bin/pod /bin/ +#COPY --from=builder /usr/bin/sh /bin/ +#RUN echo $PATH && /bin/stroy +RUN /bin/pod version +EXPOSE 11048 11047 21048 21047 +CMD ["tail", "-f", "/dev/null"] +#CMD /usr/local/bin/parallelcoind -txindex -debug -debugnet -rpcuser=user -rpcpassword=pa55word -connect=127.0.0.1:11047 -connect=seed1.parallelcoin.info -bind=127.0.0.1 -port=11147 -rpcport=11148 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed46d05 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +p9 diff --git a/cmd/ctl/ctl.go b/cmd/ctl/ctl.go new file mode 100644 index 0000000..55ef3a8 --- /dev/null +++ b/cmd/ctl/ctl.go @@ -0,0 +1,380 @@ +package ctl + +import ( + "bufio" + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "os" + "strings" + + "github.com/btcsuite/go-socks/socks" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pod/config" +) + +// Call uses settings in the context to call the method with the given parameters and returns the raw json bytes +func Call( + cx *config.Config, wallet bool, method string, params ...interface{}, +) (result []byte, e error) { + // Ensure the specified method identifies a valid registered command and is one of the usable types. + var usageFlags btcjson.UsageFlag + usageFlags, e = btcjson.MethodUsageFlags(method) + if e != nil { + e = errors.New("Unrecognized command '" + method + "' : " + e.Error()) + // HelpPrint() + return + } + if usageFlags&btcjson.UnusableFlags != 0 { + E.F("The '%s' command can only be used via websockets\n", method) + // HelpPrint() + return + } + // Attempt to create the appropriate command using the arguments provided by the user. + var cmd interface{} + cmd, e = btcjson.NewCmd(method, params...) + if e != nil { + // Show the error along with its error code when it's a json. BTCJSONError as it realistically will always be + // since the NewCmd function is only supposed to return errors of that type. + if jerr, ok := e.(btcjson.GeneralError); ok { + errText := fmt.Sprintf("%s command: %v (code: %s)\n", method, e, jerr.ErrorCode) + e = errors.New(errText) + // CommandUsage(method) + return + } + // The error is not a json.BTCJSONError and this really should not happen. Nevertheless fall back to just + // showing the error if it should happen due to a bug in the package. + errText := fmt.Sprintf("%s command: %v\n", method, e) + e = errors.New(errText) + // CommandUsage(method) + return + } + // Marshal the command into a JSON-RPC byte slice in preparation for sending it to the RPC server. + var marshalledJSON []byte + marshalledJSON, e = btcjson.MarshalCmd(1, cmd) + if e != nil { + return + } + // Send the JSON-RPC request to the server using the user-specified connection configuration. + result, e = sendPostRequest(marshalledJSON, cx, wallet) + if e != nil { + return + } + return +} + +// newHTTPClient returns a new HTTP client that is configured according to the proxy and TLS settings in the associated +// connection configuration. +func newHTTPClient(cfg *config.Config) (*http.Client, func(), error) { + var dial func(ctx context.Context, network string, + addr string) (net.Conn, error) + ctx, cancel := context.WithCancel(context.Background()) + // Configure proxy if needed. + if cfg.ProxyAddress.V() != "" { + proxy := &socks.Proxy{ + Addr: cfg.ProxyAddress.V(), + Username: cfg.ProxyUser.V(), + Password: cfg.ProxyPass.V(), + } + dial = func(_ context.Context, network string, addr string) ( + net.Conn, error, + ) { + c, e := proxy.Dial(network, addr) + if e != nil { + return nil, e + } + go func() { + out: + for { + select { + case <-ctx.Done(): + if e := c.Close(); E.Chk(e) { + } + break out + } + } + }() + return c, nil + } + } + // Configure TLS if needed. + var tlsConfig *tls.Config + if cfg.ClientTLS.True() && cfg.RPCCert.V() != "" { + pem, e := ioutil.ReadFile(cfg.RPCCert.V()) + if e != nil { + cancel() + return nil, nil, e + } + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(pem) + tlsConfig = &tls.Config{ + RootCAs: pool, + InsecureSkipVerify: cfg.TLSSkipVerify.True(), + } + } + // Create and return the new HTTP client potentially configured with a proxy and TLS. + client := http.Client{ + Transport: &http.Transport{ + Proxy: nil, + DialContext: dial, + TLSClientConfig: tlsConfig, + TLSHandshakeTimeout: 0, + DisableKeepAlives: false, + DisableCompression: false, + MaxIdleConns: 0, + MaxIdleConnsPerHost: 0, + MaxConnsPerHost: 0, + IdleConnTimeout: 0, + ResponseHeaderTimeout: 0, + ExpectContinueTimeout: 0, + TLSNextProto: nil, + ProxyConnectHeader: nil, + MaxResponseHeaderBytes: 0, + WriteBufferSize: 0, + ReadBufferSize: 0, + ForceAttemptHTTP2: false, + }, + } + return &client, cancel, nil +} + +// sendPostRequest sends the marshalled JSON-RPC command using HTTP-POST mode to the server described in the passed +// config struct. It also attempts to unmarshal the response as a JSON-RPC response and returns either the result field +// or the error field depending on whether or not there is an error. +func sendPostRequest( + marshalledJSON []byte, cx *config.Config, wallet bool, +) ([]byte, error) { + // Generate a request to the configured RPC server. + protocol := "http" + if cx.ClientTLS.True() { + protocol = "https" + } + serverAddr := cx.RPCConnect.V() + if wallet { + serverAddr = cx.WalletServer.V() + _, _ = fmt.Fprintln(os.Stderr, "using wallet server", serverAddr) + } + url := protocol + "://" + serverAddr + bodyReader := bytes.NewReader(marshalledJSON) + httpRequest, e := http.NewRequest("POST", url, bodyReader) + if e != nil { + return nil, e + } + httpRequest.Close = true + httpRequest.Header.Set("Content-Type", "application/json") + // Configure basic access authorization. + httpRequest.SetBasicAuth(cx.Username.V(), cx.Password.V()) + // T.Ln(cx.Username.V(), cx.Password.V()) + // Create the new HTTP client that is configured according to the user - specified options and submit the request. + var httpClient *http.Client + var cancel func() + httpClient, cancel, e = newHTTPClient(cx) + if e != nil { + return nil, e + } + httpResponse, e := httpClient.Do(httpRequest) + if e != nil { + return nil, e + } + // close connection + cancel() + // Read the raw bytes and close the response. + respBytes, e := ioutil.ReadAll(httpResponse.Body) + if e := httpResponse.Body.Close(); E.Chk(e) { + e = fmt.Errorf("error reading json reply: %v", e) + return nil, e + } + // Handle unsuccessful HTTP responses + if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { + // Generate a standard error to return if the server body is empty. This should not happen very often, but it's + // better than showing nothing in case the target server has a poor implementation. + if len(respBytes) == 0 { + return nil, fmt.Errorf("%d %s", httpResponse.StatusCode, + http.StatusText(httpResponse.StatusCode), + ) + } + return nil, fmt.Errorf("%s", respBytes) + } + // Unmarshal the response. + var resp btcjson.Response + if e := json.Unmarshal(respBytes, &resp); E.Chk(e) { + return nil, e + } + if resp.Error != nil { + return nil, resp.Error + } + return resp.Result, nil +} + +// ListCommands categorizes and lists all of the usable commands along with their one-line usage. +func ListCommands() (s string) { + const ( + categoryChain uint8 = iota + categoryWallet + numCategories + ) + // Get a list of registered commands and categorize and filter them. + cmdMethods := btcjson.RegisteredCmdMethods() + categorized := make([][]string, numCategories) + for _, method := range cmdMethods { + var e error + var flags btcjson.UsageFlag + if flags, e = btcjson.MethodUsageFlags(method); E.Chk(e) { + continue + } + // Skip the commands that aren't usable from this utility. + if flags&btcjson.UnusableFlags != 0 { + continue + } + var usage string + if usage, e = btcjson.MethodUsageText(method); E.Chk(e) { + continue + } + // Categorize the command based on the usage flags. + category := categoryChain + if flags&btcjson.UFWalletOnly != 0 { + category = categoryWallet + } + categorized[category] = append(categorized[category], usage) + } + // Display the command according to their categories. + categoryTitles := make([]string, numCategories) + categoryTitles[categoryChain] = "Chain Server Commands:" + categoryTitles[categoryWallet] = "Wallet Server Commands (--wallet):" + for category := uint8(0); category < numCategories; category++ { + s += categoryTitles[category] + s += "\n" + for _, usage := range categorized[category] { + s += "\t" + usage + "\n" + } + s += "\n" + } + return +} + +// HelpPrint is the uninitialized help print function +var HelpPrint = func() { + fmt.Println("help has not been overridden") +} + +// CtlMain is the entry point for the pod.Ctl component +func CtlMain(cx *config.Config) { + args := cx.ExtraArgs + if len(args) < 1 { + ListCommands() + os.Exit(1) + } + // Ensure the specified method identifies a valid registered command and is one of the usable types. + method := args[0] + var usageFlags btcjson.UsageFlag + var e error + if usageFlags, e = btcjson.MethodUsageFlags(method); E.Chk(e) { + _, _ = fmt.Fprintf(os.Stderr, "Unrecognized command '%s'\n", method) + HelpPrint() + os.Exit(1) + } + if usageFlags&btcjson.UnusableFlags != 0 { + _, _ = fmt.Fprintf(os.Stderr, "The '%s' command can only be used via websockets\n", method) + HelpPrint() + os.Exit(1) + } + // Convert remaining command line args to a slice of interface values to be passed along as parameters to new + // command creation function. Since some commands, such as submitblock, can involve data which is too large for the + // Operating System to allow as a normal command line parameter, support using '-' as an argument to allow the + // argument to be read from a stdin pipe. + bio := bufio.NewReader(os.Stdin) + params := make([]interface{}, 0, len(args[1:])) + for _, arg := range args[1:] { + if arg == "-" { + var param string + if param, e = bio.ReadString('\n'); E.Chk(e) && e != io.EOF { + _, _ = fmt.Fprintf(os.Stderr, "Failed to read data from stdin: %v\n", e) + os.Exit(1) + } + if e == io.EOF && len(param) == 0 { + _, _ = fmt.Fprintln(os.Stderr, "Not enough lines provided on stdin") + os.Exit(1) + } + param = strings.TrimRight(param, "\r\n") + params = append(params, param) + continue + } + params = append(params, arg) + } + var result []byte + if result, e = Call(cx, cx.UseWallet.True(), method, params...); E.Chk(e) { + return + } + // // Attempt to create the appropriate command using the arguments provided by the user. + // cmd, e := btcjson.NewCmd(method, params...) + // if e != nil { + // E.Ln(e) + // // Show the error along with its error code when it's a json. BTCJSONError as it realistically will always be + // // since the NewCmd function is only supposed to return errors of that type. + // if jerr, ok := err.(btcjson.BTCJSONError); ok { + // fmt.Fprintf(os.Stderr, "%s command: %v (code: %s)\n", method, e, jerr.ErrorCode) + // CommandUsage(method) + // os.Exit(1) + // } + // // The error is not a json.BTCJSONError and this really should not happen. Nevertheless fall back to just + // // showing the error if it should happen due to a bug in the package. + // fmt.Fprintf(os.Stderr, "%s command: %v\n", method, e) + // CommandUsage(method) + // os.Exit(1) + // } + // // Marshal the command into a JSON-RPC byte slice in preparation for sending it to the RPC server. + // marshalledJSON, e := btcjson.MarshalCmd(1, cmd) + // if e != nil { + // E.Ln(e) + // fmt.Println(e) + // os.Exit(1) + // } + // // Send the JSON-RPC request to the server using the user-specified connection configuration. + // result, e := sendPostRequest(marshalledJSON, cx) + // if e != nil { + // E.Ln(e) + // os.Exit(1) + // } + // Choose how to display the result based on its type. + strResult := string(result) + switch { + case strings.HasPrefix(strResult, "{") || strings.HasPrefix(strResult, "["): + var dst bytes.Buffer + if e = json.Indent(&dst, result, "", " "); E.Chk(e) { + fmt.Printf("Failed to format result: %v", e) + os.Exit(1) + } + fmt.Println(dst.String()) + case strings.HasPrefix(strResult, `"`): + var str string + if e = json.Unmarshal(result, &str); E.Chk(e) { + _, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal result: %v", e) + os.Exit(1) + } + fmt.Println(str) + case strResult != "null": + fmt.Println(strResult) + } +} + +// CommandUsage display the usage for a specific command. +func CommandUsage(method string) { + var usage string + var e error + if usage, e = btcjson.MethodUsageText(method); E.Chk(e) { + // This should never happen since the method was already checked before calling this function, but be safe. + fmt.Println("Failed to obtain command usage:", e) + return + } + fmt.Println("Usage:") + fmt.Printf(" %s\n", usage) +} diff --git a/cmd/ctl/log.go b/cmd/ctl/log.go new file mode 100644 index 0000000..75f4b53 --- /dev/null +++ b/cmd/ctl/log.go @@ -0,0 +1,11 @@ +package ctl + + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + diff --git a/cmd/gui/app.go b/cmd/gui/app.go new file mode 100644 index 0000000..37ef98f --- /dev/null +++ b/cmd/gui/app.go @@ -0,0 +1,811 @@ +package gui + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + uberatomic "go.uber.org/atomic" + "golang.org/x/exp/shiny/materialdesign/icons" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/cmd/gui/cfg" + "github.com/p9c/p9/pkg/p9icons" +) + +func (wg *WalletGUI) GetAppWidget() (a *gel.App) { + a = wg.App(wg.Window.Width, uberatomic.NewString("home"), Break1). + SetMainDirection(l.W). + SetLogo(&p9icons.ParallelCoin). + SetAppTitleText("Parallelcoin Wallet") + wg.MainApp = a + wg.config = cfg.New(wg.Window, wg.quit) + wg.configs = wg.config.Config() + a.Pages( + map[string]l.Widget{ + "home": wg.Page( + "home", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{Widget: wg.OverviewPage()}, + }, + ), + "send": wg.Page( + "send", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{Widget: wg.SendPage.Fn}, + }, + ), + "receive": wg.Page( + "receive", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{Widget: wg.ReceivePage.Fn}, + }, + ), + "history": wg.Page( + "history", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{Widget: wg.HistoryPage()}, + }, + ), + "settings": wg.Page( + "settings", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{ + Widget: func(gtx l.Context) l.Dimensions { + return wg.configs.Widget(wg.config)(gtx) + }, + }, + }, + ), + "console": wg.Page( + "console", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{Widget: wg.console.Fn}, + }, + ), + "help": wg.Page( + "help", gel.Widgets{ + gel.WidgetSize{Widget: wg.HelpPage()}, + }, + ), + "log": wg.Page( + "log", gel.Widgets{ + gel.WidgetSize{Widget: a.Placeholder("log")}, + }, + ), + "quit": wg.Page( + "quit", gel.Widgets{ + gel.WidgetSize{ + Widget: func(gtx l.Context) l.Dimensions { + return wg.VFlex(). + SpaceEvenly(). + AlignMiddle(). + Rigid( + wg.H4("are you sure?").Color(wg.MainApp.BodyColorGet()).Alignment(text.Middle).Fn, + ). + Rigid( + wg.Flex(). + // SpaceEvenly(). + Flexed(0.5, gel.EmptyMaxWidth()). + Rigid( + wg.Button( + wg.clickables["quit"].SetClick( + func() { + // interrupt.Request() + wg.gracefulShutdown() + // close(wg.quit) + }, + ), + ).Color("Light").TextScale(5).Text( + "yes!!!", + ).Fn, + ). + Flexed(0.5, gel.EmptyMaxWidth()). + Fn, + ). + Fn(gtx) + }, + }, + }, + ), + // "goroutines": wg.Page( + // "log", p9.Widgets{ + // // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + // + // p9.WidgetSize{ + // Widget: func(gtx l.Context) l.Dimensions { + // le := func(gtx l.Context, index int) l.Dimensions { + // return wg.State.goroutines[index](gtx) + // } + // return func(gtx l.Context) l.Dimensions { + // return wg.ButtonInset( + // 0.25, + // wg.Fill( + // "DocBg", + // wg.lists["recent"]. + // Vertical(). + // // Background("DocBg").Color("DocText").Active("Primary"). + // Length(len(wg.State.goroutines)). + // ListElement(le). + // Fn, + // ).Fn, + // ). + // Fn(gtx) + // }(gtx) + // // wg.NodeRunCommandChan <- "stop" + // // consume.Kill(wg.Worker) + // // consume.Kill(wg.cx.StateCfg.Miner) + // // close(wg.cx.NodeKill) + // // close(wg.cx.KillAll) + // // time.Sleep(time.Second*3) + // // interrupt.Request() + // // os.Exit(0) + // // return l.Dimensions{} + // }, + // }, + // }, + // ), + "mining": wg.Page( + "mining", gel.Widgets{ + gel.WidgetSize{ + Widget: a.Placeholder("mining"), + }, + }, + ), + "explorer": wg.Page( + "explorer", gel.Widgets{ + gel.WidgetSize{ + Widget: a.Placeholder("explorer"), + }, + }, + ), + }, + ) + a.SideBar( + []l.Widget{ + // wg.SideBarButton(" ", " ", 11), + wg.SideBarButton("home", "home", 0), + wg.SideBarButton("send", "send", 1), + wg.SideBarButton("receive", "receive", 2), + wg.SideBarButton("history", "history", 3), + // wg.SideBarButton("explorer", "explorer", 6), + // wg.SideBarButton("mining", "mining", 7), + wg.SideBarButton("console", "console", 9), + wg.SideBarButton("settings", "settings", 5), + // wg.SideBarButton("log", "log", 10), + wg.SideBarButton("help", "help", 8), + // wg.SideBarButton(" ", " ", 11), + // wg.SideBarButton("quit", "quit", 11), + }, + ) + a.ButtonBar( + []l.Widget{ + + // gel.EmptyMaxWidth(), + // wg.PageTopBarButton( + // "goroutines", 0, &icons.ActionBugReport, func(name string) { + // wg.App.ActivePage(name) + // }, a, "", + // ), + wg.PageTopBarButton( + "help", 1, &icons.ActionHelp, func(name string) { + wg.MainApp.ActivePage(name) + }, a, "", + ), + wg.PageTopBarButton( + "home", 4, &icons.ActionLockOpen, func(name string) { + wg.unlockPassword.Wipe() + wg.unlockPassword.Focus() + wg.WalletWatcher.Q() + // if wg.WalletClient != nil { + // wg.WalletClient.Disconnect() + // wg.WalletClient = nil + // } + // wg.wallet.Stop() + // wg.node.Stop() + wg.State.SetActivePage("home") + wg.unlockPage.ActivePage("home") + wg.stateLoaded.Store(false) + wg.ready.Store(false) + }, a, "green", + ), + // wg.Flex().Rigid(wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn).Fn, + // wg.PageTopBarButton( + // "quit", 3, &icons.ActionExitToApp, func(name string) { + // wg.MainApp.ActivePage(name) + // }, a, "", + // ), + }, + ) + a.StatusBar( + []l.Widget{ + // func(gtx l.Context) l.Dimensions { return wg.RunStatusPanel(gtx) }, + // wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + // wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + wg.RunStatusPanel, + }, + []l.Widget{ + // gel.EmptyMaxWidth(), + wg.StatusBarButton( + "console", 3, &p9icons.Terminal, func(name string) { + wg.MainApp.ActivePage(name) + }, a, + ), + wg.StatusBarButton( + "log", 4, &icons.ActionList, func(name string) { + D.Ln("click on button", name) + if wg.MainApp.MenuOpen { + wg.MainApp.MenuOpen = false + } + wg.MainApp.ActivePage(name) + }, a, + ), + wg.StatusBarButton( + "settings", 5, &icons.ActionSettings, func(name string) { + D.Ln("click on button", name) + if wg.MainApp.MenuOpen { + wg.MainApp.MenuOpen = false + } + wg.MainApp.ActivePage(name) + }, a, + ), + // wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + }, + ) + // a.PushOverlay(wg.toasts.DrawToasts()) + // a.PushOverlay(wg.dialog.DrawDialog()) + return +} + +func (wg *WalletGUI) Page(title string, widget gel.Widgets) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return wg.VFlex(). + // SpaceEvenly(). + Rigid( + wg.Responsive( + wg.Size.Load(), gel.Widgets{ + // p9.WidgetSize{ + // Widget: a.ButtonInset(0.25, a.H5(title).Color(wg.App.BodyColorGet()).Fn).Fn, + // }, + gel.WidgetSize{ + // Size: 800, + Widget: gel.EmptySpace(0, 0), + // a.ButtonInset(0.25, a.Caption(title).Color(wg.BodyColorGet()).Fn).Fn, + }, + }, + ).Fn, + ). + Flexed( + 1, + wg.Inset( + 0.25, + wg.Responsive(wg.Size.Load(), widget).Fn, + ).Fn, + ).Fn(gtx) + } +} + +func (wg *WalletGUI) SideBarButton(title, page string, index int) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + var scale float32 + scale = gel.Scales["H6"] + var color string + background := "Transparent" + color = "DocText" + var ins float32 = 0.5 + // var hl = false + if wg.MainApp.ActivePageGet() == page || wg.MainApp.PreRendering { + background = "PanelBg" + scale = gel.Scales["H6"] + color = "DocText" + // ins = 0.5 + // hl = true + } + if title == " " { + scale = gel.Scales["H6"] / 2 + } + max := int(wg.MainApp.SideBarSize.V) + if max > 0 { + gtx.Constraints.Max.X = max + gtx.Constraints.Min.X = max + } + // D.Ln("sideMAXXXXXX!!", max) + return wg.Direction().E().Embed( + wg.ButtonLayout(wg.sidebarButtons[index]). + CornerRadius(scale).Corners(0). + Background(background). + Embed( + wg.Inset( + ins, + func(gtx l.Context) l.Dimensions { + return wg.H5(title). + Color(color). + Alignment(text.End). + Fn(gtx) + }, + ).Fn, + ). + SetClick( + func() { + if wg.MainApp.MenuOpen { + wg.MainApp.MenuOpen = false + } + wg.MainApp.ActivePage(page) + }, + ). + Fn, + ). + Fn(gtx) + } +} + +func (wg *WalletGUI) PageTopBarButton( + name string, index int, ico *[]byte, onClick func(string), app *gel.App, + highlightColor string, +) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + background := "Transparent" + // background := app.TitleBarBackgroundGet() + color := app.MenuColorGet() + + if app.ActivePageGet() == name { + color = "PanelText" + // background = "scrim" + background = "PanelBg" + } + // if name == "home" { + // background = "scrim" + // } + if highlightColor != "" { + color = highlightColor + } + ic := wg.Icon(). + Scale(gel.Scales["H5"]). + Color(color). + Src(ico). + Fn + return wg.Flex().Rigid( + // wg.ButtonInset(0.25, + wg.ButtonLayout(wg.buttonBarButtons[index]). + CornerRadius(0). + Embed( + wg.Inset( + 0.375, + ic, + ).Fn, + ). + Background(background). + SetClick(func() { onClick(name) }). + Fn, + // ).Fn, + ).Fn(gtx) + } +} + +func (wg *WalletGUI) StatusBarButton( + name string, + index int, + ico *[]byte, + onClick func(string), + app *gel.App, +) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + background := app.StatusBarBackgroundGet() + color := app.StatusBarColorGet() + if app.ActivePageGet() == name { + // background, color = color, background + background = "PanelBg" + // color = "Danger" + } + ic := wg.Icon(). + Scale(gel.Scales["H5"]). + Color(color). + Src(ico). + Fn + return wg.Flex(). + Rigid( + wg.ButtonLayout(wg.statusBarButtons[index]). + CornerRadius(0). + Embed( + wg.Inset(0.25, ic).Fn, + ). + Background(background). + SetClick(func() { onClick(name) }). + Fn, + ).Fn(gtx) + } +} + +func (wg *WalletGUI) SetNodeRunState(b bool) { + go func() { + D.Ln("node run state is now", b) + if b { + wg.node.Start() + } else { + wg.node.Stop() + } + }() +} + +func (wg *WalletGUI) SetWalletRunState(b bool) { + go func() { + D.Ln("node run state is now", b) + if b { + wg.wallet.Start() + } else { + wg.wallet.Stop() + } + }() +} + +func (wg *WalletGUI) RunStatusPanel(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + t, f := &p9icons.Link, &p9icons.LinkOff + var runningIcon *[]byte + if wg.node.Running() { + runningIcon = t + } else { + runningIcon = f + } + miningIcon := &p9icons.Mine + if !wg.miner.Running() { + miningIcon = &p9icons.NoMine + } + controllerIcon := &icons.NotificationSyncDisabled + if wg.cx.Config.Controller.True() { + controllerIcon = &icons.NotificationSync + } + discoverColor := + "DocText" + discoverIcon := + &icons.DeviceWiFiTethering + if wg.cx.Config.Discovery.False() { + discoverIcon = + &icons.CommunicationPortableWiFiOff + discoverColor = + "scrim" + } + clr := "scrim" + if wg.cx.Config.Controller.True() { + clr = "DocText" + } + clr2 := "DocText" + if wg.cx.Config.GenThreads.V() == 0 { + clr2 = "scrim" + } + // background := wg.App.StatusBarBackgroundGet() + color := wg.MainApp.StatusBarColorGet() + ic := wg.Icon(). + Scale(gel.Scales["H5"]). + Color(color). + Src(&icons.NavigationRefresh). + Fn + return wg.Flex().AlignMiddle(). + Rigid( + wg.ButtonLayout(wg.statusBarButtons[0]). + CornerRadius(0). + Embed( + wg.Inset( + 0.25, + wg.Icon(). + Scale(gel.Scales["H5"]). + Color("DocText"). + Src(runningIcon). + Fn, + ).Fn, + ). + Background(wg.MainApp.StatusBarBackgroundGet()). + SetClick( + func() { + go func() { + D.Ln("clicked node run control button", wg.node.Running()) + // wg.toggleNode() + wg.unlockPassword.Wipe() + wg.unlockPassword.Focus() + if wg.node.Running() { + if wg.wallet.Running() { + wg.WalletWatcher.Q() + } + wg.node.Stop() + wg.ready.Store(false) + wg.stateLoaded.Store(false) + wg.State.SetActivePage("home") + } else { + wg.node.Start() + // wg.ready.Store(true) + // wg.stateLoaded.Store(true) + } + }() + }, + ). + Fn, + ). + Rigid( + wg.Inset( + 0.33, + wg.Body1(fmt.Sprintf("%d", wg.State.bestBlockHeight.Load())). + Font("go regular").TextScale(gel.Scales["Caption"]). + Color("DocText"). + Fn, + ).Fn, + ). + Rigid( + wg.ButtonLayout(wg.statusBarButtons[6]). + CornerRadius(0). + Embed( + wg.Inset( + 0.25, + wg.Icon(). + Scale(gel.Scales["H5"]). + Color(discoverColor). + Src(discoverIcon). + Fn, + ).Fn, + ). + Background(wg.MainApp.StatusBarBackgroundGet()). + SetClick( + func() { + go func() { + wg.cx.Config.Discovery.Flip() + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + I.Ln("discover enabled:", + wg.cx.Config.Discovery.True()) + }() + }, + ). + Fn, + ). + Rigid( + wg.Inset( + 0.33, + wg.Caption(fmt.Sprintf("%d LAN %d", len(wg.otherNodes), wg.peerCount.Load())). + Font("go regular"). + Color("DocText"). + Fn, + ).Fn, + ). + Rigid( + wg.ButtonLayout(wg.statusBarButtons[7]). + CornerRadius(0). + Embed( + wg. + Inset( + 0.25, wg. + Icon(). + Scale(gel.Scales["H5"]). + Color(clr). + Src(controllerIcon).Fn, + ).Fn, + ). + Background(wg.MainApp.StatusBarBackgroundGet()). + SetClick( + func() { + if wg.ChainClient != nil && !wg.ChainClient.Disconnected() { + wg.cx.Config.Controller.Flip() + I.Ln("controller running:", + wg.cx.Config.Controller.True()) + var e error + if e = wg.ChainClient.SetGenerate( + wg.cx.Config.Controller.True(), + wg.cx.Config.GenThreads.V(), + ); !E.Chk(e) { + } + } + // // wg.toggleMiner() + // go func() { + // if wg.miner.Running() { + // *wg.cx.Config.Generate = false + // wg.miner.Stop() + // } else { + // wg.miner.Start() + // *wg.cx.Config.Generate = true + // } + // save.Save(wg.cx.Config) + // }() + }, + ). + Fn, + ). + Rigid( + wg.ButtonLayout(wg.statusBarButtons[1]). + CornerRadius(0). + Embed( + wg.Inset( + 0.25, wg. + Icon(). + Scale(gel.Scales["H5"]). + Color(clr2). + Src(miningIcon).Fn, + ).Fn, + ). + Background(wg.MainApp.StatusBarBackgroundGet()). + SetClick( + func() { + // wg.toggleMiner() + go func() { + if wg.cx.Config.GenThreads.V() != 0 { + if wg.miner.Running() { + wg.cx.Config.Generate.F() + wg.miner.Stop() + } else { + wg.miner.Start() + wg.cx.Config.Generate.T() + } + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + } + }() + }, + ). + Fn, + ). + Rigid( + func(gtx l.Context) l.Dimensions { + return wg.incdecs["generatethreads"]. + // Color("DocText"). + // Background(wg.MainApp.StatusBarBackgroundGet()). + Fn(gtx) + }, + ). + Rigid( + func(gtx l.Context) l.Dimensions { + if !wg.wallet.Running() { + return l.Dimensions{} + } + + return wg.Flex(). + Rigid( + wg.ButtonLayout(wg.statusBarButtons[2]). + CornerRadius(0). + Embed( + wg.Inset(0.25, ic).Fn, + ). + Background(wg.MainApp.StatusBarBackgroundGet()). + SetClick( + func() { + D.Ln("clicked reset wallet button") + go func() { + var e error + wasRunning := wg.wallet.Running() + D.Ln("was running", wasRunning) + if wasRunning { + wg.wallet.Stop() + } + args := []string{ + os.Args[0], + "DD"+ + wg.cx.Config.DataDir.V(), + "pipelog", + "walletpass"+ + wg.cx.Config.WalletPass.V(), + "wallet", + "drophistory", + } + runner := exec.Command(args[0], args[1:]...) + runner.Stderr = os.Stderr + runner.Stdout = os.Stderr + if e = wg.writeWalletCookie(); E.Chk(e) { + } + if e = runner.Run(); E.Chk(e) { + } + if wasRunning { + wg.wallet.Start() + } + }() + }, + ). + Fn, + ).Fn(gtx) + }, + ). + Fn(gtx) + }(gtx) +} + +func (wg *WalletGUI) writeWalletCookie() (e error) { + // for security with apps launching the wallet, the public password can be set with a file that is deleted after + walletPassPath := filepath.Join(wg.cx.Config.DataDir.V(), wg.cx.ActiveNet.Name, "wp.txt") + D.Ln("runner", walletPassPath) + b := wg.cx.Config.WalletPass.Bytes() + if e = ioutil.WriteFile(walletPassPath, b, 0700); E.Chk(e) { + } + D.Ln("created password cookie") + return +} + +// +// func (wg *WalletGUI) toggleNode() { +// if wg.node.Running() { +// wg.node.Stop() +// *wg.cx.Config.NodeOff = true +// } else { +// wg.node.Start() +// *wg.cx.Config.NodeOff = false +// } +// save.Save(wg.cx.Config) +// } +// +// func (wg *WalletGUI) startNode() { +// if !wg.node.Running() { +// wg.node.Start() +// } +// D.Ln("startNode") +// } +// +// func (wg *WalletGUI) stopNode() { +// if wg.wallet.Running() { +// wg.stopWallet() +// wg.unlockPassword.Wipe() +// // wg.walletLocked.Store(true) +// } +// if wg.node.Running() { +// wg.node.Stop() +// } +// D.Ln("stopNode") +// } +// +// func (wg *WalletGUI) toggleMiner() { +// if wg.miner.Running() { +// wg.miner.Stop() +// *wg.cx.Config.Generate = false +// } +// if !wg.miner.Running() && *wg.cx.Config.GenThreads > 0 { +// wg.miner.Start() +// *wg.cx.Config.Generate = true +// } +// save.Save(wg.cx.Config) +// } +// +// func (wg *WalletGUI) startMiner() { +// if *wg.cx.Config.GenThreads == 0 && wg.miner.Running() { +// wg.stopMiner() +// D.Ln("was zero threads") +// } else { +// wg.miner.Start() +// D.Ln("startMiner") +// } +// } +// +// func (wg *WalletGUI) stopMiner() { +// if wg.miner.Running() { +// wg.miner.Stop() +// } +// D.Ln("stopMiner") +// } +// +// func (wg *WalletGUI) toggleWallet() { +// if wg.wallet.Running() { +// wg.stopWallet() +// *wg.cx.Config.WalletOff = true +// } else { +// wg.startWallet() +// *wg.cx.Config.WalletOff = false +// } +// save.Save(wg.cx.Config) +// } +// +// func (wg *WalletGUI) startWallet() { +// if !wg.node.Running() { +// wg.startNode() +// } +// if !wg.wallet.Running() { +// wg.wallet.Start() +// wg.unlockPassword.Wipe() +// // wg.walletLocked.Store(false) +// } +// D.Ln("startWallet") +// } +// +// func (wg *WalletGUI) stopWallet() { +// if wg.wallet.Running() { +// wg.wallet.Stop() +// // wg.unlockPassword.Wipe() +// // wg.walletLocked.Store(true) +// } +// wg.unlockPassword.Wipe() +// D.Ln("stopWallet") +// } diff --git a/cmd/gui/cfg/config.go b/cmd/gui/cfg/config.go new file mode 100644 index 0000000..8d2f963 --- /dev/null +++ b/cmd/gui/cfg/config.go @@ -0,0 +1,588 @@ +package cfg + +import ( + "sort" + + "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gel/gio/text" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/pkg/gel" +) + +type Item struct { + slug string + typ string + label string + description string + widget string + dataType string + options []string + Slot interface{} +} + +func (it *Item) Item(ng *Config) l.Widget { + return func(gtx l.Context) l.Dimensions { + return ng.Theme.VFlex().Rigid( + ng.H6(it.label).Fn, + ).Fn(gtx) + } +} + +type ItemMap map[string]*Item + +type GroupsMap map[string]ItemMap + +type ListItem struct { + name string + widget func() []l.Widget +} + +type ListItems []ListItem + +func (l ListItems) Len() int { + return len(l) +} + +func (l ListItems) Less(i, j int) bool { + return l[i].name < l[j].name +} + +func (l ListItems) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +type List struct { + name string + items ListItems +} + +type Lists []List + +func (l Lists) Len() int { + return len(l) +} + +func (l Lists) Less(i, j int) bool { + return l[i].name < l[j].name +} + +func (l Lists) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (c *Config) Config() GroupsMap { + // schema := podcfg.GetConfigSchema(c.cx.Config) + tabNames := make(GroupsMap) + // // tabs := make(p9.WidgetMap) + // for i := range schema.Groups { + // for j := range schema.Groups[i].Fields { + // sgf := schema.Groups[i].Fields[j] + // if _, ok := tabNames[sgf.Group]; !ok { + // tabNames[sgf.Group] = make(ItemMap) + // } + // tabNames[sgf.Group][sgf.Slug] = &Item{ + // slug: sgf.Slug, + // typ: sgf.Type, + // label: sgf.Label, + // description: sgf.Title, + // widget: sgf.Widget, + // dataType: sgf.Datatype, + // options: sgf.Options, + // Slot: c.cx.ConfigMap[sgf.Slug], + // } + // // D.S(sgf) + // // create all the necessary widgets required before display + // tgs := tabNames[sgf.Group][sgf.Slug] + // switch sgf.Widget { + // case "toggle": + // c.Bools[sgf.Slug] = c.Bool(*tgs.Slot.(*bool)).SetOnChange( + // func(b bool) { + // D.Ln(sgf.Slug, "submitted", b) + // bb := c.cx.ConfigMap[sgf.Slug].(*bool) + // *bb = b + // podcfg.Save(c.cx.Config) + // if sgf.Slug == "DarkTheme" { + // c.Theme.Colors.SetTheme(b) + // } + // }, + // ) + // case "integer": + // c.inputs[sgf.Slug] = c.Input( + // fmt.Sprint(*tgs.Slot.(*int)), sgf.Slug, "DocText", "DocBg", "PanelBg", func(txt string) { + // D.Ln(sgf.Slug, "submitted", txt) + // i := c.cx.ConfigMap[sgf.Slug].(*int) + // if n, e := strconv.Atoi(txt); !E.Chk(e) { + // *i = n + // } + // podcfg.Save(c.cx.Config) + // }, nil, + // ) + // case "time": + // c.inputs[sgf.Slug] = c.Input( + // fmt.Sprint( + // *tgs.Slot.(*time. + // Duration), + // ), sgf.Slug, "DocText", "DocBg", "PanelBg", func(txt string) { + // D.Ln(sgf.Slug, "submitted", txt) + // tt := c.cx.ConfigMap[sgf.Slug].(*time.Duration) + // if d, e := time.ParseDuration(txt); !E.Chk(e) { + // *tt = d + // } + // podcfg.Save(c.cx.Config) + // }, nil, + // ) + // case "float": + // c.inputs[sgf.Slug] = c.Input( + // strconv.FormatFloat( + // *tgs.Slot.( + // *float64), 'f', -1, 64, + // ), sgf.Slug, "DocText", "DocBg", "PanelBg", func(txt string) { + // D.Ln(sgf.Slug, "submitted", txt) + // ff := c.cx.ConfigMap[sgf.Slug].(*float64) + // if f, e := strconv.ParseFloat(txt, 64); !E.Chk(e) { + // *ff = f + // } + // podcfg.Save(c.cx.Config) + // }, nil, + // ) + // case "string": + // c.inputs[sgf.Slug] = c.Input( + // *tgs.Slot.(*string), sgf.Slug, "DocText", "DocBg", "PanelBg", func(txt string) { + // D.Ln(sgf.Slug, "submitted", txt) + // ss := c.cx.ConfigMap[sgf.Slug].(*string) + // *ss = txt + // podcfg.Save(c.cx.Config) + // }, nil, + // ) + // case "password": + // c.passwords[sgf.Slug] = c.Password( + // "password", + // tgs.Slot.(*string), "DocText", "DocBg", "PanelBg", + // func(txt string) { + // D.Ln(sgf.Slug, "submitted", txt) + // pp := c.cx.ConfigMap[sgf.Slug].(*string) + // *pp = txt + // podcfg.Save(c.cx.Config) + // }, + // ) + // case "multi": + // c.multis[sgf.Slug] = c.Multiline( + // tgs.Slot.(*cli.StringSlice), "DocText", "DocBg", "PanelBg", 30, func(txt []string) { + // D.Ln(sgf.Slug, "submitted", txt) + // sss := c.cx.ConfigMap[sgf.Slug].(*cli.StringSlice) + // *sss = txt + // podcfg.Save(c.cx.Config) + // }, + // ) + // // c.multis[sgf.Slug] + // case "radio": + // c.checkables[sgf.Slug] = c.Checkable() + // for i := range sgf.Options { + // c.checkables[sgf.Slug+sgf.Options[i]] = c.Checkable() + // } + // txt := *tabNames[sgf.Group][sgf.Slug].Slot.(*string) + // c.enums[sgf.Slug] = c.Enum().SetValue(txt).SetOnChange( + // func(value string) { + // rr := c.cx.ConfigMap[sgf.Slug].(*string) + // *rr = value + // podcfg.Save(c.cx.Config) + // }, + // ) + // c.lists[sgf.Slug] = c.List() + // } + // } + // } + + // D.S(tabNames) + return tabNames // .Widget(c) + // return func(gtx l.Context) l.Dimensions { + // return l.Dimensions{} + // } +} + +func (gm GroupsMap) Widget(ng *Config) l.Widget { + // _, file, line, _ := runtime.Caller(2) + // D.F("%s:%d", file, line) + var groups Lists + for i := range gm { + var li ListItems + gmi := gm[i] + for j := range gmi { + gmij := gmi[j] + li = append( + li, ListItem{ + name: j, + widget: func() []l.Widget { + return ng.RenderConfigItem(gmij, len(li)) + }, + // }, + }, + ) + } + sort.Sort(li) + groups = append(groups, List{name: i, items: li}) + } + sort.Sort(groups) + var out []l.Widget + first := true + for i := range groups { + // D.Ln(groups[i].name) + g := groups[i] + if !first { + // put a space between the sections + out = append( + out, func(gtx l.Context) l.Dimensions { + dims := ng.VFlex(). + // Rigid( + // // ng.Inset(0.25, + // ng.Fill("DocBg", l.Center, ng.TextSize.True, l.S, ng.Inset(0.25, + // gel.EmptyMaxWidth()).Fn, + // ).Fn, + // // ).Fn, + // ). + Rigid(ng.Inset(0.25, gel.EmptyMaxWidth()).Fn). + // Rigid( + // // ng.Inset(0.25, + // ng.Fill("DocBg", l.Center, ng.TextSize.True, l.N, ng.Inset(0.25, + // gel.EmptyMaxWidth()).Fn, + // ).Fn, + // // ).Fn, + // ). + Fn(gtx) + // ng.Fill("PanelBg", gel.EmptySpace(gtx.Constraints.Max.X, gtx.Constraints.Max.Y), l.Center, 0).Fn(gtx) + return dims + }, + ) + // out = append(out, func(gtx l.Context) l.Dimensions { + // return ng.th.ButtonInset(0.25, p9.EmptySpace(0, 0)).Fn(gtx) + // }) + } else { + first = false + } + // put in the header + out = append( + out, + ng.Fill( + "Primary", l.Center, ng.TextSize.V*2, 0, ng.Flex().Flexed( + 1, + ng.Inset( + 0.75, + ng.H3(g.name). + Color("DocText"). + Alignment(text.Start). + Fn, + ).Fn, + ).Fn, + ).Fn, + ) + // out = append(out, func(gtx l.Context) l.Dimensions { + // return ng.th.Fill("PanelBg", + // ng.th.ButtonInset(0.25, + // ng.th.Flex().Flexed(1, + // p9.EmptyMaxWidth(), + // ).Fn, + // ).Fn, + // ).Fn(gtx) + // }) + // add the widgets + for j := range groups[i].items { + gi := groups[i].items[j] + for x := range gi.widget() { + k := x + out = append( + out, func(gtx l.Context) l.Dimensions { + if k < len(gi.widget()) { + return ng.Fill( + "DocBg", l.Center, ng.TextSize.V, 0, ng.Flex(). + // Rigid( + // ng.Inset(0.25, gel.EmptySpace(0, 0)).Fn, + // ). + Rigid( + ng.Inset( + 0.25, + gi.widget()[k], + ).Fn, + ).Fn, + ).Fn(gtx) + } + return l.Dimensions{} + }, + ) + } + } + } + le := func(gtx l.Context, index int) l.Dimensions { + return out[index](gtx) + } + return func(gtx l.Context) l.Dimensions { + // clip.UniformRRect(f32.Rectangle{ + // Max: f32.Pt(float32(gtx.Constraints.Max.X), float32(gtx.Constraints.Max.Y)), + // }, ng.TextSize.True/2).Add(gtx.Ops) + return ng.Fill( + "DocBg", l.Center, ng.TextSize.V, 0, ng.Inset( + 0.25, + ng.lists["settings"]. + Vertical(). + Length(len(out)). + // Background("PanelBg"). + // Color("DocBg"). + // Active("Primary"). + ListElement(le). + Fn, + ).Fn, + ).Fn(gtx) + } +} + +// RenderConfigItem renders a config item. It takes a position variable which tells it which index it begins on +// the bigger config widget list, with this and its current data set the multi can insert and delete elements above +// its add button without rerendering the config item or worse, the whole config widget +func (c *Config) RenderConfigItem(item *Item, position int) []l.Widget { + switch item.widget { + case "toggle": + return c.RenderToggle(item) + case "integer": + return c.RenderInteger(item) + case "time": + return c.RenderTime(item) + case "float": + return c.RenderFloat(item) + case "string": + return c.RenderString(item) + case "password": + return c.RenderPassword(item) + case "multi": + return c.RenderMulti(item, position) + case "radio": + return c.RenderRadio(item) + } + D.Ln("fallthrough", item.widget) + return []l.Widget{func(l.Context) l.Dimensions { return l.Dimensions{} }} +} + +func (c *Config) RenderToggle(item *Item) []l.Widget { + return []l.Widget{ + func(gtx l.Context) l.Dimensions { + return c.Inset( + 0.25, + c.Flex(). + Rigid( + c.Switch(c.Bools[item.slug]).DisabledColor("Light").Fn, + ). + Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ).Fn(gtx) + }, + } +} + +func (c *Config) RenderInteger(item *Item) []l.Widget { + return []l.Widget{ + func(gtx l.Context) l.Dimensions { + return c.Inset( + 0.25, + c.Flex().Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.inputs[item.slug].Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ).Fn(gtx) + }, + } +} + +func (c *Config) RenderTime(item *Item) []l.Widget { + return []l.Widget{ + func(gtx l.Context) l.Dimensions { + return c.Inset( + 0.25, + c.Flex().Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.inputs[item.slug].Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ). + Fn(gtx) + }, + } +} + +func (c *Config) RenderFloat(item *Item) []l.Widget { + return []l.Widget{ + func(gtx l.Context) l.Dimensions { + return c.Inset( + 0.25, + c.Flex().Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.inputs[item.slug].Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ). + Fn(gtx) + }, + } +} + +func (c *Config) RenderString(item *Item) []l.Widget { + return []l.Widget{ + c.Inset( + 0.25, + c.Flex().Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.inputs[item.slug].Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ). + Fn, + } +} + +func (c *Config) RenderPassword(item *Item) []l.Widget { + return []l.Widget{ + c.Inset( + 0.25, + c.Flex().Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.passwords[item.slug].Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ). + Fn, + } +} + +func (c *Config) RenderMulti(item *Item, position int) []l.Widget { + // D.Ln("rendering multi") + // c.multis[item.slug]. + w := []l.Widget{ + func(gtx l.Context) l.Dimensions { + return c.Inset( + 0.25, + c.Flex().Flexed( + 1, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.Caption(item.description).Fn, + ).Fn, + ).Fn, + ). + Fn(gtx) + }, + } + widgets := c.multis[item.slug].Widgets() + // D.Ln(widgets) + w = append(w, widgets...) + return w +} + +func (c *Config) RenderRadio(item *Item) []l.Widget { + out := func(gtx l.Context) l.Dimensions { + var options []l.Widget + for i := range item.options { + var color string + color = "PanelBg" + if c.enums[item.slug].Value() == item.options[i] { + color = "Primary" + } + options = append( + options, + c.RadioButton( + c.checkables[item.slug+item.options[i]]. + Color("DocText"). + IconColor(color). + CheckedStateIcon(&icons.ToggleRadioButtonChecked). + UncheckedStateIcon(&icons.ToggleRadioButtonUnchecked), + c.enums[item.slug], item.options[i], item.options[i], + ).Fn, + ) + } + return c.Inset( + 0.25, + c.VFlex(). + Rigid( + c.Body1(item.label).Fn, + ). + Rigid( + c.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + gtx.Constraints.Max.X = int(c.Theme.TextSize.Scale(10).V) + return c.lists[item.slug].DisableScroll(true).Slice(gtx, options...)(gtx) + // // return c.lists[item.slug].Length(len(options)).Vertical().ListElement(func(gtx l.Context, index int) l.Dimensions { + // // return options[index](gtx) + // // }).Fn(gtx) + // return c.lists[item.slug].Slice(gtx, options...)(gtx) + // // return l.Dimensions{} + }, + ). + Flexed( + 1, + c.Caption(item.description).Fn, + ). + Fn, + ).Fn, + ). + Fn(gtx) + } + return []l.Widget{out} +} diff --git a/cmd/gui/cfg/log.go b/cmd/gui/cfg/log.go new file mode 100644 index 0000000..724907d --- /dev/null +++ b/cmd/gui/cfg/log.go @@ -0,0 +1,43 @@ +package cfg + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/gui/cfg/main.go b/cmd/gui/cfg/main.go new file mode 100644 index 0000000..0b744f7 --- /dev/null +++ b/cmd/gui/cfg/main.go @@ -0,0 +1,62 @@ +package cfg + +import ( + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/gel" +) + +func New(w *gel.Window, killAll qu.C) *Config { + cfg := &Config{ + Window: w, + // cx: cx, + quit: killAll, + } + // cfg.Theme = cx.App + return cfg.Init() +} + +type Config struct { + // cx *state.State + *gel.Window + Bools map[string]*gel.Bool + lists map[string]*gel.List + enums map[string]*gel.Enum + checkables map[string]*gel.Checkable + clickables map[string]*gel.Clickable + editors map[string]*gel.Editor + inputs map[string]*gel.Input + multis map[string]*gel.Multi + configs GroupsMap + passwords map[string]*gel.Password + quit qu.C +} + +func (c *Config) Init() *Config { + c.Theme.SetDarkTheme(c.Theme.Dark.True()) + c.enums = map[string]*gel.Enum{ + // "runmode": ng.th.Enum().SetValue(ng.runMode), + } + c.Bools = map[string]*gel.Bool{ + // "runstate": ng.th.Bool(false).SetOnChange(func(b bool) { + // D.Ln("run state is now", b) + // }), + } + c.lists = map[string]*gel.List{ + // "overview": ng.th.List(), + "settings": c.List(), + } + c.clickables = map[string]*gel.Clickable{ + // "quit": ng.th.Clickable(), + } + c.checkables = map[string]*gel.Checkable{ + // "runmodenode": ng.th.Checkable(), + // "runmodewallet": ng.th.Checkable(), + // "runmodeshell": ng.th.Checkable(), + } + c.editors = make(map[string]*gel.Editor) + c.inputs = make(map[string]*gel.Input) + c.multis = make(map[string]*gel.Multi) + c.passwords = make(map[string]*gel.Password) + return c +} diff --git a/cmd/gui/console.go b/cmd/gui/console.go new file mode 100644 index 0000000..fec27f8 --- /dev/null +++ b/cmd/gui/console.go @@ -0,0 +1,592 @@ +package gui + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/atotto/clipboard" + "golang.org/x/exp/shiny/materialdesign/icons" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + ctl2 "github.com/p9c/p9/cmd/ctl" + + icons2 "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gel" +) + +type Console struct { + *gel.Window + output []l.Widget + outputList *gel.List + editor *gel.Editor + clearClickable *gel.Clickable + clearButton *gel.IconButton + copyClickable *gel.Clickable + copyButton *gel.IconButton + pasteClickable *gel.Clickable + pasteButton *gel.IconButton + submitFunc func(txt string) + clickables []*gel.Clickable +} + +var findSpaceRegexp = regexp.MustCompile(`\s+`) + +func (wg *WalletGUI) ConsolePage() *Console { + D.Ln("running ConsolePage") + c := &Console{ + Window: wg.Window, + editor: wg.Editor().SingleLine().Submit(true), + clearClickable: wg.Clickable(), + copyClickable: wg.Clickable(), + pasteClickable: wg.Clickable(), + outputList: wg.List().ScrollToEnd(), + } + c.submitFunc = func(txt string) { + go func() { + D.Ln("submit", txt) + c.output = append( + c.output, + func(gtx l.Context) l.Dimensions { + return wg.VFlex(). + Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid( + wg.Flex(). + Flexed( + 1, + wg.Body2(txt).Color("DocText").Font("bariol bold").Fn, + ). + Fn, + ).Fn(gtx) + }, + ) + c.editor.SetText("") + split := strings.Split(txt, " ") + method, args := split[0], split[1:] + var params []interface{} + var e error + var result []byte + var o string + var errString, prev string + for i := range args { + params = append(params, args[i]) + } + if method == "clear" || method == "cls" { + // clear the list of display widgets + c.output = c.output[:0] + // free up the pool widgets used in the current output + for i := range c.clickables { + wg.WidgetPool.FreeClickable(c.clickables[i]) + } + c.clickables = c.clickables[:0] + return + } + if method == "help" { + if len(args) == 0 { + D.Ln("rpc called help") + var result1, result2 []byte + if result1, e = ctl2.Call(wg.cx.Config, false, method, params...); E.Chk(e) { + } + r1 := string(result1) + if r1, e = strconv.Unquote(r1); E.Chk(e) { + } + o = r1 + "\n" + if result2, e = ctl2.Call(wg.cx.Config, true, method, params...); E.Chk(e) { + } + r2 := string(result2) + if r2, e = strconv.Unquote(r2); E.Chk(e) { + } + o += r2 + "\n" + splitted := strings.Split(o, "\n") + sort.Strings(splitted) + var dedup []string + for i := range splitted { + if i > 0 { + if splitted[i] != prev { + dedup = append(dedup, splitted[i]) + } + } + prev = splitted[i] + } + o = strings.Join(dedup, "\n") + if errString != "" { + o += "BTCJSONError:\n" + o += errString + } + splitResult := strings.Split(o, "\n") + const maxPerWidget = 6 + for i := 0; i < len(splitResult)-maxPerWidget; i += maxPerWidget { + sri := strings.Join(splitResult[i:i+maxPerWidget], "\n") + c.output = append( + c.output, + func(gtx l.Context) l.Dimensions { + return wg.Flex(). + Flexed( + 1, + wg.Caption(sri). + Color("DocText"). + Font("bariol regular"). + MaxLines(maxPerWidget).Fn, + ). + Fn(gtx) + }, + ) + } + return + } else { + var out string + var isErr bool + if result, e = ctl2.Call(wg.cx.Config, false, method, params...); E.Chk(e) { + isErr = true + out = e.Error() + I.Ln(out) + // if out, e = strconv.Unquote(); E.Chk(e) { + // } + } else { + if out, e = strconv.Unquote(string(result)); E.Chk(e) { + } + } + strings.ReplaceAll(out, "\t", " ") + D.Ln(out) + splitResult := strings.Split(out, "\n") + outputColor := "DocText" + if isErr { + outputColor = "Danger" + } + for i := range splitResult { + sri := splitResult[i] + c.output = append( + c.output, + func(gtx l.Context) l.Dimensions { + return c.Theme.Flex().AlignStart(). + Rigid( + wg.Body2(sri). + Color(outputColor). + Font("go regular").MaxLines(4). + Fn, + ). + Fn(gtx) + }, + ) + } + return + } + } else { + D.Ln("method", method, "args", args) + if result, e = ctl2.Call(wg.cx.Config, false, method, params...); E.Chk(e) { + var errR string + if result, e = ctl2.Call(wg.cx.Config, true, method, params...); E.Chk(e) { + if e != nil { + errR = e.Error() + } + c.output = append( + c.output, c.Theme.Flex().AlignStart(). + Rigid(wg.Body2(errR).Color("Danger").Fn).Fn, + ) + return + } + if e != nil { + errR = e.Error() + } + c.output = append( + c.output, c.Theme.Flex().AlignStart(). + Rigid( + wg.Body2(errR).Color("Danger").Fn, + ).Fn, + ) + } + c.output = append(c.output, wg.console.JSONWidget("DocText", result)...) + } + c.outputList.JumpToEnd() + }() + } + clearClickableFn := func() { + c.editor.SetText("") + c.editor.Focus() + } + copyClickableFn := func() { + go func() { + if e := clipboard.WriteAll(c.editor.Text()); E.Chk(e) { + } + }() + c.editor.Focus() + } + pasteClickableFn := func() { + // // col := c.editor.Caret.Col + // go func() { + // txt := c.editor.Text() + // var e error + // var cb string + // if cb, e = clipboard.ReadAll(); E.Chk(e) { + // } + // cb = findSpaceRegexp.ReplaceAllString(cb, " ") + // txt = txt[:col] + cb + txt[col:] + // c.editor.SetText(txt) + // c.editor.Move(col + len(cb)) + // }() + } + c.clearButton = wg.IconButton(c.clearClickable.SetClick(clearClickableFn)). + Icon( + wg.Icon(). + Color("DocText"). + Src(&icons2.ContentBackspace), + ). + Background(""). + ButtonInset(0.25) + c.copyButton = wg.IconButton(c.copyClickable.SetClick(copyClickableFn)). + Icon( + wg.Icon(). + Color("DocText"). + Src(&icons2.ContentContentCopy), + ). + Background(""). + ButtonInset(0.25) + c.pasteButton = wg.IconButton(c.pasteClickable.SetClick(pasteClickableFn)). + Icon( + wg.Icon(). + Color("DocText"). + Src(&icons2.ContentContentPaste), + ). + Background(""). + ButtonInset(0.25) + c.output = append( + c.output, func(gtx l.Context) l.Dimensions { + return c.Theme.Flex().AlignStart().Rigid(c.H6("Welcome to the Parallelcoin RPC console").Color("DocText").Fn).Fn(gtx) + }, func(gtx l.Context) l.Dimensions { + return c.Theme.Flex().AlignStart().Rigid(c.Caption("Type 'help' to get available commands and 'clear' or 'cls' to clear the screen").Color("DocText").Fn).Fn(gtx) + }, + ) + return c +} + +func (c *Console) Fn(gtx l.Context) l.Dimensions { + le := func(gtx l.Context, index int) l.Dimensions { + if index >= len(c.output) || index < 0 { + return l.Dimensions{} + } else { + return c.output[index](gtx) + } + } + fn := c.Theme.VFlex(). + Flexed( + 0.1, + c.Fill( + "PanelBg", l.Center, c.TextSize.V, 0, func(gtx l.Context) l.Dimensions { + return c.Inset( + 0.25, + c.outputList. + ScrollToEnd(). + End(). + Background("PanelBg"). + Color("DocBg"). + Active("Primary"). + Vertical(). + Length(len(c.output)). + ListElement(le). + Fn, + ). + Fn(gtx) + }, + ).Fn, + ). + Rigid( + c.Fill( + "DocBg", l.Center, c.TextSize.V, 0, c.Inset( + 0.25, + c.Theme.Flex(). + Flexed( + 1, + c.TextInput(c.editor.SetSubmit(c.submitFunc), "enter an rpc command"). + Color("DocText"). + Fn, + ). + Rigid(c.copyButton.Fn). + Rigid(c.pasteButton.Fn). + Rigid(c.clearButton.Fn). + Fn, + ).Fn, + ).Fn, + ). + Fn + return fn(gtx) +} + +type JSONElement struct { + key string + value interface{} +} + +type JSONElements []JSONElement + +func (je JSONElements) Len() int { + return len(je) +} + +func (je JSONElements) Less(i, j int) bool { + return je[i].key < je[j].key +} + +func (je JSONElements) Swap(i, j int) { + je[i], je[j] = je[j], je[i] +} + +func GetJSONElements(in map[string]interface{}) (je JSONElements) { + for i := range in { + je = append( + je, JSONElement{ + key: i, + value: in[i], + }, + ) + } + sort.Sort(je) + return +} + +func (c *Console) getIndent(n int, size float32, widget l.Widget) (out l.Widget) { + o := c.Theme.Flex() + for i := 0; i < n; i++ { + o.Rigid(c.Inset(size/2, gel.EmptySpace(0, 0)).Fn) + } + o.Rigid(widget) + out = o.Fn + return +} + +func (c *Console) JSONWidget(color string, j []byte) (out []l.Widget) { + var ifc interface{} + var e error + if e = json.Unmarshal(j, &ifc); E.Chk(e) { + } + return c.jsonWidget(color, 0, "", ifc) +} + +func (c *Console) jsonWidget(color string, depth int, key string, in interface{}) (out []l.Widget) { + switch in.(type) { + case []interface{}: + if key != "" { + out = append( + out, c.getIndent( + depth, 1, + func(gtx l.Context) l.Dimensions { + return c.Body2(key).Font("bariol bold").Color(color).Fn(gtx) + }, + ), + ) + } + D.Ln("got type []interface{}") + res := in.([]interface{}) + if len(res) == 0 { + out = append( + out, c.getIndent( + depth+1, 1, + func(gtx l.Context) l.Dimensions { + return c.Body2("[]").Color(color).Fn(gtx) + }, + ), + ) + } else { + for i := range res { + // D.S(res[i]) + out = append(out, c.jsonWidget(color, depth+1, fmt.Sprint(i), res[i])...) + } + } + case map[string]interface{}: + if key != "" { + out = append( + out, c.getIndent( + depth, 1, + func(gtx l.Context) l.Dimensions { + return c.Body2(key).Font("bariol bold").Color(color).Fn(gtx) + }, + ), + ) + } + D.Ln("got type map[string]interface{}") + res := in.(map[string]interface{}) + je := GetJSONElements(res) + // D.S(je) + if len(res) == 0 { + out = append( + out, c.getIndent( + depth+1, 1, + func(gtx l.Context) l.Dimensions { + return c.Body2("{}").Color(color).Fn(gtx) + }, + ), + ) + } else { + for i := range je { + D.S(je[i]) + out = append(out, c.jsonWidget(color, depth+1, je[i].key, je[i].value)...) + } + } + case JSONElement: + res := in.(JSONElement) + key = res.key + switch res.value.(type) { + case string: + D.Ln("got type string") + res := res.value.(string) + clk := c.Theme.WidgetPool.GetClickable() + out = append( + out, + c.jsonElement( + key, color, depth, func(gtx l.Context) l.Dimensions { + return c.Theme.Flex(). + Rigid(c.Body2("\"" + res + "\"").Color(color).Fn). + Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid( + c.IconButton(clk). + Background(""). + ButtonInset(0). + Color(color). + Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)). + SetClick( + func() { + go func() { + if e := clipboard.WriteAll(res); E.Chk(e) { + } + }() + }, + ).Fn, + ).Fn(gtx) + }, + ), + ) + case float64: + D.Ln("got type float64") + res := res.value.(float64) + clk := c.Theme.WidgetPool.GetClickable() + out = append( + out, + c.jsonElement( + key, color, depth, func(gtx l.Context) l.Dimensions { + return c.Theme.Flex(). + Rigid(c.Body2(fmt.Sprint(res)).Color(color).Fn). + Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid( + c.IconButton(clk). + Background(""). + ButtonInset(0). + Color(color). + Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)). + SetClick( + func() { + go func() { + if e := clipboard.WriteAll(fmt.Sprint(res)); E.Chk(e) { + } + }() + }, + ).Fn, + ).Fn(gtx) + // return c.th.ButtonLayout(clk).Embed(c.th.Body2().Color(color).Fn).Fn(gtx) + }, + ), + ) + case bool: + D.Ln("got type bool") + res := res.value.(bool) + out = append( + out, + c.jsonElement( + key, color, depth, func(gtx l.Context) l.Dimensions { + return c.Body2(fmt.Sprint(res)).Color(color).Fn(gtx) + }, + ), + ) + } + case string: + D.Ln("got type string") + res := in.(string) + clk := c.Theme.WidgetPool.GetClickable() + out = append( + out, + c.jsonElement( + key, color, depth, func(gtx l.Context) l.Dimensions { + return c.Theme.Flex(). + Rigid(c.Body2("\"" + res + "\"").Color(color).Fn). + Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid( + c.IconButton(clk). + Background(""). + ButtonInset(0). + Color(color). + Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)). + SetClick( + func() { + go func() { + if e := clipboard.WriteAll(res); E.Chk(e) { + } + }() + }, + ).Fn, + ).Fn(gtx) + }, + ), + ) + case float64: + D.Ln("got type float64") + res := in.(float64) + clk := c.Theme.WidgetPool.GetClickable() + out = append( + out, + c.jsonElement( + key, color, depth, func(gtx l.Context) l.Dimensions { + return c.Theme.Flex(). + Rigid(c.Body2(fmt.Sprint(res)).Color(color).Fn). + Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid( + c.IconButton(clk). + Background(""). + ButtonInset(0). + Color(color). + Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)). + SetClick( + func() { + go func() { + if e := clipboard.WriteAll(fmt.Sprint(res)); E.Chk(e) { + } + }() + }, + ).Fn, + ).Fn(gtx) + // return c.th.ButtonLayout(clk).Embed(c.th.Body2(fmt.Sprint(res)).Color(color).Fn).Fn(gtx) + }, + ), + ) + case bool: + D.Ln("got type bool") + res := in.(bool) + out = append( + out, + c.jsonElement( + key, color, depth, func(gtx l.Context) l.Dimensions { + return c.Body2(fmt.Sprint(res)).Color(color).Fn(gtx) + }, + ), + ) + default: + D.S(in) + } + return +} + +func (c *Console) jsonElement(key, color string, depth int, w l.Widget) l.Widget { + return func(gtx l.Context) l.Dimensions { + return c.Theme.Flex(). + Rigid( + c.getIndent( + depth, 1, + c.Body2(key).Font("bariol bold").Color(color).Fn, + ), + ). + Rigid(c.Inset(0.125, gel.EmptySpace(0, 0)).Fn). + Rigid(w). + Fn(gtx) + } +} diff --git a/cmd/gui/createform.go b/cmd/gui/createform.go new file mode 100644 index 0000000..7d197db --- /dev/null +++ b/cmd/gui/createform.go @@ -0,0 +1,494 @@ +package gui + +import ( + "encoding/hex" + "strings" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/tyler-smith/go-bip39" + "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/p9icons" +) + +func (wg *WalletGUI) centered(w l.Widget) l.Widget { + return wg.Flex(). + Flexed(0.5, gel.EmptyMaxWidth()). + Rigid( + wg.VFlex(). + AlignMiddle(). + Rigid( + w, + ). + Fn, + ). + Flexed(0.5, gel.EmptyMaxWidth()). + Fn +} + +func (wg *WalletGUI) cwfLogoHeader() l.Widget { + return wg.centered( + wg.Icon(). + Scale(gel.Scales["H2"]). + Color("DocText"). + Src(&p9icons.ParallelCoin).Fn, + ) +} + +func (wg *WalletGUI) cwfHeader() l.Widget { + return wg.centered( + wg.H4("create new wallet"). + Color("PanelText"). + Fn, + ) +} + +func (wg *WalletGUI) cwfPasswordHeader() l.Widget { + return wg.H5("password"). + Color("PanelText"). + Fn +} + +func (wg *WalletGUI) cwfShuffleButton() l.Widget { + return wg.ButtonLayout( + wg.clickables["createShuffle"].SetClick( + func() { + wg.ShuffleSeed() + wg.inputs["walletWords"].SetText("") // wg.createWords) + wg.inputs["walletWords"].SetText("") // wg.createWords) + wg.restoring = false + wg.createVerifying = false + }, + ), + ). + CornerRadius(0). + Corners(0). + Background("Primary"). + Embed( + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H6"], + ). + Color("DocText"). + Src( + &icons.NavigationRefresh, + ).Fn, + ). + Rigid( + wg.Body1("new").Color("DocText").Fn, + ). + Fn, + ).Fn, + ).Fn +} + +func (wg *WalletGUI) cwfRestoreButton() l.Widget { + return wg.ButtonLayout( + wg.clickables["createRestore"].SetClick( + func() { + D.Ln("clicked restore button") + if !wg.restoring { + wg.inputs["walletRestore"].SetText("") + wg.createMatch = "" + wg.restoring = true + wg.createVerifying = false + } else { + wg.createMatch = "" + wg.restoring = false + wg.createVerifying = false + } + }, + ), + ). + CornerRadius(0). + Corners(0). + Background("Primary"). + Embed( + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H6"], + ). + Color("DocText"). + Src( + &icons.ActionRestore, + ).Fn, + ). + Rigid( + wg.Body1("restore").Color("DocText").Fn, + ). + Fn, + ).Fn, + ).Fn +} + +func (wg *WalletGUI) cwfSetGenesis() l.Widget { + return func(gtx l.Context) l.Dimensions { + if !wg.bools["testnet"].GetValue() { + return l.Dimensions{} + } else { + return wg.ButtonLayout( + wg.clickables["genesis"].SetClick( + func() { + seedString := "f4d2c4c542bb52512ed9e6bbfa2d000e576a0c8b4ebd1acafd7efa37247366bc" + var e error + if wg.createSeed, e = hex.DecodeString(seedString); F.Chk(e) { + panic(e) + } + var wk string + if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) { + panic(e) + } + wks := strings.Split(wk, " ") + var out string + for i := 0; i < 24; i += 4 { + out += strings.Join(wks[i:i+4], " ") + if i+4 < 24 { + out += "\n" + } + } + wg.showWords = out + wg.createWords = wk + wg.createMatch = wk + wg.inputs["walletWords"].SetText(wk) + wg.createVerifying = true + }, + ), + ). + CornerRadius(0). + Corners(0). + Background("Primary"). + Embed( + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H6"], + ). + Color("DocText"). + Src( + &icons.ActionOpenInNew, + ).Fn, + ). + Rigid( + wg.Body1("genesis").Color("DocText").Fn, + ). + Fn, + ).Fn, + ).Fn(gtx) + } + } +} + +func (wg *WalletGUI) cwfSetAutofill() l.Widget { + return func(gtx l.Context) l.Dimensions { + if !wg.bools["testnet"].GetValue() { + return l.Dimensions{} + } else { + return wg.ButtonLayout( + wg.clickables["autofill"].SetClick( + func() { + wk := wg.createWords + wg.createMatch = wk + wg.inputs["walletWords"].SetText(wk) + wg.createVerifying = true + }, + ), + ). + CornerRadius(0). + Corners(0). + Background("Primary"). + Embed( + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H6"], + ). + Color("DocText"). + Src( + &icons.ActionOpenInNew, + ).Fn, + ). + Rigid( + wg.Body1("autofill").Color("DocText").Fn, + ). + Fn, + ).Fn, + ).Fn(gtx) + } + } +} + +func (wg *WalletGUI) cwfSeedHeader() l.Widget { + return wg.Flex(). //AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.H5("seed"). + Color("PanelText"). + Fn, + ).Fn, + ). + Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid(wg.cwfShuffleButton()). + Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid(wg.cwfRestoreButton()). + Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid(wg.cwfSetGenesis()). + Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Rigid(wg.cwfSetAutofill()). + Fn +} + +func (wg *WalletGUI) cfwWords() (w l.Widget) { + if !wg.createVerifying { + col := "DocText" + if wg.createWords == wg.createMatch { + col = "Success" + } + return wg.Flex(). + Rigid( + wg.ButtonLayout( + wg.clickables["createVerify"].SetClick( + func() { + wg.createVerifying = true + }, + ), + ).Background("Transparent").Embed( + wg.VFlex(). + Rigid( + wg.Caption("Write the following words down, then click to re-enter and verify transcription"). + Color("PanelText"). + Fn, + ). + Rigid( + wg.Flex().Flexed( + 1, + wg.Body1(wg.showWords).Alignment(text.Middle).Color(col).Fn, + ).Fn, + ).Fn, + ).Fn, + ). + Fn + } + return nil +} + +func (wg *WalletGUI) cfwWordsVerify() (w l.Widget) { + if wg.createVerifying { + verifyState := wg.Button( + wg.clickables["createVerify"].SetClick( + func() { + wg.createVerifying = false + }, + ), + ).Text("back").Fn + if wg.createWords == wg.createMatch { + verifyState = wg.Inset(0.25, wg.Body1("match").Color("Success").Fn).Fn + } + return wg.Flex(). + Rigid( + verifyState, + ). + Rigid( + wg.inputs["walletWords"].Fn, + ). + Fn + } + return nil +} + +func (wg *WalletGUI) cfwRestore() (w l.Widget) { + w = func(l.Context) l.Dimensions { + return l.Dimensions{} + } + if wg.restoring { + // restoreState := wg.Button( + // wg.clickables["createRestore"].SetClick( + // func() { + // wg.restoring = false + // }, + // ), + // ).Text("back").Fn + if wg.createWords == wg.createMatch { + w = wg.Flex().AlignMiddle(). + Rigid( + wg.Inset(0.25, wg.H5("valid").Color("Success").Fn).Fn, + ).Fn + } + return wg.Flex(). + Rigid( + w, + ). + Rigid( + wg.inputs["walletRestore"].Fn, + ). + Fn + } + return +} + +func (wg *WalletGUI) cwfTestnetSettings() (out l.Widget) { + return wg.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + return wg.CheckBox( + wg.bools["testnet"].SetOnChange( + func(b bool) { + if !b { + wg.bools["solo"].Value(false) + wg.bools["lan"].Value(false) + // wg.cx.Config.MulticastPass.Set("pa55word") + wg.cx.Config.Solo.F() + wg.cx.Config.LAN.F() + wg.ShuffleSeed() + wg.createVerifying = false + wg.inputs["walletWords"].SetText("") + wg.Invalidate() + } + wg.createWalletTestnetToggle(b) + }, + ), + ). + IconColor("Primary"). + TextColor("DocText"). + Text("Use Testnet"). + Fn(gtx) + }, + ). + Rigid( + func(gtx l.Context) l.Dimensions { + checkColor, textColor := "Primary", "DocText" + if !wg.bools["testnet"].GetValue() { + gtx = gtx.Disabled() + checkColor, textColor = "scrim", "scrim" + } + return wg.CheckBox( + wg.bools["lan"].SetOnChange( + func(b bool) { + D.Ln("lan now set to", b) + wg.cx.Config.LAN.Set(b) + if b && wg.cx.Config.Solo.True() { + wg.cx.Config.Solo.F() + wg.cx.Config.DisableDNSSeed.T() + wg.cx.Config.AutoListen.F() + wg.bools["solo"].Value(false) + // wg.cx.Config.MulticastPass.Set("pa55word") + wg.Invalidate() + } else { + wg.cx.Config.Solo.F() + wg.cx.Config.DisableDNSSeed.F() + // wg.cx.Config.MulticastPass.Set("pa55word") + wg.cx.Config.AutoListen.T() + } + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + }, + ), + ). + IconColor(checkColor). + TextColor(textColor). + Text("LAN only"). + Fn(gtx) + }, + ). + Rigid( + func(gtx l.Context) l.Dimensions { + checkColor, textColor := "Primary", "DocText" + if !wg.bools["testnet"].GetValue() { + gtx = gtx.Disabled() + checkColor, textColor = "scrim", "scrim" + } + return wg.CheckBox( + wg.bools["solo"].SetOnChange( + func(b bool) { + D.Ln("solo now set to", b) + wg.cx.Config.Solo.Set(b) + if b && wg.cx.Config.LAN.True() { + wg.cx.Config.LAN.F() + wg.cx.Config.DisableDNSSeed.T() + wg.cx.Config.AutoListen.F() + // wg.cx.Config.MulticastPass.Set("pa55word") + wg.bools["lan"].Value(false) + wg.Invalidate() + } else { + wg.cx.Config.LAN.F() + wg.cx.Config.DisableDNSSeed.F() + // wg.cx.Config.MulticastPass.Set("pa55word") + wg.cx.Config.AutoListen.T() + } + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + }, + ), + ). + IconColor(checkColor). + TextColor(textColor). + Text("Solo (mine without peers)"). + Fn(gtx) + }, + ). + Fn +} + +func (wg *WalletGUI) cwfConfirmation() (out l.Widget) { + return wg.CheckBox( + wg.bools["ihaveread"].SetOnChange( + func(b bool) { + D.Ln("confirmed read", b) + // if the password has been entered, we need to copy it to the variable + if wg.createWalletPasswordsMatch() { + wg.cx.Config.WalletPass.Set(wg.passwords["confirmPassEditor"].GetPassword()) + } + }, + ), + ). + IconColor("Primary"). + TextColor("DocText"). + Text( + "I have stored the seed and password safely " + + "and understand it cannot be recovered", + ). + Fn +} + +func (wg *WalletGUI) createWalletFormWidgets() (out []l.Widget) { + out = append( + out, + wg.cwfLogoHeader(), + wg.cwfHeader(), + wg.cwfPasswordHeader(), + wg.passwords["passEditor"]. + Fn, + wg.passwords["confirmPassEditor"]. + Fn, + wg.cwfSeedHeader(), + ) + if wg.createVerifying { + out = append( + out, wg.cfwWordsVerify(), + ) + } else if wg.restoring { + out = append(out, wg.cfwRestore()) + } else { + out = append(out, wg.cfwWords()) + } + out = append( + out, + wg.cwfTestnetSettings(), + wg.cwfConfirmation(), + ) + return +} diff --git a/cmd/gui/createwallet.go b/cmd/gui/createwallet.go new file mode 100644 index 0000000..5cfe49d --- /dev/null +++ b/cmd/gui/createwallet.go @@ -0,0 +1,298 @@ +package gui + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/p9c/p9/pkg/qu" + "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/cmd/wallet" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/constant" + "github.com/p9c/p9/pkg/fork" + + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +const slash = string(os.PathSeparator) + +func (wg *WalletGUI) CreateWalletPage(gtx l.Context) l.Dimensions { + walletForm := wg.createWalletFormWidgets() + le := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset(0.25, walletForm[index]).Fn(gtx) + } + return func(gtx l.Context) l.Dimensions { + return wg.Fill( + "DocBg", l.Center, 0, 0, + // wg.Inset( + // 0.5, + wg.VFlex(). + Flexed( + 1, + wg.lists["createWallet"].Vertical().Start().Length(len(walletForm)).ListElement(le).Fn, + ). + Rigid( + wg.createConfirmExitBar(), + ).Fn, + // ).Fn, + ).Fn(gtx) + }(gtx) +} + +func (wg *WalletGUI) createConfirmExitBar() l.Widget { + return wg.VFlex(). + // Rigid( + // wg.Inset( + // , + // ).Fn, + // ). + Rigid( + wg.Inset(0.5, + wg.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + return wg.Flex(). + Rigid( + wg.ButtonLayout( + wg.clickables["quit"].SetClick( + func() { + interrupt.Request() + }, + ), + ). + CornerRadius(0.5). + Corners(0). + Background("PanelBg"). + Embed( + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H4"], + ). + Color("DocText"). + Src( + &icons. + MapsDirectionsRun, + ).Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Rigid( + wg.H6("exit").Color("DocText").Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Fn, + ).Fn, + ).Fn, + ). + Fn(gtx) + }, + ). + Flexed( + 1, + gel.EmptyMaxWidth(), + ). + Rigid( + func(gtx l.Context) l.Dimensions { + if !wg.createWalletInputsAreValid() { + gtx = gtx.Disabled() + } + return wg.Flex(). + Rigid( + wg.ButtonLayout( + wg.clickables["createWallet"].SetClick( + func() { + go wg.createWalletAction() + }, + ), + ). + CornerRadius(0). + Corners(0). + Background("Primary"). + Embed( + // wg.Fill("DocText", + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H4"], + ). + Color("DocText"). + Src( + &icons. + ContentCreate, + ).Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Rigid( + wg.H6("create").Color("DocText").Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Fn, + ).Fn, + ).Fn, + ). + Fn(gtx) + }, + ). + Fn, + ).Fn, + ). + Fn +} + +func (wg *WalletGUI) createWalletPasswordsMatch() bool { + return wg.passwords["passEditor"].GetPassword() != "" && + wg.passwords["confirmPassEditor"].GetPassword() != "" && + len(wg.passwords["passEditor"].GetPassword()) >= 8 && + wg.passwords["passEditor"].GetPassword() == + wg.passwords["confirmPassEditor"].GetPassword() +} + +func (wg *WalletGUI) createWalletInputsAreValid() bool { + return wg.createWalletPasswordsMatch() && wg.bools["ihaveread"].GetValue() && wg.createWords == wg.createMatch +} + +func (wg *WalletGUI) createWalletAction() { + // wg.NodeRunCommandChan <- "stop" + D.Ln("clicked submit wallet") + wg.cx.Config.WalletFile.Set(filepath.Join(wg.cx.Config.DataDir.V(), wg.cx.ActiveNet.Name, constant.DbName)) + dbDir := wg.cx.Config.WalletFile.V() + loader := wallet.NewLoader(wg.cx.ActiveNet, dbDir, 250) + // seed, _ := hex.DecodeString(wg.inputs["walletSeed"].GetText()) + seed := wg.createSeed + pass := wg.passwords["passEditor"].GetPassword() + wg.cx.Config.WalletPass.Set(pass) + D.Ln("password", pass) + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + w, e := loader.CreateNewWallet( + []byte(pass), + []byte(pass), + seed, + time.Now(), + false, + wg.cx.Config, + qu.T(), + ) + D.Ln("*** created wallet") + if E.Chk(e) { + // return + } + w.Stop() + D.Ln("shutting down wallet", w.ShuttingDown()) + w.WaitForShutdown() + D.Ln("starting main app") + wg.cx.Config.Generate.T() + wg.cx.Config.GenThreads.Set(1) + wg.cx.Config.NodeOff.F() + wg.cx.Config.WalletOff.F() + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + // // we are going to assume the config is not manually misedited + // if apputil.FileExists(*wg.cx.Config.ConfigFile) { + // b, e := ioutil.ReadFile(*wg.cx.Config.ConfigFile) + // if e == nil { + // wg.cx.Config, wg.cx.ConfigMap = pod.EmptyConfig() + // e = json.Unmarshal(b, wg.cx.Config) + // if e != nil { + // E.Ln("error unmarshalling config", e) + // // os.Exit(1) + // panic(e) + // } + // } else { + // F.Ln("unexpected error reading configuration file:", e) + // // os.Exit(1) + // // return e + // panic(e) + // } + // } + *wg.noWallet = false + // interrupt.Request() + // wg.wallet.Stop() + // wg.wallet.Start() + // wg.node.Start() + // wg.miner.Start() + wg.unlockPassword.Editor().SetText(pass) + wg.unlockWallet(pass) + interrupt.RequestRestart() +} + +func (wg *WalletGUI) createWalletTestnetToggle(b bool) { + D.Ln("testnet on?", b) + // if the password has been entered, we need to copy it to the variable + if wg.passwords["passEditor"].GetPassword() != "" || + wg.passwords["confirmPassEditor"].GetPassword() != "" || + len(wg.passwords["passEditor"].GetPassword()) >= 8 || + wg.passwords["passEditor"].GetPassword() == + wg.passwords["confirmPassEditor"].GetPassword() { + wg.cx.Config.WalletPass.Set(wg.passwords["confirmPassEditor"].GetPassword()) + D.Ln("wallet pass", wg.cx.Config.WalletPass.V()) + } + if b { + wg.cx.ActiveNet = &chaincfg.TestNet3Params + fork.IsTestnet = true + } else { + wg.cx.ActiveNet = &chaincfg.MainNetParams + fork.IsTestnet = false + } + I.Ln("activenet:", wg.cx.ActiveNet.Name) + D.Ln("setting ports to match network") + wg.cx.Config.Network.Set(wg.cx.ActiveNet.Name) + wg.cx.Config.P2PListeners.Set( + []string{"0.0.0.0:" + wg.cx.ActiveNet.DefaultPort}, + ) + wg.cx.Config.P2PConnect.Set([]string{"127.0.0.1:" + wg.cx.ActiveNet. + DefaultPort}) + address := fmt.Sprintf( + "127.0.0.1:%s", + wg.cx.ActiveNet.RPCClientPort, + ) + wg.cx.Config.RPCListeners.Set([]string{address}) + wg.cx.Config.RPCConnect.Set(address) + address = fmt.Sprintf("127.0.0.1:" + wg.cx.ActiveNet.WalletRPCServerPort) + wg.cx.Config.WalletRPCListeners.Set([]string{address}) + wg.cx.Config.WalletServer.Set(address) + wg.cx.Config.NodeOff.F() + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) +} diff --git a/cmd/gui/debug.go b/cmd/gui/debug.go new file mode 100644 index 0000000..809a64e --- /dev/null +++ b/cmd/gui/debug.go @@ -0,0 +1,62 @@ +package gui + +// func (wg *WalletGUI) goRoutines() { +// var e error +// if wg.App.ActivePageGet() == "goroutines" || wg.unlockPage.ActivePageGet() == "goroutines" { +// D.Ln("updating goroutines data") +// var b []byte +// buf := bytes.NewBuffer(b) +// if e = pprof.Lookup("goroutine").WriteTo(buf, 2); E.Chk(e) { +// } +// lines := strings.Split(buf.String(), "\n") +// var out []l.Widget +// var clickables []*p9.Clickable +// for x := range lines { +// i := x +// clickables = append(clickables, wg.Clickable()) +// var text string +// if strings.HasPrefix(lines[i], "goroutine") && i < len(lines)-2 { +// text = lines[i+2] +// text = strings.TrimSpace(strings.Split(text, " ")[0]) +// // outString += text + "\n" +// out = append( +// out, func(gtx l.Context) l.Dimensions { +// return wg.ButtonLayout(clickables[i]).Embed( +// wg.ButtonInset( +// 0.25, +// wg.Caption(text). +// Color("DocText").Fn, +// ).Fn, +// ).Background("Transparent").SetClick( +// func() { +// go func() { +// out := make([]string, 2) +// split := strings.Split(text, ":") +// if len(split) > 2 { +// out[0] = strings.Join(split[:len(split)-1], ":") +// out[1] = split[len(split)-1] +// } else { +// out[0] = split[0] +// out[1] = split[1] +// } +// D.Ln("path", out[0], "line", out[1]) +// goland := "goland64.exe" +// if runtime.GOOS != "windows" { +// goland = "goland" +// } +// launch := exec.Command(goland, "--line", out[1], out[0]) +// if e = launch.Start(); E.Chk(e) { +// } +// }() +// }, +// ). +// Fn(gtx) +// }, +// ) +// } +// } +// // D.Ln(outString) +// wg.State.SetGoroutines(out) +// wg.invalidate <- struct{}{} +// } +// } diff --git a/cmd/gui/events.go b/cmd/gui/events.go new file mode 100644 index 0000000..351959d --- /dev/null +++ b/cmd/gui/events.go @@ -0,0 +1,728 @@ +package gui + +import ( + "encoding/json" + "io/ioutil" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/chainrpc/p2padvt" + "github.com/p9c/p9/pkg/transport" + "github.com/p9c/p9/pkg/wire" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/util" +) + +func (wg *WalletGUI) WalletAndClientRunning() bool { + running := wg.wallet.Running() && wg.WalletClient != nil && !wg.WalletClient.Disconnected() + // D.Ln("wallet and wallet rpc client are running?", running) + return running +} + +func (wg *WalletGUI) Advertise() (e error) { + if wg.node.Running() && wg.cx.Config.Discovery.True() { + // I.Ln("sending out p2p advertisment") + if e = wg.multiConn.SendMany( + p2padvt.Magic, + transport.GetShards(p2padvt.Get(uint64(wg.cx.Config.UUID.V()), (wg.cx.Config.P2PListeners.S())[0])), + ); E.Chk(e) { + } + } + return +} + +// func (wg *WalletGUI) Tickers() { +// first := true +// D.Ln("updating best block") +// var e error +// var height int32 +// var h *chainhash.Hash +// if h, height, e = wg.ChainClient.GetBestBlock(); E.Chk(e) { +// // interrupt.Request() +// return +// } +// D.Ln(h, height) +// wg.State.SetBestBlockHeight(height) +// wg.State.SetBestBlockHash(h) +// go func() { +// var e error +// seconds := time.Tick(time.Second * 3) +// fiveSeconds := time.Tick(time.Second * 5) +// totalOut: +// for { +// preconnect: +// for { +// select { +// case <-seconds: +// D.Ln("preconnect loop") +// if e = wg.Advertise(); D.Chk(e) { +// } +// if wg.ChainClient != nil { +// wg.ChainClient.Disconnect() +// wg.ChainClient.Shutdown() +// wg.ChainClient = nil +// } +// if wg.WalletClient != nil { +// wg.WalletClient.Disconnect() +// wg.WalletClient.Shutdown() +// wg.WalletClient = nil +// } +// if !wg.node.Running() { +// break +// } +// break preconnect +// case <-fiveSeconds: +// continue +// case <-wg.quit.Wait(): +// break totalOut +// } +// } +// out: +// for { +// select { +// case <-seconds: +// if e = wg.Advertise(); D.Chk(e) { +// } +// if !wg.cx.IsCurrent() { +// continue +// } else { +// wg.cx.Syncing.Store(false) +// } +// D.Ln("---------------------- ready", wg.ready.Load()) +// D.Ln("---------------------- WalletAndClientRunning", wg.WalletAndClientRunning()) +// D.Ln("---------------------- stateLoaded", wg.stateLoaded.Load()) +// wg.node.Start() +// if e = wg.writeWalletCookie(); E.Chk(e) { +// } +// wg.wallet.Start() +// D.Ln("connecting to chain") +// if e = wg.chainClient(); E.Chk(e) { +// break +// } +// if wg.wallet.Running() { // && wg.WalletClient == nil { +// D.Ln("connecting to wallet") +// if e = wg.walletClient(); E.Chk(e) { +// break +// } +// } +// if !wg.node.Running() { +// D.Ln("breaking out node not running") +// break out +// } +// if wg.ChainClient == nil { +// D.Ln("breaking out chainclient is nil") +// break out +// } +// // if wg.WalletClient == nil { +// // D.Ln("breaking out walletclient is nil") +// // break out +// // } +// if wg.ChainClient.Disconnected() { +// D.Ln("breaking out chainclient disconnected") +// break out +// } +// // if wg.WalletClient.Disconnected() { +// // D.Ln("breaking out walletclient disconnected") +// // break out +// // } +// // var e error +// if first { +// wg.updateChainBlock() +// wg.invalidate <- struct{}{} +// } +// +// if wg.WalletAndClientRunning() { +// if first { +// wg.processWalletBlockNotification() +// } +// // if wg.stateLoaded.Load() { // || wg.currentReceiveGetNew.Load() { +// // wg.ReceiveAddressbook = func(gtx l.Context) l.Dimensions { +// // var widgets []l.Widget +// // widgets = append(widgets, wg.ReceivePage.GetAddressbookHistoryCards("DocBg")...) +// // le := func(gtx l.Context, index int) l.Dimensions { +// // return widgets[index](gtx) +// // } +// // return wg.Flex().Rigid( +// // wg.lists["receiveAddresses"].Length(len(widgets)).Vertical(). +// // ListElement(le).Fn, +// // ).Fn(gtx) +// // } +// // } +// if wg.stateLoaded.Load() && !wg.State.IsReceivingAddress() { // || wg.currentReceiveGetNew.Load() { +// wg.GetNewReceivingAddress() +// if wg.currentReceiveQRCode == nil || wg.currentReceiveRegenerate.Load() { // || wg.currentReceiveGetNew.Load() { +// wg.GetNewReceivingQRCode(wg.ReceivePage.urn) +// } +// } +// } +// wg.invalidate <- struct{}{} +// first = false +// case <-fiveSeconds: +// case <-wg.quit.Wait(): +// break totalOut +// } +// } +// } +// }() +// } + +func (wg *WalletGUI) updateThingies() (e error) { + // update the configuration + var b []byte + if b, e = ioutil.ReadFile(wg.cx.Config.ConfigFile.V()); !E.Chk(e) { + if e = json.Unmarshal(b, wg.cx.Config); !E.Chk(e) { + return + } + } + return +} +func (wg *WalletGUI) updateChainBlock() { + D.Ln("processChainBlockNotification") + var e error + if wg.ChainClient != nil && wg.ChainClient.Disconnected() || wg.ChainClient.Disconnected() { + D.Ln("connecting ChainClient") + if e = wg.chainClient(); E.Chk(e) { + return + } + } + var h *chainhash.Hash + var height int32 + D.Ln("updating best block") + if h, height, e = wg.ChainClient.GetBestBlock(); E.Chk(e) { + // interrupt.Request() + return + } + D.Ln(h, height) + wg.State.SetBestBlockHeight(height) + wg.State.SetBestBlockHash(h) +} + +func (wg *WalletGUI) processChainBlockNotification(hash *chainhash.Hash, height int32, t time.Time) { + D.Ln("processChainBlockNotification") + wg.State.SetBestBlockHeight(height) + wg.State.SetBestBlockHash(hash) + // if wg.WalletAndClientRunning() { + // wg.processWalletBlockNotification() + // } +} + +func (wg *WalletGUI) processWalletBlockNotification() bool { + D.Ln("processWalletBlockNotification") + if !wg.WalletAndClientRunning() { + D.Ln("wallet and client not running") + return false + } + // check account balance + var unconfirmed amt.Amount + var e error + if unconfirmed, e = wg.WalletClient.GetUnconfirmedBalance("default"); E.Chk(e) { + return false + } + wg.State.SetBalanceUnconfirmed(unconfirmed.ToDUO()) + var confirmed amt.Amount + if confirmed, e = wg.WalletClient.GetBalance("default"); E.Chk(e) { + return false + } + wg.State.SetBalance(confirmed.ToDUO()) + var atr []btcjson.ListTransactionsResult + // str := wg.State.allTxs.Load() + if atr, e = wg.WalletClient.ListTransactionsCount("default", 2<<32); E.Chk(e) { + return false + } + // D.Ln(len(atr)) + // wg.State.SetAllTxs(append(str, atr...)) + wg.State.SetAllTxs(atr) + wg.txMx.Lock() + wg.txHistoryList = wg.State.filteredTxs.Load() + atrl := 10 + if len(atr) < atrl { + atrl = len(atr) + } + wg.txMx.Unlock() + wg.RecentTransactions(10, "recent") + wg.RecentTransactions(-1, "history") + return true +} + +func (wg *WalletGUI) forceUpdateChain() { + wg.updateChainBlock() + var e error + var height int32 + var tip *chainhash.Hash + if tip, height, e = wg.ChainClient.GetBestBlock(); E.Chk(e) { + return + } + var block *wire.Block + if block, e = wg.ChainClient.GetBlock(tip); E.Chk(e) { + } + t := block.Header.Timestamp + wg.processChainBlockNotification(tip, height, t) +} + +func (wg *WalletGUI) ChainNotifications() *rpcclient.NotificationHandlers { + return &rpcclient.NotificationHandlers{ + // OnClientConnected: func() { + // // go func() { + // D.Ln("(((NOTIFICATION))) CHAIN CLIENT CONNECTED!") + // wg.cx.Syncing.Store(true) + // wg.forceUpdateChain() + // wg.processWalletBlockNotification() + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // wg.cx.Syncing.Store(false) + // }, + // OnBlockConnected: func(hash *chainhash.Hash, height int32, t time.Time) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) chain OnBlockConnected", hash, height, t) + // wg.processChainBlockNotification(hash, height, t) + // // wg.processWalletBlockNotification() + // // todo: send system notification of new block, set configuration to disable also + // // if wg.WalletAndClientRunning() { + // // var e error + // // if _, e = wg.WalletClient.RescanBlocks([]chainhash.Hash{*hash}); E.Chk(e) { + // // } + // // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + OnFilteredBlockConnected: func(height int32, header *wire.BlockHeader, txs []*util.Tx) { + nbh := header.BlockHash() + wg.processChainBlockNotification(&nbh, height, header.Timestamp) + // if time.Now().Sub(time.Unix(wg.lastUpdated.Load(), 0)) < time.Second { + // return + // } + wg.lastUpdated.Store(time.Now().Unix()) + hash := header.BlockHash() + D.Ln( + "(((NOTIFICATION))) OnFilteredBlockConnected hash", hash, "POW hash:", + header.BlockHashWithAlgos(height), "height", height, + ) + // D.S(txs) + if wg.processWalletBlockNotification() { + } + // filename := filepath.Join(*wg.cx.Config.DataDir, "state.json") + // if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) { + // } + // if wg.WalletAndClientRunning() { + // var e error + // if _, e = wg.WalletClient.RescanBlocks([]chainhash.Hash{hash}); E.Chk(e) { + // } + // } + wg.RecentTransactions(10, "recent") + wg.RecentTransactions(-1, "history") + wg.invalidate <- struct{}{} + }, + // OnBlockDisconnected: func(hash *chainhash.Hash, height int32, t time.Time) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnBlockDisconnected", hash, height, t) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnFilteredBlockDisconnected: func(height int32, header *wire.BlockHeader) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnFilteredBlockDisconnected", height, header) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnRecvTx: func(transaction *util.Tx, details *btcjson.BlockDetails) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRecvTx", transaction, details) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnRedeemingTx: func(transaction *util.Tx, details *btcjson.BlockDetails) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRedeemingTx", transaction, details) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnRelevantTxAccepted: func(transaction []byte) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRelevantTxAccepted", transaction) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnRescanFinished: func(hash *chainhash.Hash, height int32, blkTime time.Time) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRescanFinished", hash, height, blkTime) + // wg.processChainBlockNotification(hash, height, blkTime) + // // update best block height + // // wg.processWalletBlockNotification() + // // stop showing syncing indicator + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnRescanProgress: func(hash *chainhash.Hash, height int32, blkTime time.Time) { + // D.Ln("(((NOTIFICATION))) OnRescanProgress", hash, height, blkTime) + // // update best block height + // // wg.processWalletBlockNotification() + // // set to show syncing indicator + // if wg.processWalletBlockNotification() { + // } + // wg.Syncing.Store(true) + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + OnTxAccepted: func(hash *chainhash.Hash, amount amt.Amount) { + // if wg.syncing.Load() { + // D.Ln("OnTxAccepted but we are syncing") + // return + // } + D.Ln("(((NOTIFICATION))) OnTxAccepted") + D.Ln(hash, amount) + // if wg.processWalletBlockNotification() { + // } + wg.RecentTransactions(10, "recent") + wg.RecentTransactions(-1, "history") + wg.invalidate <- struct{}{} + }, + // OnTxAcceptedVerbose: func(txDetails *btcjson.TxRawResult) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnTxAcceptedVerbose") + // D.S(txDetails) + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnPodConnected: func(connected bool) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnPodConnected", connected) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // wg.RecentTransactions(10, "recent") + // wg.RecentTransactions(-1, "history") + // wg.invalidate <- struct{}{} + // }, + // OnAccountBalance: func(account string, balance util.Amount, confirmed bool) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("OnAccountBalance") + // // what does this actually do + // D.Ln(account, balance, confirmed) + // }, + // OnWalletLockState: func(locked bool) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("OnWalletLockState", locked) + // // switch interface to unlock page + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // // TODO: lock when idle... how to get trigger for idleness in UI? + // }, + // OnUnknownNotification: func(method string, params []json.RawMessage) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnUnknownNotification", method, params) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + } + +} + +func (wg *WalletGUI) WalletNotifications() *rpcclient.NotificationHandlers { + // if !wg.wallet.Running() || wg.WalletClient == nil || wg.WalletClient.Disconnected() { + // return nil + // } + // var updating bool + return &rpcclient.NotificationHandlers{ + // OnClientConnected: func() { + // if wg.cx.Syncing.Load() { + // return + // } + // if updating { + // return + // } + // D.Ln("(((NOTIFICATION))) wallet client connected, running initial processes") + // for !wg.processWalletBlockNotification() { + // time.Sleep(time.Second) + // D.Ln("(((NOTIFICATION))) retry attempting to update wallet transactions") + // } + // filename := filepath.Join(wg.cx.DataDir, "state.json") + // if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) { + // } + // wg.invalidate <- struct{}{} + // updating = false + // }, + // OnBlockConnected: func(hash *chainhash.Hash, height int32, t time.Time) { + // if wg.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) wallet OnBlockConnected", hash, height, t) + // wg.processWalletBlockNotification() + // filename := filepath.Join(wg.cx.DataDir, "state.json") + // if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) { + // } + // wg.invalidate <- struct{}{} + // }, + // OnFilteredBlockConnected: func(height int32, header *wire.BlockHeader, txs []*util.Tx) { + // if wg.Syncing.Load() { + // return + // } + // D.Ln( + // "(((NOTIFICATION))) wallet OnFilteredBlockConnected hash", header.BlockHash(), "POW hash:", + // header.BlockHashWithAlgos(height), "height", height, + // ) + // // D.S(txs) + // nbh := header.BlockHash() + // wg.processChainBlockNotification(&nbh, height, header.Timestamp) + // if wg.processWalletBlockNotification() { + // } + // filename := filepath.Join(wg.cx.DataDir, "state.json") + // if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) { + // } + // wg.invalidate <- struct{}{} + // }, + // OnBlockDisconnected: func(hash *chainhash.Hash, height int32, t time.Time) { + // if wg.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnBlockDisconnected", hash, height, t) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + // OnFilteredBlockDisconnected: func(height int32, header *wire.BlockHeader) { + // if wg.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnFilteredBlockDisconnected", height, header) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + // OnRecvTx: func(transaction *util.Tx, details *btcjson.BlockDetails) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRecvTx", transaction, details) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + // OnRedeemingTx: func(transaction *util.Tx, details *btcjson.BlockDetails) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRedeemingTx", transaction, details) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + // OnRelevantTxAccepted: func(transaction []byte) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRelevantTxAccepted", transaction) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + // OnRescanFinished: func(hash *chainhash.Hash, height int32, blkTime time.Time) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnRescanFinished", hash, height, blkTime) + // wg.processChainBlockNotification(hash, height, blkTime) + // // update best block height + // // wg.processWalletBlockNotification() + // // stop showing syncing indicator + // if wg.processWalletBlockNotification() { + // } + // wg.cx.Syncing.Store(false) + // wg.invalidate <- struct{}{} + // }, + // OnRescanProgress: func(hash *chainhash.Hash, height int32, blkTime time.Time) { + // D.Ln("(((NOTIFICATION))) OnRescanProgress", hash, height, blkTime) + // // // update best block height + // // // wg.processWalletBlockNotification() + // // // set to show syncing indicator + // // if wg.processWalletBlockNotification() { + // // } + // // wg.Syncing.Store(true) + // wg.invalidate <- struct{}{} + // }, + // OnTxAccepted: func(hash *chainhash.Hash, amount util.Amount) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnTxAccepted") + // D.Ln(hash, amount) + // if wg.processWalletBlockNotification() { + // } + // }, + // OnTxAcceptedVerbose: func(txDetails *btcjson.TxRawResult) { + // if wg.cx.Syncing.Load() { + // return + // } + // D.Ln("(((NOTIFICATION))) OnTxAcceptedVerbose") + // D.S(txDetails) + // if wg.processWalletBlockNotification() { + // } + // }, + // OnPodConnected: func(connected bool) { + // D.Ln("(((NOTIFICATION))) OnPodConnected", connected) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + // OnAccountBalance: func(account string, balance util.Amount, confirmed bool) { + // D.Ln("OnAccountBalance") + // // what does this actually do + // D.Ln(account, balance, confirmed) + // }, + // OnWalletLockState: func(locked bool) { + // D.Ln("OnWalletLockState", locked) + // // switch interface to unlock page + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // // TODO: lock when idle... how to get trigger for idleness in UI? + // }, + // OnUnknownNotification: func(method string, params []json.RawMessage) { + // D.Ln("(((NOTIFICATION))) OnUnknownNotification", method, params) + // wg.forceUpdateChain() + // if wg.processWalletBlockNotification() { + // } + // }, + } + +} + +func (wg *WalletGUI) chainClient() (e error) { + D.Ln("starting up chain client") + if wg.cx.Config.NodeOff.True() { + W.Ln("node is disabled") + return nil + } + if wg.ChainClient == nil { // || wg.ChainClient.Disconnected() { + D.Ln(wg.cx.Config.RPCConnect.V()) + // wg.ChainMutex.Lock() + // defer wg.ChainMutex.Unlock() + // I.S(wg.certs) + if wg.ChainClient, e = rpcclient.New( + &rpcclient.ConnConfig{ + Host: wg.cx.Config.RPCConnect.V(), + Endpoint: "ws", + User: wg.cx.Config.Username.V(), + Pass: wg.cx.Config.Password.V(), + TLS: wg.cx.Config.ClientTLS.True(), + Certificates: wg.certs, + DisableAutoReconnect: false, + DisableConnectOnNew: false, + }, wg.ChainNotifications(), wg.cx.KillAll, + ); E.Chk(e) { + return + } + } + if wg.ChainClient.Disconnected() { + D.Ln("connecting chain client") + if e = wg.ChainClient.Connect(1); E.Chk(e) { + return + } + } + if e = wg.ChainClient.NotifyBlocks(); !E.Chk(e) { + D.Ln("subscribed to new blocks") + // wg.WalletNotifications() + wg.invalidate <- struct{}{} + } + return +} + +func (wg *WalletGUI) walletClient() (e error) { + D.Ln("connecting to wallet") + if wg.cx.Config.WalletOff.True() { + W.Ln("wallet is disabled") + return nil + } + // walletRPC := (*wg.cx.Config.WalletRPCListeners)[0] + // certs := wg.cx.Config.ReadCAFile() + // I.Ln("config.tls", wg.cx.Config.ClientTLS.True()) + wg.WalletMutex.Lock() + if wg.WalletClient, e = rpcclient.New( + &rpcclient.ConnConfig{ + Host: wg.cx.Config.WalletServer.V(), + Endpoint: "ws", + User: wg.cx.Config.Username.V(), + Pass: wg.cx.Config.Password.V(), + TLS: wg.cx.Config.ClientTLS.True(), + Certificates: wg.certs, + DisableAutoReconnect: false, + DisableConnectOnNew: false, + }, wg.WalletNotifications(), wg.cx.KillAll, + ); E.Chk(e) { + wg.WalletMutex.Unlock() + return + } + wg.WalletMutex.Unlock() + // if e = wg.WalletClient.Connect(1); E.Chk(e) { + // return + // } + if e = wg.WalletClient.NotifyNewTransactions(true); !E.Chk(e) { + D.Ln("subscribed to new transactions") + } else { + // return + } + // if e = wg.WalletClient.NotifyBlocks(); E.Chk(e) { + // // return + // } else { + // D.Ln("subscribed to wallet client notify blocks") + // } + D.Ln("wallet connected") + return +} diff --git a/cmd/gui/help.go b/cmd/gui/help.go new file mode 100644 index 0000000..b272971 --- /dev/null +++ b/cmd/gui/help.go @@ -0,0 +1,121 @@ +package gui + +import ( + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/p9icons" + "github.com/p9c/p9/version" +) + +func (wg *WalletGUI) HelpPage() func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return wg.VFlex().AlignMiddle(). + Flexed(0.5, gel.EmptyMaxWidth()). + Rigid( + wg.H5("ParallelCoin Pod Gio Wallet").Alignment(text.Middle).Fn, + ). + Rigid( + wg.Fill( + "DocBg", l.Center, wg.TextSize.V, 0, wg.Inset( + 0.5, + wg.VFlex(). + AlignMiddle(). + Rigid( + + wg.VFlex().AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.Caption("Built from git repository:"). + Font("bariol bold").Fn, + ).Fn, + ). + Rigid( + wg.Caption(version.URL).Fn, + ). + Fn, + + ). + Rigid( + + wg.VFlex().AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.Caption("GitRef:"). + Font("bariol bold").Fn, + ).Fn, + ). + Rigid( + wg.Caption(version.GitRef).Fn, + ). + Fn, + + ). + Rigid( + + wg.VFlex().AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.Caption("GitCommit:"). + Font("bariol bold").Fn, + ).Fn, + ). + Rigid( + wg.Caption(version.GitCommit).Fn, + ). + Fn, + + ). + Rigid( + + wg.VFlex().AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.Caption("BuildTime:"). + Font("bariol bold").Fn, + ).Fn, + ). + Rigid( + wg.Caption(version.BuildTime).Fn, + ). + Fn, + + ). + Rigid( + + wg.VFlex().AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.Caption("Tag:"). + Font("bariol bold").Fn, + ).Fn, + ). + Rigid( + wg.Caption(version.Tag).Fn, + ). + Fn, + + ). + Rigid( + wg.Icon().Scale(gel.Scales["H6"]). + Color("DocText"). + Src(&p9icons.Gio). + Fn, + ). + Rigid( + wg.Caption("powered by Gio").Fn, + ). + Fn, + ).Fn, + ).Fn, + ). + Flexed(0.5, gel.EmptyMaxWidth()). + Fn(gtx) + } +} diff --git a/cmd/gui/history.go b/cmd/gui/history.go new file mode 100644 index 0000000..b010ce5 --- /dev/null +++ b/cmd/gui/history.go @@ -0,0 +1,199 @@ +package gui + +import ( + "fmt" + "time" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/pkg/gel" +) + +func (wg *WalletGUI) HistoryPage() l.Widget { + if wg.TxHistoryWidget == nil { + wg.TxHistoryWidget = func(gtx l.Context) l.Dimensions { + return l.Dimensions{Size: gtx.Constraints.Max} + } + } + return func(gtx l.Context) l.Dimensions { + if wg.openTxID.Load() != "" { + for i := range wg.txHistoryList { + if wg.txHistoryList[i].TxID == wg.openTxID.Load() { + txs := wg.txHistoryList[i] + // instead return detail view + var out []l.Widget + out = []l.Widget{ + wg.txDetailEntry("Abandoned", fmt.Sprint(txs.Abandoned), "DocBg", false), + wg.txDetailEntry("Account", fmt.Sprint(txs.Account), "DocBgDim", false), + wg.txDetailEntry("Address", txs.Address, "DocBg", false), + wg.txDetailEntry("Block Hash", txs.BlockHash, "DocBgDim", true), + wg.txDetailEntry("Block Index", fmt.Sprint(txs.BlockIndex), "DocBg", false), + wg.txDetailEntry("Block Time", fmt.Sprint(time.Unix(txs.BlockTime, 0)), "DocBgDim", false), + wg.txDetailEntry("Category", txs.Category, "DocBg", false), + wg.txDetailEntry("Confirmations", fmt.Sprint(txs.Confirmations), "DocBgDim", false), + wg.txDetailEntry("Fee", fmt.Sprintf("%0.8f", txs.Fee), "DocBg", false), + wg.txDetailEntry("Generated", fmt.Sprint(txs.Generated), "DocBgDim", false), + wg.txDetailEntry("Involves Watch Only", fmt.Sprint(txs.InvolvesWatchOnly), "DocBg", false), + wg.txDetailEntry("Time", fmt.Sprint(time.Unix(txs.Time, 0)), "DocBgDim", false), + wg.txDetailEntry("Time Received", fmt.Sprint(time.Unix(txs.TimeReceived, 0)), "DocBg", false), + wg.txDetailEntry("Trusted", fmt.Sprint(txs.Trusted), "DocBgDim", false), + wg.txDetailEntry("TxID", txs.TxID, "DocBg", true), + // todo: add WalletConflicts here + wg.txDetailEntry("Comment", fmt.Sprintf("%0.8f", txs.Amount), "DocBgDim", false), + wg.txDetailEntry("OtherAccount", fmt.Sprint(txs.OtherAccount), "DocBg", false), + } + le := func(gtx l.Context, index int) l.Dimensions { + return out[index](gtx) + } + return wg.VFlex().AlignStart(). + Rigid( + wg.recentTxCardSummaryButton(&txs, wg.clickables["txPageBack"], "Primary", true), + // wg.H6(wg.openTxID.Load()).Fn, + ). + Rigid( + wg.lists["txdetail"]. + Vertical(). + Length(len(out)). + ListElement(le). + Fn, + ). + Fn(gtx) + + // return wg.Flex().Flexed( + // 1, + // wg.H3(wg.openTxID.Load()).Fn, + // ).Fn(gtx) + } + } + // if we got to here, the tx was not found + if wg.originTxDetail != "" { + wg.MainApp.ActivePage(wg.originTxDetail) + wg.originTxDetail = "" + } + } + return wg.VFlex(). + Rigid( + // wg.Fill("DocBg", l.Center, 0, 0, + // wg.Inset(0.25, + wg.Responsive( + wg.Size.Load(), gel.Widgets{ + { + Widget: wg.VFlex(). + Flexed(1, wg.HistoryPageView()). + // Rigid( + // // wg.Fill("DocBg", + // wg.Flex().AlignMiddle().SpaceBetween(). + // Flexed(0.5, gel.EmptyMaxWidth()). + // Rigid(wg.HistoryPageStatusFilter()). + // Flexed(0.5, gel.EmptyMaxWidth()). + // Fn, + // // ).Fn, + // ). + // Rigid( + // wg.Fill("DocBg", + // wg.Flex().AlignMiddle().SpaceBetween(). + // Rigid(wg.HistoryPager()). + // Rigid(wg.HistoryPagePerPageCount()). + // Fn, + // ).Fn, + // ). + Fn, + }, + { + Size: 64, + Widget: wg.VFlex(). + Flexed(1, wg.HistoryPageView()). + // Rigid( + // // wg.Fill("DocBg", + // wg.Flex().AlignMiddle().SpaceBetween(). + // // Rigid(wg.HistoryPager()). + // Flexed(0.5, gel.EmptyMaxWidth()). + // Rigid(wg.HistoryPageStatusFilter()). + // Flexed(0.5, gel.EmptyMaxWidth()). + // // Rigid(wg.HistoryPagePerPageCount()). + // Fn, + // // ).Fn, + // ). + Fn, + }, + }, + ).Fn, + // ).Fn, + // ).Fn, + ).Fn(gtx) + } +} + +func (wg *WalletGUI) HistoryPageView() l.Widget { + return wg.VFlex(). + Rigid( + // wg.Fill("DocBg", l.Center, wg.TextSize.True, 0, + // wg.Inset(0.25, + wg.TxHistoryWidget, + // ).Fn, + // ).Fn, + ).Fn +} + +func (wg *WalletGUI) HistoryPageStatusFilter() l.Widget { + return wg.Flex().AlignMiddle(). + Rigid( + wg.Inset( + 0.25, + wg.Caption("show").Fn, + ).Fn, + ). + Rigid( + wg.Inset( + 0.25, + func(gtx l.Context) l.Dimensions { + return wg.CheckBox(wg.bools["showGenerate"]). + TextColor("DocText"). + TextScale(1). + Text("generate"). + IconScale(1). + Fn(gtx) + }, + ).Fn, + ). + Rigid( + wg.Inset( + 0.25, + func(gtx l.Context) l.Dimensions { + return wg.CheckBox(wg.bools["showSent"]). + TextColor("DocText"). + TextScale(1). + Text("sent"). + IconScale(1). + Fn(gtx) + }, + ).Fn, + ). + Rigid( + wg.Inset( + 0.25, + func(gtx l.Context) l.Dimensions { + return wg.CheckBox(wg.bools["showReceived"]). + TextColor("DocText"). + TextScale(1). + Text("received"). + IconScale(1). + Fn(gtx) + }, + ).Fn, + ). + Rigid( + wg.Inset( + 0.25, + func(gtx l.Context) l.Dimensions { + return wg.CheckBox(wg.bools["showImmature"]). + TextColor("DocText"). + TextScale(1). + Text("immature"). + IconScale(1). + Fn(gtx) + }, + ).Fn, + ). + Fn +} diff --git a/cmd/gui/loadingscreen.go b/cmd/gui/loadingscreen.go new file mode 100644 index 0000000..dbcb1de --- /dev/null +++ b/cmd/gui/loadingscreen.go @@ -0,0 +1,78 @@ +package gui + +import ( + "golang.org/x/exp/shiny/materialdesign/icons" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/p9icons" +) + +func (wg *WalletGUI) getLoadingPage() (a *gel.App) { + a = wg.App(wg.Window.Width, wg.State.activePage, Break1). + SetMainDirection(l.Center + 1). + SetLogo(&p9icons.ParallelCoin). + SetAppTitleText("Parallelcoin Wallet") + a.Pages( + map[string]l.Widget{ + "loading": wg.Page( + "loading", gel.Widgets{ + gel.WidgetSize{ + Widget: + func(gtx l.Context) l.Dimensions { + return a.Flex().Flexed(1, a.Direction().Center().Embed(a.H1("loading").Fn).Fn).Fn(gtx) + }, + }, + }, + ), + "unlocking": wg.Page( + "unlocking", gel.Widgets{ + gel.WidgetSize{ + Widget: + func(gtx l.Context) l.Dimensions { + return a.Flex().Flexed(1, a.Direction().Center().Embed(a.H1("unlocking").Fn).Fn).Fn(gtx) + }, + }, + }, + ), + }, + ) + a.ButtonBar( + []l.Widget{ + wg.PageTopBarButton( + "home", 4, &icons.ActionLock, func(name string) { + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, "Danger", + ), + // wg.Flex().Rigid(wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn).Fn, + }, + ) + a.StatusBar( + []l.Widget{ + wg.RunStatusPanel, + }, + []l.Widget{ + wg.StatusBarButton( + "console", 2, &p9icons.Terminal, func(name string) { + wg.MainApp.ActivePage(name) + }, a, + ), + wg.StatusBarButton( + "log", 4, &icons.ActionList, func(name string) { + D.Ln("click on button", name) + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, + ), + wg.StatusBarButton( + "settings", 5, &icons.ActionSettings, func(name string) { + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, + ), + // wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + }, + ) + // a.PushOverlay(wg.toasts.DrawToasts()) + // a.PushOverlay(wg.dialog.DrawDialog()) + return +} diff --git a/cmd/gui/log.go b/cmd/gui/log.go new file mode 100644 index 0000000..19bb9a2 --- /dev/null +++ b/cmd/gui/log.go @@ -0,0 +1,43 @@ +package gui + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/gui/main.go b/cmd/gui/main.go new file mode 100644 index 0000000..2a55a5a --- /dev/null +++ b/cmd/gui/main.go @@ -0,0 +1,722 @@ +package gui + +import ( + "crypto/rand" + "fmt" + "net" + "os" + "runtime" + "strings" + "sync" + "time" + + "github.com/niubaoshu/gotiny" + "github.com/tyler-smith/go-bip39" + + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/text" + "github.com/p9c/p9/pkg/chainrpc/p2padvt" + "github.com/p9c/p9/pkg/pipe" + "github.com/p9c/p9/pkg/transport" + "github.com/p9c/p9/pod/state" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/gel/gio/op/paint" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/btcjson" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/cmd/gui/cfg" + "github.com/p9c/p9/pkg/apputil" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/util/rununit" +) + +// Main is the entrypoint for the wallet GUI +func Main(cx *state.State) (e error) { + size := uberatomic.NewInt32(0) + noWallet := true + wg := &WalletGUI{ + cx: cx, + invalidate: qu.Ts(16), + quit: cx.KillAll, + Size: size, + noWallet: &noWallet, + otherNodes: make(map[uint64]*nodeSpec), + certs: cx.Config.ReadCAFile(), + + } + return wg.Run() +} + +type BoolMap map[string]*gel.Bool +type ListMap map[string]*gel.List +type CheckableMap map[string]*gel.Checkable +type ClickableMap map[string]*gel.Clickable +type Inputs map[string]*gel.Input +type Passwords map[string]*gel.Password +type IncDecMap map[string]*gel.IncDec + +type WalletGUI struct { + wg sync.WaitGroup + cx *state.State + quit qu.C + State *State + noWallet *bool + node, wallet, miner *rununit.RunUnit + walletToLock time.Time + walletLockTime int + ChainMutex, WalletMutex sync.Mutex + ChainClient, WalletClient *rpcclient.Client + WalletWatcher qu.C + *gel.Window + Size *uberatomic.Int32 + MainApp *gel.App + invalidate qu.C + unlockPage *gel.App + loadingPage *gel.App + config *cfg.Config + configs cfg.GroupsMap + unlockPassword *gel.Password + sidebarButtons []*gel.Clickable + buttonBarButtons []*gel.Clickable + statusBarButtons []*gel.Clickable + receiveAddressbookClickables []*gel.Clickable + sendAddressbookClickables []*gel.Clickable + quitClickable *gel.Clickable + bools BoolMap + lists ListMap + checkables CheckableMap + clickables ClickableMap + inputs Inputs + passwords Passwords + incdecs IncDecMap + console *Console + RecentTxsWidget, TxHistoryWidget l.Widget + recentTxsClickables, txHistoryClickables []*gel.Clickable + txHistoryList []btcjson.ListTransactionsResult + openTxID, prevOpenTxID *uberatomic.String + originTxDetail string + txMx sync.Mutex + stateLoaded *uberatomic.Bool + currentReceiveQRCode *paint.ImageOp + currentReceiveAddress string + currentReceiveQR l.Widget + currentReceiveRegenClickable *gel.Clickable + currentReceiveCopyClickable *gel.Clickable + currentReceiveRegenerate *uberatomic.Bool + // currentReceiveGetNew *uberatomic.Bool + sendClickable *gel.Clickable + ready *uberatomic.Bool + mainDirection l.Direction + preRendering bool + // ReceiveAddressbook l.Widget + // SendAddressbook l.Widget + ReceivePage *ReceivePage + SendPage *SendPage + // toasts *toast.Toasts + // dialog *dialog.Dialog + createSeed []byte + createWords, showWords, createMatch string + createVerifying bool + restoring bool + lastUpdated uberatomic.Int64 + multiConn *transport.Channel + otherNodes map[uint64]*nodeSpec + uuid uint64 + peerCount *uberatomic.Int32 + certs []byte +} + +type nodeSpec struct { + time.Time + addr string +} + +func (wg *WalletGUI) Run() (e error) { + wg.openTxID = uberatomic.NewString("") + var mc *transport.Channel + quit := qu.T() + // I.Ln(wg.cx.Config.MulticastPass.V(), string(wg.cx.Config.MulticastPass. + // Bytes())) + if mc, e = transport.NewBroadcastChannel( + "controller", + wg, + wg.cx.Config.MulticastPass.Bytes(), + transport.DefaultPort, + 16384, + handlersMulticast, + quit, + ); E.Chk(e) { + return + } + wg.multiConn = mc + wg.peerCount = uberatomic.NewInt32(0) + wg.prevOpenTxID = uberatomic.NewString("") + wg.stateLoaded = uberatomic.NewBool(false) + wg.currentReceiveRegenerate = uberatomic.NewBool(true) + wg.ready = uberatomic.NewBool(false) + wg.Window = gel.NewWindowP9(wg.quit) + wg.Dark = wg.cx.Config.DarkTheme + wg.Colors.SetDarkTheme(wg.Dark.True()) + *wg.noWallet = true + wg.GetButtons() + wg.lists = wg.GetLists() + wg.clickables = wg.GetClickables() + wg.checkables = wg.GetCheckables() + before := func() { D.Ln("running before") } + after := func() { D.Ln("running after") } + I.Ln(os.Args[1:]) + options := []string{os.Args[0]} + // options = append(options, wg.cx.Config.FoundArgs...) + // options = append(options, "pipelog") + wg.node = wg.GetRunUnit( + "NODE", before, after, + append(options, "node")..., + // "node", + ) + wg.wallet = wg.GetRunUnit( + "WLLT", before, after, + append(options, "wallet")..., + // "wallet", + ) + wg.miner = wg.GetRunUnit( + "MINE", before, after, + append(options, "kopach")..., + // "wallet", + ) + // I.S(wg.node, wg.wallet, wg.miner) + wg.bools = wg.GetBools() + wg.inputs = wg.GetInputs() + wg.passwords = wg.GetPasswords() + // wg.toasts = toast.New(wg.th) + // wg.dialog = dialog.New(wg.th) + wg.console = wg.ConsolePage() + wg.quitClickable = wg.Clickable() + wg.incdecs = wg.GetIncDecs() + wg.Size = wg.Window.Width + wg.currentReceiveCopyClickable = wg.WidgetPool.GetClickable() + wg.currentReceiveRegenClickable = wg.WidgetPool.GetClickable() + wg.currentReceiveQR = func(gtx l.Context) l.Dimensions { + return l.Dimensions{} + } + wg.ReceivePage = wg.GetReceivePage() + wg.SendPage = wg.GetSendPage() + wg.MainApp = wg.GetAppWidget() + wg.State = GetNewState(wg.cx.ActiveNet, wg.MainApp.ActivePageGetAtomic()) + wg.unlockPage = wg.getWalletUnlockAppWidget() + wg.loadingPage = wg.getLoadingPage() + if !apputil.FileExists(wg.cx.Config.WalletFile.V()) { + I.Ln("wallet file does not exist", wg.cx.Config.WalletFile.V()) + } else { + *wg.noWallet = false + // if !*wg.cx.Config.NodeOff { + // // wg.startNode() + // wg.node.Start() + // } + if wg.cx.Config.Generate.True() && wg.cx.Config.GenThreads.V() != 0 { + // wg.startMiner() + wg.miner.Start() + } + wg.unlockPassword.Focus() + } + interrupt.AddHandler( + func() { + D.Ln("quitting wallet gui") + // consume.Kill(wg.Node) + // consume.Kill(wg.Miner) + // wg.gracefulShutdown() + wg.quit.Q() + }, + ) + go func() { + ticker := time.NewTicker(time.Second) + out: + for { + select { + case <-ticker.C: + go func() { + if e = wg.Advertise(); E.Chk(e) { + } + if wg.node.Running() { + if wg.ChainClient != nil { + if !wg.ChainClient.Disconnected() { + var pi []btcjson.GetPeerInfoResult + if pi, e = wg.ChainClient.GetPeerInfo(); E.Chk(e) { + return + } + wg.peerCount.Store(int32(len(pi))) + wg.Invalidate() + } + } + } + }() + case <-wg.invalidate.Wait(): + T.Ln("invalidating render queue") + wg.Window.Window.Invalidate() + // TODO: make a more appropriate trigger for this - ie, when state actually changes. + // if wg.wallet.Running() && wg.stateLoaded.Load() { + // filename := filepath.Join(wg.cx.DataDir, "state.json") + // if e := wg.State.Save(filename, wg.cx.Config.WalletPass); E.Chk(e) { + // } + // } + case <-wg.cx.KillAll.Wait(): + break out + case <-wg.quit.Wait(): + break out + } + } + }() + if e := wg.Window. + Size(56, 32). + Title("ParallelCoin Wallet"). + Open(). + Run( + func(gtx l.Context) l.Dimensions { + return wg.Fill( + "DocBg", l.Center, 0, 0, func(gtx l.Context) l.Dimensions { + return gel.If( + *wg.noWallet, + wg.CreateWalletPage, + func(gtx l.Context) l.Dimensions { + switch { + case wg.stateLoaded.Load(): + return wg.MainApp.Fn()(gtx) + default: + return wg.unlockPage.Fn()(gtx) + } + }, + )(gtx) + }, + ).Fn(gtx) + }, + wg.quit.Q, + wg.quit, + ); E.Chk(e) { + } + wg.gracefulShutdown() + wg.quit.Q() + return +} + +func (wg *WalletGUI) GetButtons() { + wg.sidebarButtons = make([]*gel.Clickable, 12) + // wg.walletLocked.Store(true) + for i := range wg.sidebarButtons { + wg.sidebarButtons[i] = wg.Clickable() + } + wg.buttonBarButtons = make([]*gel.Clickable, 5) + for i := range wg.buttonBarButtons { + wg.buttonBarButtons[i] = wg.Clickable() + } + wg.statusBarButtons = make([]*gel.Clickable, 8) + for i := range wg.statusBarButtons { + wg.statusBarButtons[i] = wg.Clickable() + } +} + +func (wg *WalletGUI) ShuffleSeed() { + wg.createSeed = make([]byte, 32) + _, _ = rand.Read(wg.createSeed) + var e error + var wk string + if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) { + panic(e) + } + wg.createWords = wk + // wg.createMatch = wk + wks := strings.Split(wk, " ") + var out string + for i := 0; i < 24; i += 4 { + out += strings.Join(wks[i:i+4], " ") + if i+4 < 24 { + out += "\n" + } + } + wg.showWords = out +} + +func (wg *WalletGUI) GetInputs() Inputs { + wg.ShuffleSeed() + return Inputs{ + "receiveAmount": wg.Input("", "Amount", "DocText", "PanelBg", "DocBg", func(amt string) {}, func(string) {}), + "receiveMessage": wg.Input( + "", + "Title", + "DocText", + "PanelBg", + "DocBg", + func(pass string) {}, + func(string) {}, + ), + + "sendAddress": wg.Input( + "", + "Parallelcoin Address", + "DocText", + "PanelBg", + "DocBg", + func(amt string) {}, + func(string) {}, + ), + "sendAmount": wg.Input("", "Amount", "DocText", "PanelBg", "DocBg", func(amt string) {}, func(string) {}), + "sendMessage": wg.Input( + "", + "Title", + "DocText", + "PanelBg", + "DocBg", + func(pass string) {}, + func(string) {}, + ), + + "console": wg.Input( + "", + "enter rpc command", + "DocText", + "Transparent", + "PanelBg", + func(pass string) {}, + func(string) {}, + ), + "walletWords": wg.Input( + /*wg.createWords*/ "", "wallet word seed", "DocText", "DocBg", "PanelBg", func(string) {}, + func(seedWords string) { + wg.createMatch = seedWords + wg.Invalidate() + }, + ), + "walletRestore": wg.Input( + /*wg.createWords*/ "", "enter seed to restore", "DocText", "DocBg", "PanelBg", func(string) {}, + func(seedWords string) { + var e error + wg.createMatch = seedWords + if wg.createSeed, e = bip39.EntropyFromMnemonic(seedWords); E.Chk(e) { + return + } + wg.createWords = seedWords + wg.Invalidate() + }, + ), + // "walletSeed": wg.Input( + // seedString, "wallet seed", "DocText", "DocBg", "PanelBg", func(seedHex string) { + // var e error + // if wg.createSeed, e = hex.DecodeString(seedHex); E.Chk(e) { + // return + // } + // var wk string + // if wk, e = bip39.NewMnemonic(wg.createSeed); E.Chk(e) { + // panic(e) + // } + // wg.createWords=wk + // wks := strings.Split(wk, " ") + // var out string + // for i := 0; i < 24; i += 4 { + // out += strings.Join(wks[i:i+4], " ") + "\n" + // } + // wg.showWords = out + // }, nil, + // ), + } +} + +// GetPasswords returns the passwords used in the wallet GUI +func (wg *WalletGUI) GetPasswords() (passwords Passwords) { + passwords = Passwords{ + "passEditor": wg.Password( + "password (minimum 8 characters length)", + text.New(meta.Data{}, ""), + "DocText", + "DocBg", + "PanelBg", + func(pass string) {}, + ), + "confirmPassEditor": wg.Password( + "confirm", + text.New(meta.Data{}, ""), + "DocText", + "DocBg", + "PanelBg", + func(pass string) {}, + ), + "publicPassEditor": wg.Password( + "public password (optional)", + wg.cx.Config.WalletPass, + "Primary", + "DocText", + "PanelBg", + func(pass string) {}, + ), + } + return +} + +func (wg *WalletGUI) GetIncDecs() IncDecMap { + return IncDecMap{ + "generatethreads": wg.IncDec(). + NDigits(2). + Min(0). + Max(runtime.NumCPU()). + SetCurrent(wg.cx.Config.GenThreads.V()). + ChangeHook( + func(n int) { + D.Ln("threads value now", n) + go func() { + D.Ln("setting thread count") + if wg.miner.Running() && n != 0 { + wg.miner.Stop() + wg.miner.Start() + } + if n == 0 { + wg.miner.Stop() + } + wg.cx.Config.GenThreads.Set(n) + _ = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()) + // if wg.miner.Running() { + // D.Ln("restarting miner") + // wg.miner.Stop() + // wg.miner.Start() + // } + }() + }, + ), + "idleTimeout": wg.IncDec(). + Scale(4). + Min(60). + Max(3600). + NDigits(4). + Amount(60). + SetCurrent(300). + ChangeHook( + func(n int) { + D.Ln("idle timeout", time.Duration(n)*time.Second) + }, + ), + } +} + +func (wg *WalletGUI) GetRunUnit( + name string, before, after func(), args ...string, +) *rununit.RunUnit { + I.Ln("getting rununit for", name, args) + // we have to copy the args otherwise further mutations affect this one + argsCopy := make([]string, len(args)) + copy(argsCopy, args) + return rununit.New(name, before, after, pipe.SimpleLog(name), + pipe.FilterNone, wg.quit, argsCopy...) +} + +func (wg *WalletGUI) GetLists() (o ListMap) { + return ListMap{ + "createWallet": wg.List(), + "overview": wg.List(), + "balances": wg.List(), + "recent": wg.List(), + "send": wg.List(), + "sendMedium": wg.List(), + "sendAddresses": wg.List(), + "receive": wg.List(), + "receiveMedium": wg.List(), + "receiveAddresses": wg.List(), + "transactions": wg.List(), + "settings": wg.List(), + "received": wg.List(), + "history": wg.List(), + "txdetail": wg.List(), + } +} + +func (wg *WalletGUI) GetClickables() ClickableMap { + return ClickableMap{ + "balanceConfirmed": wg.Clickable(), + "balanceUnconfirmed": wg.Clickable(), + "balanceTotal": wg.Clickable(), + "createWallet": wg.Clickable(), + "createVerify": wg.Clickable(), + "createShuffle": wg.Clickable(), + "createRestore": wg.Clickable(), + "genesis": wg.Clickable(), + "autofill": wg.Clickable(), + "quit": wg.Clickable(), + "sendSend": wg.Clickable(), + "sendSave": wg.Clickable(), + "sendFromRequest": wg.Clickable(), + "receiveCreateNewAddress": wg.Clickable(), + "receiveClear": wg.Clickable(), + "receiveShow": wg.Clickable(), + "receiveRemove": wg.Clickable(), + "transactions10": wg.Clickable(), + "transactions30": wg.Clickable(), + "transactions50": wg.Clickable(), + "txPageForward": wg.Clickable(), + "txPageBack": wg.Clickable(), + "theme": wg.Clickable(), + } +} + +func (wg *WalletGUI) GetCheckables() CheckableMap { + return CheckableMap{} +} + +func (wg *WalletGUI) GetBools() BoolMap { + return BoolMap{ + "runstate": wg.Bool(wg.node.Running()), + "encryption": wg.Bool(false), + "seed": wg.Bool(false), + "testnet": wg.Bool(false), + "lan": wg.Bool(false), + "solo": wg.Bool(false), + "ihaveread": wg.Bool(false), + "showGenerate": wg.Bool(true), + "showSent": wg.Bool(true), + "showReceived": wg.Bool(true), + "showImmature": wg.Bool(true), + } +} + +var shuttingDown = false + +func (wg *WalletGUI) gracefulShutdown() { + if shuttingDown { + D.Ln(log.Caller("already called gracefulShutdown", 1)) + return + } else { + shuttingDown = true + } + D.Ln("\nquitting wallet gui\n") + if wg.miner.Running() { + D.Ln("stopping miner") + wg.miner.Stop() + wg.miner.Shutdown() + } + if wg.wallet.Running() { + D.Ln("stopping wallet") + wg.wallet.Stop() + wg.wallet.Shutdown() + wg.unlockPassword.Wipe() + // wg.walletLocked.Store(true) + } + if wg.node.Running() { + D.Ln("stopping node") + wg.node.Stop() + wg.node.Shutdown() + } + // wg.ChainMutex.Lock() + if wg.ChainClient != nil { + D.Ln("stopping chain client") + wg.ChainClient.Shutdown() + wg.ChainClient = nil + } + // wg.ChainMutex.Unlock() + // wg.WalletMutex.Lock() + if wg.WalletClient != nil { + D.Ln("stopping wallet client") + wg.WalletClient.Shutdown() + wg.WalletClient = nil + } + // wg.WalletMutex.Unlock() + // interrupt.Request() + // time.Sleep(time.Second) + wg.quit.Q() +} + +var handlersMulticast = transport.Handlers{ + // string(sol.Magic): processSolMsg, + string(p2padvt.Magic): processAdvtMsg, + // string(hashrate.Magic): processHashrateMsg, +} + +func processAdvtMsg( + ctx interface{}, src net.Addr, dst string, b []byte, +) (e error) { + wg := ctx.(*WalletGUI) + if wg.cx.Config.Discovery.False() { + return + } + if wg.ChainClient == nil { + T.Ln("no chain client to process advertisment") + return + } + var j p2padvt.Advertisment + gotiny.Unmarshal(b, &j) + // I.S(j) + var peerUUID uint64 + peerUUID = j.UUID + // I.Ln("peerUUID of advertisment", peerUUID, wg.otherNodes) + if int(peerUUID) == wg.cx.Config.UUID.V() { + D.Ln("ignoring own advertisment message") + return + } + if _, ok := wg.otherNodes[peerUUID]; !ok { + var pi []btcjson.GetPeerInfoResult + if pi, e = wg.ChainClient.GetPeerInfo(); E.Chk(e) { + } + for i := range pi { + for k := range j.IPs { + jpa := net.JoinHostPort(k, fmt.Sprint(j.P2P)) + if jpa == pi[i].Addr { + I.Ln("not connecting to node already connected outbound") + return + } + if jpa == pi[i].AddrLocal { + I.Ln("not connecting to node already connected inbound") + return + } + } + } + // if we haven't already added it to the permanent peer list, we can add it now + I.Ln("connecting to lan peer with same PSK", j.IPs, peerUUID) + wg.otherNodes[peerUUID] = &nodeSpec{} + wg.otherNodes[peerUUID].Time = time.Now() + for i := range j.IPs { + addy := net.JoinHostPort(i, fmt.Sprint(j.P2P)) + for j := range pi { + if addy == pi[j].Addr || addy == pi[j].AddrLocal { + // not connecting to peer we already have connected to + return + } + } + } + // try all IPs + for addr := range j.IPs { + peerIP := net.JoinHostPort(addr, fmt.Sprint(j.P2P)) + if e = wg.ChainClient.AddNode(peerIP, "add"); E.Chk(e) { + continue + } + D.Ln("connected to peer via address", peerIP) + wg.otherNodes[peerUUID].addr = peerIP + break + } + I.Ln(peerUUID, "added", "otherNodes", wg.otherNodes) + } else { + // update last seen time for peerUUID for garbage collection of stale disconnected + // nodes + D.Ln("other node seen again", peerUUID, wg.otherNodes[peerUUID].addr) + wg.otherNodes[peerUUID].Time = time.Now() + } + // I.S(wg.otherNodes) + // If we lose connection for more than 9 seconds we delete and if the node + // reappears it can be reconnected + for i := range wg.otherNodes { + D.Ln(i, wg.otherNodes[i]) + tn := time.Now() + if tn.Sub(wg.otherNodes[i].Time) > time.Second*6 { + // also remove from connection manager + if e = wg.ChainClient.AddNode(wg.otherNodes[i].addr, "remove"); E.Chk(e) { + } + D.Ln("deleting", tn, wg.otherNodes[i], i) + delete(wg.otherNodes, i) + } + } + // on := int32(len(wg.otherNodes)) + // wg.otherNodeCount.Store(on) + return +} diff --git a/cmd/gui/overview.go b/cmd/gui/overview.go new file mode 100644 index 0000000..082c8fe --- /dev/null +++ b/cmd/gui/overview.go @@ -0,0 +1,822 @@ +package gui + +import ( + "fmt" + "strings" + "time" + + icons2 "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gel/gio/text" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/btcjson" +) + +func (wg *WalletGUI) balanceCard() func(gtx l.Context) l.Dimensions { + + // gtx.Constraints.Min.X = int(wg.TextSize.True * sp.inputWidth) + return func(gtx l.Context) l.Dimensions { + gtx.Constraints.Min.X = + int(wg.TextSize.V * 16) + gtx.Constraints.Max.X = + int(wg.TextSize.V * 16) + return wg.VFlex(). + AlignStart(). + Rigid( + wg.Inset( + 0.25, + wg.H5("balances"). + Fn, + ).Fn, + ). + Rigid( + wg.Fill( + "Primary", l.E, 0, 0, + wg.Inset( + 0.25, + wg.Flex(). + AlignEnd(). + Flexed( + 1, + wg.VFlex().AlignEnd(). + Rigid( + wg.ButtonLayout(wg.clickables["balanceConfirmed"]).SetClick( + func() { + go wg.WriteClipboard( + fmt.Sprintf( + "%6.8f", + wg.State.balance.Load(), + ), + ) + }, + ).Background("Transparent").Embed( + wg.Flex().AlignEnd().Flexed( + 1, + wg.Inset( + 0.5, + wg.Caption( + "confirmed"+leftPadTo( + 14, 14, + fmt.Sprintf( + "%6.8f", + wg.State.balance.Load(), + ), + ), + ). + Font("go regular"). + Alignment(text.End). + Color("DocText").Fn, + ).Fn, + ).Fn, + ).Fn, + ). + Rigid( + wg.ButtonLayout(wg.clickables["balanceUnconfirmed"]).SetClick( + func() { + go wg.WriteClipboard( + fmt.Sprintf( + "%6.8f", + wg.State.balanceUnconfirmed.Load(), + ), + ) + }, + ).Background("Transparent").Embed( + wg.Flex().AlignEnd().Flexed( + 1, + wg.Inset( + 0.5, + wg.Caption( + "unconfirmed"+leftPadTo( + 14, 14, + fmt.Sprintf( + "%6.8f", + wg.State.balanceUnconfirmed.Load(), + ), + ), + ). + Font("go regular"). + Alignment(text.End). + Color("DocText").Fn, + + ).Fn, + ).Fn, + ).Fn, + ). + Rigid( + wg.ButtonLayout(wg.clickables["balanceTotal"]).SetClick( + func() { + go wg.WriteClipboard( + fmt.Sprintf( + "%6.8f", + wg.State.balance.Load()+wg.State.balanceUnconfirmed.Load(), + ), + ) + }, + ).Background("Transparent").Embed( + wg.Flex().AlignEnd().Flexed( + 1, + wg.Inset( + 0.5, + wg.H5( + "total"+leftPadTo( + 14, 14, fmt.Sprintf( + "%6.8f", wg.State.balance.Load()+wg. + State.balanceUnconfirmed.Load(), + ), + ), + ). + Alignment(text.End). + Color("DocText").Fn, + ). + Fn, + ).Fn, + ).Fn, + ).Fn, + ).Fn, + ).Fn, + ).Fn, + ).Fn(gtx) + } +} + +func (wg *WalletGUI) OverviewPage() l.Widget { + if wg.RecentTxsWidget == nil { + wg.RecentTxsWidget = func(gtx l.Context) l.Dimensions { + return l.Dimensions{Size: gtx.Constraints.Max} + } + } + return func(gtx l.Context) l.Dimensions { + return wg.Responsive( + wg.Size.Load(), gel.Widgets{ + { + Size: 0, + Widget: + wg.VFlex().AlignStart(). + Rigid( + // wg.ButtonInset(0.25, + wg.VFlex(). + Rigid( + wg.Inset( + 0.25, + wg.balanceCard(), + ).Fn, + ).Fn, + // ).Fn, + ). + // Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Flexed( + 1, + wg.VFlex().AlignStart(). + Rigid( + wg.Inset( + 0.25, + wg.H5("Recent Transactions").Fn, + ).Fn, + ). + Flexed( + 1, + // wg.Inset(0.5, + wg.RecentTxsWidget, + // p9.EmptyMaxWidth(), + // ).Fn, + ). + Fn, + ). + Fn, + }, + { + Size: 64, + Widget: wg.Flex().AlignStart(). + Rigid( + // wg.ButtonInset(0.25, + wg.VFlex(). // SpaceSides().AlignStart(). + Rigid( + wg.Inset( + 0.25, + wg.balanceCard(), + ).Fn, + ).Fn, + // ).Fn, + ). + // Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Flexed( + 1, + // wg.Inset( + // 0.25, + wg.VFlex().AlignStart(). + Rigid( + wg.Inset( + 0.25, + wg.H5("Recent transactions").Fn, + ).Fn, + ). + Flexed( + 1, + // wg.Fill("DocBg", l.W, wg.TextSize.True, 0, wg.Inset(0.25, + wg.RecentTxsWidget, + // p9.EmptyMaxWidth(), + // ).Fn).Fn, + ). + Fn, + // ). + // Fn, + ). + Fn, + }, + }, + ).Fn(gtx) + } +} + +func (wg *WalletGUI) recentTxCardStub(txs *btcjson.ListTransactionsResult) l.Widget { + return wg.Inset( + 0.25, + wg.Flex(). + // AlignBaseline(). + // AlignStart(). + // SpaceEvenly(). + SpaceBetween(). + // Flexed( + // 1, + // wg.Inset( + // 0.25, + // wg.Caption(txs.Address). + // Font("go regular"). + // Color("PanelText"). + // TextScale(0.66). + // Alignment(text.End). + // Fn, + // ).Fn, + // ). + Rigid( + wg.Caption(fmt.Sprintf("%-6.8f DUO", txs.Amount)).Font("go regular").Color("DocText").Fn, + ). + Rigid( + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(1).Src(&icons2.DeviceWidgets).Fn, + ). + Rigid( + wg.Caption(fmt.Sprint(txs.BlockIndex)).Fn, + // wg.buttonIconText(txs.clickBlock, + // fmt.Sprint(*txs.BlockIndex), + // &icons2.DeviceWidgets, + // wg.blockPage(*txs.BlockIndex)), + ). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ActionCheckCircle).Fn, + ). + Rigid( + wg.Caption(fmt.Sprintf("%d ", txs.Confirmations)).Fn, + ). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + switch txs.Category { + case "generate": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ActionStars).Fn(gtx) + case "immature": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ImageTimeLapse).Fn(gtx) + case "receive": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ActionPlayForWork).Fn(gtx) + case "unknown": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.AVNewReleases).Fn(gtx) + } + return l.Dimensions{} + }, + ). + Rigid( + wg.Caption(txs.Category+" ").Fn, + ). + Fn, + ). + // Flexed(1, gel.EmptyMaxWidth()). + Rigid( + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(1).Src(&icons2.DeviceAccessTime).Fn, + ). + Rigid( + wg.Caption( + time.Unix( + txs.Time, + 0, + ).Format("02 Jan 06 15:04:05 MST"), + ).Font("go regular"). + // Alignment(text.End). + Color("PanelText").Fn, + ). + Fn, + ). + Fn, + ). + Fn +} + +func (wg *WalletGUI) recentTxCardSummary(txs *btcjson.ListTransactionsResult) l.Widget { + return wg.VFlex().AlignStart().SpaceBetween(). + Rigid( + // wg.Inset( + // 0.25, + wg.Flex().AlignStart().SpaceBetween(). + Rigid( + wg.H6(fmt.Sprintf("%-.8f DUO", txs.Amount)).Alignment(text.Start).Color("PanelText").Fn, + ). + Flexed( + 1, + wg.Inset( + 0.25, + wg.Caption(txs.Address). + Font("go regular"). + Color("PanelText"). + TextScale(0.66). + Alignment(text.End). + Fn, + ).Fn, + ).Fn, + // ).Fn, + ). + Rigid( + // wg.Inset( + // 0.25, + wg.Flex(). + Flexed( + 1, + wg.Flex(). + Rigid( + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(1).Src(&icons2.DeviceWidgets).Fn, + ). + // Rigid( + // wg.Caption(fmt.Sprint(*txs.BlockIndex)).Fn, + // // wg.buttonIconText(txs.clickBlock, + // // fmt.Sprint(*txs.BlockIndex), + // // &icons2.DeviceWidgets, + // // wg.blockPage(*txs.BlockIndex)), + // ). + Rigid( + wg.Caption(fmt.Sprintf("%d ", txs.BlockIndex)).Fn, + ). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ActionCheckCircle).Fn, + ). + Rigid( + wg.Caption(fmt.Sprintf("%d ", txs.Confirmations)).Fn, + ). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + switch txs.Category { + case "generate": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ActionStars).Fn(gtx) + case "immature": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ImageTimeLapse).Fn(gtx) + case "receive": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.ActionPlayForWork).Fn(gtx) + case "unknown": + return wg.Icon().Color("PanelText").Scale(1).Src(&icons2.AVNewReleases).Fn(gtx) + } + return l.Dimensions{} + }, + ). + Rigid( + wg.Caption(txs.Category+" ").Fn, + ). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(1).Src(&icons2.DeviceAccessTime).Fn, + ). + Rigid( + wg.Caption( + time.Unix( + txs.Time, + 0, + ).Format("02 Jan 06 15:04:05 MST"), + ).Color("PanelText").Fn, + ). + Fn, + ).Fn, + ).Fn, + // ).Fn, + ).Fn +} + +func (wg *WalletGUI) recentTxCardSummaryButton( + txs *btcjson.ListTransactionsResult, + clickable *gel.Clickable, + bgColor string, back bool, +) l.Widget { + return wg.ButtonLayout( + clickable.SetClick( + func() { + D.Ln("clicked tx") + // D.S(txs) + curr := wg.openTxID.Load() + if curr == txs.TxID { + wg.prevOpenTxID.Store(wg.openTxID.Load()) + wg.openTxID.Store("") + moveto := wg.originTxDetail + if moveto == "" { + moveto = wg.MainApp.ActivePageGet() + } + wg.MainApp.ActivePage(moveto) + } else { + if wg.MainApp.ActivePageGet() == "home" { + wg.originTxDetail = "home" + wg.MainApp.ActivePage("history") + } else { + wg.originTxDetail = "history" + } + wg.openTxID.Store(txs.TxID) + } + }, + ), + ). + Background(bgColor). + Embed( + gel.If( + back, + wg.Flex(). + Rigid( + wg.Icon().Color("PanelText").Scale(4).Src(&icons2.NavigationArrowBack).Fn, + ). + Rigid( + wg.Inset(0.5, gel.EmptyMinWidth()).Fn, + ). + Flexed( + 1, + wg.Fill( + "DocBg", l.Center, 0, 0, wg.Inset( + 0.5, + wg.recentTxCardSummary(txs), + ).Fn, + ).Fn, + ). + Fn, + wg.Flex(). + Rigid( + wg.Inset(0.5, gel.EmptyMaxHeight()).Fn, + ). + Flexed( + 1, + wg.Fill( + "DocBg", l.Center, 0, 0, wg.Inset( + 0.5, + wg.recentTxCardSummary(txs), + ).Fn, + ).Fn, + ). + Fn, + ), + ).Fn +} + +func (wg *WalletGUI) recentTxCardSummaryButtonGenerate( + txs *btcjson.ListTransactionsResult, + clickable *gel.Clickable, + bgColor string, back bool, +) l.Widget { + return wg.ButtonLayout( + clickable.SetClick( + func() { + D.Ln("clicked tx") + // D.S(txs) + curr := wg.openTxID.Load() + if curr == txs.TxID { + wg.prevOpenTxID.Store(wg.openTxID.Load()) + wg.openTxID.Store("") + moveto := wg.originTxDetail + if moveto == "" { + moveto = wg.MainApp.ActivePageGet() + } + wg.MainApp.ActivePage(moveto) + } else { + if wg.MainApp.ActivePageGet() == "home" { + wg.originTxDetail = "home" + wg.MainApp.ActivePage("history") + } else { + wg.originTxDetail = "history" + } + wg.openTxID.Store(txs.TxID) + } + }, + ), + ). + Background(bgColor). + Embed( + wg.Flex().AlignStart(). + Rigid( + // wg.Fill( + // "Primary", l.W, 0, 0, wg.Inset( + // 0.5, + gel.If( + back, + wg.Flex().AlignStart(). + Rigid( + wg.Icon().Color("PanelText").Scale(4).Src(&icons2.NavigationArrowBack).Fn, + ). + Flexed( + 1, + wg.recentTxCardSummary(txs), + ). + Fn, + wg.Flex().AlignStart(). + Flexed( + 1, + wg.recentTxCardStub(txs), + ). + Fn, + // wg.Flex(). + // Rigid( + // wg.Inset(0.5, gel.EmptyMaxHeight()).Fn, + // ). + // Flexed( + // 1, + // wg.Fill( + // "DocBg", l.Center, 0, 0, wg.Inset( + // 0.5, + // wg.recentTxCardSummary(txs), + // ).Fn, + // ).Fn, + // ). + // Fn, + ), + ).Fn, + // ).Fn, + // ).Fn, + ).Fn +} + +func (wg *WalletGUI) recentTxCardDetail(txs *btcjson.ListTransactionsResult, clickable *gel.Clickable) l.Widget { + return wg.VFlex(). + Rigid( + wg.Fill( + "Primary", l.Center, wg.TextSize.V, 0, + wg.recentTxCardSummaryButton(txs, clickable, "Primary", false), + ).Fn, + // ). + // Rigid( + // wg.Fill( + // "DocBg", l.Center, wg.TextSize.True, 0, + // wg.Flex(). + // Flexed( + // 1, + // wg.Inset( + // 0.25, + // wg.VFlex(). + // Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + // Rigid( + // wg.H6("Transaction Details"). + // Color("PanelText"). + // Fn, + // ). + // Rigid( + // wg.Inset( + // 0.25, + // wg.VFlex(). + // Rigid( + // wg.txDetailEntry("Transaction ID", txs.TxID), + // ). + // Rigid( + // wg.txDetailEntry("Address", txs.Address), + // ). + // Rigid( + // wg.txDetailEntry("Amount", fmt.Sprintf("%0.8f", txs.Amount)), + // ). + // Rigid( + // wg.txDetailEntry("In Block", fmt.Sprint(txs.BlockIndex)), + // ). + // Rigid( + // wg.txDetailEntry("First Mined", fmt.Sprint(txs.BlockTime)), + // ). + // Rigid( + // wg.txDetailEntry("Category", txs.Category), + // ). + // Rigid( + // wg.txDetailEntry("Confirmations", fmt.Sprint(txs.Confirmations)), + // ). + // Rigid( + // wg.txDetailEntry("Fee", fmt.Sprintf("%0.8f", txs.Fee)), + // ). + // Rigid( + // wg.txDetailEntry("Confirmations", fmt.Sprint(txs.Confirmations)), + // ). + // Rigid( + // wg.txDetailEntry("Involves Watch Only", fmt.Sprint(txs.InvolvesWatchOnly)), + // ). + // Rigid( + // wg.txDetailEntry("Time", fmt.Sprint(txs.Time)), + // ). + // Rigid( + // wg.txDetailEntry("Time Received", fmt.Sprint(txs.TimeReceived)), + // ). + // Rigid( + // wg.txDetailEntry("Trusted", fmt.Sprint(txs.Trusted)), + // ). + // Rigid( + // wg.txDetailEntry("Abandoned", fmt.Sprint(txs.Abandoned)), + // ). + // Rigid( + // wg.txDetailEntry("BIP125 Replaceable", fmt.Sprint(txs.BIP125Replaceable)), + // ). + // Fn, + // ).Fn, + // ).Fn, + // ).Fn, + // ).Fn, + // ).Fn, + ).Fn +} + +func (wg *WalletGUI) txDetailEntry(name, detail string, bgColor string, small bool) l.Widget { + content := wg.Body1 + if small { + content = wg.Caption + } + return wg.Fill( + bgColor, l.Center, wg.TextSize.V, 0, + wg.Flex().AlignBaseline(). + Flexed( + 0.25, + wg.Inset( + 0.25, + wg.Body1(name). + Color("PanelText"). + Font("bariol bold"). + Fn, + ).Fn, + ). + Flexed( + 0.75, + wg.Flex().SpaceStart().Rigid( + wg.Inset( + 0.25, + content(detail).Font("go regular"). + Color("PanelText"). + Fn, + ).Fn, + ).Fn, + ).Fn, + ).Fn +} + +// RecentTransactions generates a display showing recent transactions +// +// fields to use: Address, Amount, BlockIndex, BlockTime, Category, Confirmations, Generated +func (wg *WalletGUI) RecentTransactions(n int, listName string) l.Widget { + wg.txMx.Lock() + defer wg.txMx.Unlock() + // wg.ready.Store(false) + var out []l.Widget + // first := true + // out = append(out) + var txList []btcjson.ListTransactionsResult + var clickables []*gel.Clickable + txList = wg.txHistoryList + switch listName { + case "history": + clickables = wg.txHistoryClickables + case "recent": + // txList = wg.txRecentList + clickables = wg.recentTxsClickables + } + ltxl := len(txList) + ltc := len(clickables) + if ltxl > ltc { + count := ltxl - ltc + for ; count > 0; count-- { + clickables = append(clickables, wg.Clickable()) + } + } + if len(clickables) == 0 { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{Size: gtx.Constraints.Max} + } + } + D.Ln(">>>>>>>>>>>>>>>> iterating transactions", n, listName) + var collected int + for x := range txList { + if collected >= n && n > 0 { + break + } + txs := txList[x] + switch listName { + case "history": + collected++ + case "recent": + if txs.Category == "generate" || txs.Category == "immature" || txs.Amount < 0 && txs.Fee == 0 { + continue + } else { + collected++ + } + } + // spacer + // if !first { + // out = append( + // out, + // wg.Inset(0.25, gel.EmptyMaxWidth()).Fn, + // ) + // } else { + // first = false + // } + + ck := clickables[x] + out = append( + out, + func(gtx l.Context) l.Dimensions { + return gel.If( + txs.Category == "immature", + wg.recentTxCardSummaryButtonGenerate(&txs, ck, "DocBg", false), + gel.If( + txs.Category == "send", + wg.recentTxCardSummaryButton(&txs, ck, "Danger", false), + gel.If( + txs.Category == "receive", + wg.recentTxCardSummaryButton(&txs, ck, "Success", false), + gel.If( + txs.Category == "generate", + wg.recentTxCardSummaryButtonGenerate(&txs, ck, "DocBg", false), + gel.If( + wg.prevOpenTxID.Load() == txs.TxID, + wg.recentTxCardSummaryButton(&txs, ck, "Primary", false), + wg.recentTxCardSummaryButton(&txs, ck, "DocBg", false), + ), + ), + ), + ), + )(gtx) + }, + ) + // out = append(out, + // wg.Caption(txs.TxID). + // Font("go regular"). + // Color("PanelText"). + // TextScale(0.5).Fn, + // ) + // out = append( + // out, + // wg.Fill( + // "DocBg", l.W, 0, 0, + // + // ).Fn, + // ) + } + le := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset( + 0.25, + out[index], + ).Fn(gtx) + } + wo := func(gtx l.Context) l.Dimensions { + return wg.VFlex().AlignStart(). + Rigid( + wg.lists[listName]. + Vertical(). + Length(len(out)). + ListElement(le). + Fn, + ).Fn(gtx) + } + D.Ln(">>>>>>>>>>>>>>>> history widget completed", n, listName) + switch listName { + case "history": + wg.TxHistoryWidget = wo + case "recent": + wg.RecentTxsWidget = wo + } + return func(gtx l.Context) l.Dimensions { + return wo(gtx) + } +} + +func leftPadTo(length, limit int, txt string) string { + if len(txt) > limit { + return txt[:limit] + } + if len(txt) == limit { + return txt + } + pad := length - len(txt) + return strings.Repeat(" ", pad) + txt +} diff --git a/cmd/gui/receive.go b/cmd/gui/receive.go new file mode 100644 index 0000000..8a80bbd --- /dev/null +++ b/cmd/gui/receive.go @@ -0,0 +1,346 @@ +package gui + +import ( + "fmt" + "strconv" + + "github.com/p9c/p9/pkg/amt" + + "github.com/atotto/clipboard" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + + "github.com/p9c/p9/pkg/gel" +) + +const Break1 = 48 + +type ReceivePage struct { + wg *WalletGUI + inputWidth, break1 float32 + sm, md, lg, xl l.Widget + urn string +} + +func (wg *WalletGUI) GetReceivePage() (rp *ReceivePage) { + rp = &ReceivePage{ + wg: wg, + inputWidth: 17, + break1: 48, + } + rp.sm = rp.SmallList + return +} + +func (rp *ReceivePage) Fn(gtx l.Context) l.Dimensions { + wg := rp.wg + return wg.Responsive( + wg.Size.Load(), gel.Widgets{ + { + Widget: rp.SmallList, + }, + { + Size: rp.break1, + Widget: rp.MediumList, + }, + }, + ).Fn(gtx) +} + +func (rp *ReceivePage) SmallList(gtx l.Context) l.Dimensions { + wg := rp.wg + smallWidgets := []l.Widget{ + wg.Direction().Center().Embed(rp.QRButton()).Fn, + rp.InputMessage(), + rp.AmountInput(), + rp.MessageInput(), + rp.RegenerateButton(), + rp.AddressbookHeader(), + } + smallWidgets = append(smallWidgets, rp.GetAddressbookHistoryCards("DocBg")...) + le := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset(0.25, smallWidgets[index]).Fn(gtx) + } + return wg.VFlex().AlignStart(). + Flexed( + 1, + wg.lists["receive"]. + Vertical().Start(). + Length(len(smallWidgets)). + ListElement(le).Fn, + ).Fn(gtx) +} + +func (rp *ReceivePage) InputMessage() l.Widget { + return rp.wg.Body2("Input details to request a payment").Alignment(text.Middle).Fn +} + +func (rp *ReceivePage) MediumList(gtx l.Context) l.Dimensions { + wg := rp.wg + qrWidget := []l.Widget{ + wg.Direction().Center().Embed(rp.QRButton()).Fn, + rp.InputMessage(), + rp.AmountInput(), + rp.MessageInput(), + rp.RegenerateButton(), + // rp.AddressbookHeader(), + } + qrLE := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset(0.25, qrWidget[index]).Fn(gtx) + } + var historyWidget []l.Widget + + historyWidget = append(historyWidget, rp.GetAddressbookHistoryCards("DocBg")...) + historyLE := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset( + 0.25, + historyWidget[index], + ).Fn(gtx) + } + return wg.Flex().AlignStart(). + Rigid( + func(gtx l.Context) l.Dimensions { + gtx.Constraints.Max.X, gtx.Constraints.Min.X = int(wg.TextSize.V*rp.inputWidth), + int(wg.TextSize.V*rp.inputWidth) + return wg.VFlex(). + Rigid( + wg.lists["receiveMedium"]. + Vertical(). + Length(len(qrWidget)). + ListElement(qrLE).Fn, + ).Fn(gtx) + }, + ). + Rigid( + wg.VFlex().AlignStart(). + Rigid( + rp.AddressbookHeader(), + ). + Rigid( + wg.lists["receiveAddresses"]. + Vertical(). + Length(len(historyWidget)). + ListElement(historyLE).Fn, + ). + Fn, + ).Fn(gtx) +} + +func (rp *ReceivePage) Spacer() l.Widget { + return rp.wg.Flex().AlignMiddle().Flexed(1, rp.wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn).Fn +} + +func (rp *ReceivePage) GetAddressbookHistoryCards(bg string) (widgets []l.Widget) { + wg := rp.wg + avail := len(wg.receiveAddressbookClickables) + req := len(wg.State.receiveAddresses) + if req > avail { + for i := 0; i < req-avail; i++ { + wg.receiveAddressbookClickables = append(wg.receiveAddressbookClickables, wg.WidgetPool.GetClickable()) + } + } + for x := range wg.State.receiveAddresses { + j := x + i := len(wg.State.receiveAddresses) - 1 - x + widgets = append( + widgets, func(gtx l.Context) l.Dimensions { + return wg.ButtonLayout( + wg.receiveAddressbookClickables[i].SetClick( + func() { + msg := wg.State.receiveAddresses[i].Message + if len(msg) > 64 { + msg = msg[:64] + } + qrText := fmt.Sprintf( + "parallelcoin:%s?amount=%8.8f&message=%s", + wg.State.receiveAddresses[i].Address, + wg.State.receiveAddresses[i].Amount.ToDUO(), + msg, + ) + D.Ln("clicked receive address list item", j) + if e := clipboard.WriteAll(qrText); E.Chk(e) { + } + wg.GetNewReceivingQRCode(qrText) + rp.urn = qrText + }, + ), + ). + Background(bg). + Embed( + wg.Inset( + 0.25, + wg.VFlex().AlignStart(). + Rigid( + wg.Flex().AlignBaseline(). + Rigid( + wg.Caption(wg.State.receiveAddresses[i].Address). + Font("go regular").Fn, + ). + Flexed( + 1, + wg.Body1(wg.State.receiveAddresses[i].Amount.String()). + Alignment(text.End).Fn, + ). + Fn, + ). + Rigid( + wg.Caption(wg.State.receiveAddresses[i].Message).MaxLines(1).Fn, + ). + Fn, + ). + Fn, + ).Fn(gtx) + }, + ) + } + return +} + +func (rp *ReceivePage) QRMessage() l.Widget { + return rp.wg.Body2("Scan to send or click to copy").Alignment(text.Middle).Fn +} + +func (rp *ReceivePage) GetQRText() string { + wg := rp.wg + msg := wg.inputs["receiveMessage"].GetText() + if len(msg) > 64 { + msg = msg[:64] + } + return fmt.Sprintf( + "parallelcoin:%s?amount=%s&message=%s", + wg.State.currentReceivingAddress.Load().EncodeAddress(), + wg.inputs["receiveAmount"].GetText(), + msg, + ) +} + +func (rp *ReceivePage) QRButton() l.Widget { + wg := rp.wg + if !wg.WalletAndClientRunning() || wg.currentReceiveQRCode == nil { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{} + } + } + return wg.VFlex(). + Rigid( + wg.ButtonLayout( + wg.currentReceiveCopyClickable.SetClick( + func() { + D.Ln("clicked qr code copy clicker") + if e := clipboard.WriteAll(rp.urn); E.Chk(e) { + } + }, + ), + ). + Background("white"). + Embed( + wg.Inset( + 0.125, + wg.Image().Src(*wg.currentReceiveQRCode).Scale(1).Fn, + ).Fn, + ).Fn, + ).Rigid( + rp.QRMessage(), + ).Fn +} + +func (rp *ReceivePage) AddressbookHeader() l.Widget { + wg := rp.wg + return wg.Flex(). + Rigid( + wg.Inset( + 0.25, + wg.H5("Receive Address History").Alignment(text.Middle).Fn, + ).Fn, + ).Fn +} + +func (rp *ReceivePage) AmountInput() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := rp.wg + // gtx.Constraints.Max.X, gtx.Constraints.Min.X = int(wg.TextSize.True*rp.inputWidth), int(wg.TextSize.True*rp.inputWidth) + return wg.inputs["receiveAmount"].Fn(gtx) + } +} + +func (rp *ReceivePage) MessageInput() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := rp.wg + // gtx.Constraints.Max.X, gtx.Constraints.Min.X = int(wg.TextSize.True*rp.inputWidth), int(wg.TextSize.True*rp.inputWidth) + return wg.inputs["receiveMessage"].Fn(gtx) + } +} + +func (rp *ReceivePage) RegenerateButton() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := rp.wg + if wg.inputs["receiveAmount"].GetText() == "" || wg.inputs["receiveMessage"].GetText() == "" { + gtx.Queue = nil + } + // gtx.Constraints.Max.X, gtx.Constraints.Min.X = int(wg.TextSize.True*rp.inputWidth), int(wg.TextSize.True*rp.inputWidth) + return wg.ButtonLayout( + wg.currentReceiveRegenClickable. + SetClick( + func() { + D.Ln("clicked regenerate button") + var amount float64 + var am amt.Amount + var e error + if amount, e = strconv.ParseFloat( + wg.inputs["receiveAmount"].GetText(), + 64, + ); !E.Chk(e) { + if am, e = amt.NewAmount(amount); E.Chk(e) { + } + } + msg := wg.inputs["receiveMessage"].GetText() + if am == 0 || msg == "" { + // never store an entry without both fields filled + return + } + if len(wg.State.receiveAddresses) > 0 && + (wg.State.receiveAddresses[len(wg.State.receiveAddresses)-1].Amount == 0 || + wg.State.receiveAddresses[len(wg.State.receiveAddresses)-1].Message == "") { + // the first entry has neither of these, and newly generated items without them are assumed to + // not be intentional or used addresses so we don't generate a new entry for this case + wg.State.receiveAddresses[len(wg.State.receiveAddresses)-1].Amount = am + wg.State.receiveAddresses[len(wg.State.receiveAddresses)-1].Message = msg + } else { + // go func() { + wg.GetNewReceivingAddress() + msg := wg.inputs["receiveMessage"].GetText() + if len(msg) > 64 { + msg = msg[:64] + // enforce the field length limit + wg.inputs["receiveMessage"].SetText(msg) + } + qrText := fmt.Sprintf( + "parallelcoin:%s?amount=%f&message=%s", + wg.State.currentReceivingAddress.Load().EncodeAddress(), + am.ToDUO(), + msg, + ) + rp.urn = qrText + wg.GetNewReceivingQRCode(rp.urn) + // }() + } + // force user to fill fields again after regenerate to stop duplicate entries especially from + // accidental double clicks/taps + wg.inputs["receiveAmount"].SetText("") + wg.inputs["receiveMessage"].SetText("") + wg.Invalidate() + }, + ), + ). + Background("Primary"). + Embed( + wg.Inset( + 0.5, + wg.H6("regenerate").Color("Light").Fn, + ). + Fn, + ). + Fn(gtx) + } +} diff --git a/cmd/gui/regenerate.go b/cmd/gui/regenerate.go new file mode 100644 index 0000000..57ac4b8 --- /dev/null +++ b/cmd/gui/regenerate.go @@ -0,0 +1,85 @@ +package gui + +import ( + "image" + "path/filepath" + "strconv" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/atotto/clipboard" + + "github.com/p9c/p9/pkg/gel/gio/op/paint" + + "github.com/p9c/p9/pkg/qrcode" +) + +func (wg *WalletGUI) GetNewReceivingAddress() { + D.Ln("GetNewReceivingAddress") + var addr btcaddr.Address + var e error + if addr, e = wg.WalletClient.GetNewAddress("default"); !E.Chk(e) { + D.Ln( + "getting new receiving address", addr.EncodeAddress(), + "previous:", wg.State.currentReceivingAddress.String.Load(), + ) + // save to addressbook + var ae AddressEntry + ae.Address = addr.EncodeAddress() + var amount float64 + if amount, e = strconv.ParseFloat( + wg.inputs["receiveAmount"].GetText(), + 64, + ); !E.Chk(e) { + if ae.Amount, e = amt.NewAmount(amount); E.Chk(e) { + } + } + msg := wg.inputs["receiveMessage"].GetText() + if len(msg) > 64 { + msg = msg[:64] + } + ae.Message = msg + ae.Created = time.Now() + if wg.State.IsReceivingAddress() { + wg.State.receiveAddresses = append(wg.State.receiveAddresses, ae) + } else { + wg.State.receiveAddresses = []AddressEntry{ae} + wg.State.isAddress.Store(true) + } + D.S(wg.State.receiveAddresses) + wg.State.SetReceivingAddress(addr) + filename := filepath.Join(wg.cx.Config.DataDir.V(), "state.json") + if e = wg.State.Save(filename, wg.cx.Config.WalletPass.Bytes(), false); E.Chk(e) { + } + wg.Invalidate() + } +} + +func (wg *WalletGUI) GetNewReceivingQRCode(qrText string) { + wg.currentReceiveRegenerate.Store(false) + var qrc image.Image + D.Ln("generating QR code") + var e error + if qrc, e = qrcode.Encode(qrText, 0, qrcode.ECLevelL, 4); !E.Chk(e) { + iop := paint.NewImageOp(qrc) + wg.currentReceiveQRCode = &iop + wg.currentReceiveQR = wg.ButtonLayout( + wg.currentReceiveCopyClickable.SetClick( + func() { + D.Ln("clicked qr code copy clicker") + if e := clipboard.WriteAll(qrText); E.Chk(e) { + } + }, + ), + ). + Background("white"). + Embed( + wg.Inset( + 0.125, + wg.Image().Src(*wg.currentReceiveQRCode).Scale(1).Fn, + ).Fn, + ).Fn + } +} diff --git a/cmd/gui/send.go b/cmd/gui/send.go new file mode 100644 index 0000000..745276d --- /dev/null +++ b/cmd/gui/send.go @@ -0,0 +1,498 @@ +package gui + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/atotto/clipboard" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/chainhash" +) + +type SendPage struct { + wg *WalletGUI + inputWidth, break1 float32 +} + +func (wg *WalletGUI) GetSendPage() (sp *SendPage) { + sp = &SendPage{ + wg: wg, + inputWidth: 17, + break1: 48, + } + wg.inputs["sendAddress"].SetPasteFunc = sp.pasteFunction + wg.inputs["sendAmount"].SetPasteFunc = sp.pasteFunction + wg.inputs["sendMessage"].SetPasteFunc = sp.pasteFunction + return +} + +func (sp *SendPage) Fn(gtx l.Context) l.Dimensions { + wg := sp.wg + return wg.Responsive( + wg.Size.Load(), gel.Widgets{ + { + Widget: sp.SmallList, + }, + { + Size: sp.break1, + Widget: sp.MediumList, + }, + }, + ).Fn(gtx) +} + +func (sp *SendPage) SmallList(gtx l.Context) l.Dimensions { + wg := sp.wg + smallWidgets := []l.Widget{ + wg.Flex().Rigid(wg.balanceCard()).Fn, + sp.InputMessage(), + sp.AddressInput(), + sp.AmountInput(), + sp.MessageInput(), + wg.Flex(). + Flexed( + 1, + sp.SendButton(), + ). + Rigid( + wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + ). + Rigid( + sp.PasteButton(), + ). + Rigid( + wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + ). + Rigid( + sp.SaveButton(), + ).Fn, + sp.AddressbookHeader(), + } + smallWidgets = append(smallWidgets, sp.GetAddressbookHistoryCards("DocBg")...) + le := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset( + 0.25, + smallWidgets[index], + ).Fn(gtx) + } + return wg.lists["send"]. + Vertical(). + Length(len(smallWidgets)). + ListElement(le).Fn(gtx) +} + +func (sp *SendPage) InputMessage() l.Widget { + return sp.wg.Body2("Enter or paste the details for a payment").Alignment(text.Start).Fn +} + +func (sp *SendPage) MediumList(gtx l.Context) l.Dimensions { + wg := sp.wg + sendFormWidget := []l.Widget{ + wg.balanceCard(), + sp.InputMessage(), + sp.AddressInput(), + sp.AmountInput(), + sp.MessageInput(), + wg.Flex(). + Flexed( + 1, + sp.SendButton(), + ). + Rigid( + wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + ). + Rigid( + sp.PasteButton(), + ). + Rigid( + wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + ). + Rigid( + sp.SaveButton(), + ).Fn, + } + sendLE := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset(0.25, sendFormWidget[index]).Fn(gtx) + } + var historyWidget []l.Widget + historyWidget = append(historyWidget, sp.GetAddressbookHistoryCards("DocBg")...) + historyLE := func(gtx l.Context, index int) l.Dimensions { + return wg.Inset( + 0.25, + historyWidget[index], + ).Fn(gtx) + } + return wg.Flex().AlignStart(). + Rigid( + func(gtx l.Context) l.Dimensions { + gtx.Constraints.Max.X = + int(wg.TextSize.V * sp.inputWidth) + // gtx.Constraints.Min.X = int(wg.TextSize.True * sp.inputWidth) + + return wg.VFlex().AlignStart(). + Rigid( + wg.lists["sendMedium"]. + Vertical(). + Length(len(sendFormWidget)). + ListElement(sendLE).Fn, + ).Fn(gtx) + }, + ). + // Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn). + Flexed( + 1, + wg.VFlex().AlignStart(). + Rigid( + sp.AddressbookHeader(), + ). + Rigid( + wg.lists["sendAddresses"]. + Vertical(). + Length(len(historyWidget)). + ListElement(historyLE).Fn, + ).Fn, + ).Fn(gtx) +} + +func (sp *SendPage) AddressInput() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := sp.wg + return wg.inputs["sendAddress"].Fn(gtx) + } +} + +func (sp *SendPage) AmountInput() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := sp.wg + return wg.inputs["sendAmount"].Fn(gtx) + } +} + +func (sp *SendPage) MessageInput() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := sp.wg + return wg.inputs["sendMessage"].Fn(gtx) + } +} + +func (sp *SendPage) SendButton() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := sp.wg + if wg.inputs["sendAmount"].GetText() == "" || wg.inputs["sendMessage"].GetText() == "" || + wg.inputs["sendAddress"].GetText() == "" { + gtx.Queue = nil + } + return wg.ButtonLayout( + wg.clickables["sendSend"]. + SetClick( + func() { + D.Ln("clicked send button") + go func() { + if wg.WalletAndClientRunning() { + var amount float64 + var am amt.Amount + var e error + if amount, e = strconv.ParseFloat( + wg.inputs["sendAmount"].GetText(), + 64, + ); !E.Chk(e) { + if am, e = amt.NewAmount(amount); E.Chk(e) { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e) + // todo: indicate this to the user somehow + return + } + } else { + // todo: indicate this to the user somehow + return + } + var addr btcaddr.Address + if addr, e = btcaddr.Decode( + wg.inputs["sendAddress"].GetText(), + wg.cx.ActiveNet, + ); E.Chk(e) { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e) + D.Ln("invalid address") + // TODO: indicate this to the user somehow + return + } + if e = wg.WalletClient.WalletPassphrase(wg.cx.Config.WalletPass.V(), 5); E.Chk(e) { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e) + return + } + var txid *chainhash.Hash + if txid, e = wg.WalletClient.SendToAddress(addr, am); E.Chk(e) { + // TODO: indicate send failure to user somehow + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e) + return + } + wg.RecentTransactions(10, "recent") + wg.RecentTransactions(-1, "history") + wg.Invalidate() + D.Ln("transaction successful", txid) + sp.saveForm(txid.String()) + select { + case <-time.After(time.Second * 5): + case <-wg.quit: + } + } + }() + }, + ), + ). + Background("Primary"). + Embed( + wg.Inset( + 0.5, + wg.H6("send").Color("Light").Fn, + ). + Fn, + ). + Fn(gtx) + } +} +func (sp *SendPage) saveForm(txid string) { + wg := sp.wg + D.Ln("processing form data to save") + amtS := wg.inputs["sendAmount"].GetText() + var e error + var amount float64 + if amount, e = strconv.ParseFloat(amtS, 64); E.Chk(e) { + return + } + if amount == 0 { + return + } + var ua amt.Amount + if ua, e = amt.NewAmount(amount); E.Chk(e) { + return + } + msg := wg.inputs["sendMessage"].GetText() + if msg == "" { + return + } + addr := wg.inputs["sendAddress"].GetText() + var ad btcaddr.Address + if ad, e = btcaddr.Decode(addr, wg.cx.ActiveNet); E.Chk(e) { + return + } + wg.State.sendAddresses = append( + wg.State.sendAddresses, AddressEntry{ + Address: ad.EncodeAddress(), + Label: msg, + Amount: ua, + Created: time.Now(), + TxID: txid, + }, + ) + // prevent accidental double clicks recording the same entry again + wg.inputs["sendAmount"].SetText("") + wg.inputs["sendMessage"].SetText("") + wg.inputs["sendAddress"].SetText("") +} + +func (sp *SendPage) SaveButton() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := sp.wg + if wg.inputs["sendAmount"].GetText() == "" || wg.inputs["sendMessage"].GetText() == "" || + wg.inputs["sendAddress"].GetText() == "" { + gtx.Queue = nil + } + return wg.ButtonLayout( + wg.clickables["sendSave"]. + SetClick( + func() { sp.saveForm("") }, + ), + ). + Background("Primary"). + Embed( + wg.Inset( + 0.5, + wg.H6("save").Color("Light").Fn, + ). + Fn, + ). + Fn(gtx) + } +} + +func (sp *SendPage) PasteButton() l.Widget { + return func(gtx l.Context) l.Dimensions { + wg := sp.wg + return wg.ButtonLayout( + wg.clickables["sendFromRequest"]. + SetClick(func() { sp.pasteFunction() }), + ). + Background("Primary"). + Embed( + wg.Inset( + 0.5, + wg.H6("paste").Color("Light").Fn, + ). + Fn, + ). + Fn(gtx) + } +} + +func (sp *SendPage) pasteFunction() (b bool) { + wg := sp.wg + D.Ln("clicked paste button") + var urn string + var e error + if urn, e = clipboard.ReadAll(); E.Chk(e) { + return + } + if !strings.HasPrefix(urn, "parallelcoin:") { + if e = clipboard.WriteAll(urn); E.Chk(e) { + } + return + } + split1 := strings.Split(urn, "parallelcoin:") + split2 := strings.Split(split1[1], "?") + addr := split2[0] + var ua btcaddr.Address + if ua, e = btcaddr.Decode(addr, wg.cx.ActiveNet); E.Chk(e) { + return + } + _ = ua + b = true + wg.inputs["sendAddress"].SetText(addr) + if len(split2) <= 1 { + return + } + split3 := strings.Split(split2[1], "&") + for i := range split3 { + var split4 []string + split4 = strings.Split(split3[i], "=") + D.Ln(split4) + if len(split4) > 1 { + switch split4[0] { + case "amount": + wg.inputs["sendAmount"].SetText(split4[1]) + // D.Ln("############ amount", split4[1]) + case "message", "label": + msg := split4[i] + if len(msg) > 64 { + msg = msg[:64] + } + wg.inputs["sendMessage"].SetText(msg) + // D.Ln("############ message", split4[1]) + } + } + } + return +} + +func (sp *SendPage) AddressbookHeader() l.Widget { + wg := sp.wg + return wg.Flex().AlignStart(). + Rigid( + wg.Inset( + 0.25, + wg.H5("Send Address Book").Fn, + ).Fn, + ).Fn +} + +func (sp *SendPage) GetAddressbookHistoryCards(bg string) (widgets []l.Widget) { + wg := sp.wg + avail := len(wg.sendAddressbookClickables) + req := len(wg.State.sendAddresses) + if req > avail { + for i := 0; i < req-avail; i++ { + wg.sendAddressbookClickables = append(wg.sendAddressbookClickables, wg.WidgetPool.GetClickable()) + } + } + for x := range wg.State.sendAddresses { + j := x + i := len(wg.State.sendAddresses) - 1 - x + widgets = append( + widgets, func(gtx l.Context) l.Dimensions { + return wg.ButtonLayout( + wg.sendAddressbookClickables[i].SetClick( + func() { + sendText := fmt.Sprintf( + "parallelcoin:%s?amount=%8.8f&message=%s", + wg.State.sendAddresses[i].Address, + wg.State.sendAddresses[i].Amount.ToDUO(), + wg.State.sendAddresses[i].Label, + ) + D.Ln("clicked send address list item", j) + if e := clipboard.WriteAll(sendText); E.Chk(e) { + } + }, + ), + ). + Background(bg). + Embed( + wg.Inset( + 0.25, + wg.VFlex().AlignStart(). + Rigid( + wg.Flex().AlignBaseline(). + Rigid( + wg.Caption(wg.State.sendAddresses[i].Address). + Font("go regular").Fn, + ). + Flexed( + 1, + wg.Body1(wg.State.sendAddresses[i].Amount.String()). + Alignment(text.End).Fn, + ). + Fn, + ). + Rigid( + wg.Inset( + 0.25, + wg.Body1(wg.State.sendAddresses[i].Label).MaxLines(1).Fn, + ).Fn, + ). + Rigid( + gel.If( + wg.State.sendAddresses[i].TxID != "", + func(ctx l.Context) l.Dimensions { + for j := range wg.txHistoryList { + if wg.txHistoryList[j].TxID == wg.State.sendAddresses[i].TxID { + return wg.Flex().Flexed( + 1, + wg.VFlex(). + Rigid( + wg.Flex().Flexed( + 1, + wg.Caption(wg.State.sendAddresses[i].TxID).MaxLines(1).Fn, + ).Fn, + ). + Rigid( + wg.Body1( + fmt.Sprint( + "Confirmations: ", + wg.txHistoryList[j].Confirmations, + ), + ).Fn, + ).Fn, + ).Fn(gtx) + } + } + return func(ctx l.Context) l.Dimensions { return l.Dimensions{} }(gtx) + }, + func(ctx l.Context) l.Dimensions { return l.Dimensions{} }, + ), + ). + Fn, + ). + Fn, + ).Fn(gtx) + }, + ) + } + return +} diff --git a/cmd/gui/state.go b/cmd/gui/state.go new file mode 100644 index 0000000..19b7534 --- /dev/null +++ b/cmd/gui/state.go @@ -0,0 +1,319 @@ +package gui + +import ( + "crypto/cipher" + "encoding/json" + "io/ioutil" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + + uberatomic "go.uber.org/atomic" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/gcm" + "github.com/p9c/p9/pkg/transport" + "github.com/p9c/p9/pkg/util/atom" +) + +const ZeroAddress = "1111111111111111111114oLvT2" + +// CategoryFilter marks which transactions to omit from the filtered transaction list +type CategoryFilter struct { + Send bool + Generate bool + Immature bool + Receive bool + Unknown bool +} + +func (c *CategoryFilter) Filter(s string) (include bool) { + include = true + if c.Send && s == "send" { + include = false + } + if c.Generate && s == "generate" { + include = false + } + if c.Immature && s == "immature" { + include = false + } + if c.Receive && s == "receive" { + include = false + } + if c.Unknown && s == "unknown" { + include = false + } + return +} + +type AddressEntry struct { + Address string `json:"address"` + Message string `json:"message,omitempty"` + Label string `json:"label,omitempty"` + Amount amt.Amount `json:"amount"` + Created time.Time `json:"created"` + Modified time.Time `json:"modified"` + TxID string `json:txid,omitempty'` +} + +type State struct { + lastUpdated *atom.Time + bestBlockHeight *atom.Int32 + bestBlockHash *atom.Hash + balance *atom.Float64 + balanceUnconfirmed *atom.Float64 + goroutines []l.Widget + allTxs *atom.ListTransactionsResult + filteredTxs *atom.ListTransactionsResult + filter CategoryFilter + filterChanged *atom.Bool + currentReceivingAddress *atom.Address + isAddress *atom.Bool + activePage *uberatomic.String + sendAddresses []AddressEntry + receiveAddresses []AddressEntry +} + +func GetNewState(params *chaincfg.Params, activePage *uberatomic.String) *State { + fc := &atom.Bool{ + Bool: uberatomic.NewBool(false), + } + return &State{ + lastUpdated: atom.NewTime(time.Now()), + bestBlockHeight: &atom.Int32{Int32: uberatomic.NewInt32(0)}, + bestBlockHash: atom.NewHash(chainhash.Hash{}), + balance: &atom.Float64{Float64: uberatomic.NewFloat64(0)}, + balanceUnconfirmed: &atom.Float64{ + Float64: uberatomic.NewFloat64(0), + }, + goroutines: nil, + allTxs: atom.NewListTransactionsResult( + []btcjson.ListTransactionsResult{}, + ), + filteredTxs: atom.NewListTransactionsResult( + []btcjson.ListTransactionsResult{}, + ), + filter: CategoryFilter{}, + filterChanged: fc, + currentReceivingAddress: atom.NewAddress( + &btcaddr.PubKeyHash{}, + params, + ), + isAddress: &atom.Bool{Bool: uberatomic.NewBool(false)}, + activePage: activePage, + } +} + +func (s *State) BumpLastUpdated() { + s.lastUpdated.Store(time.Now()) +} + +func (s *State) SetReceivingAddress(addr btcaddr.Address) { + s.currentReceivingAddress.Store(addr) +} + +func (s *State) IsReceivingAddress() bool { + addr := s.currentReceivingAddress.String.Load() + if addr == ZeroAddress || addr == "" { + s.isAddress.Store(false) + } else { + s.isAddress.Store(true) + } + return s.isAddress.Load() +} + +// Save the state to the specified file +func (s *State) Save(filename string, pass []byte, debug bool) (e error) { + D.Ln("saving state...") + marshalled := s.Marshal() + var j []byte + if j, e = json.MarshalIndent(marshalled, "", " "); E.Chk(e) { + return + } + // D.Ln(string(j)) + var ciph cipher.AEAD + if ciph, e = gcm.GetCipher(pass); E.Chk(e) { + return + } + var nonce []byte + if nonce, e = transport.GetNonce(ciph); E.Chk(e) { + return + } + crypted := append(nonce, ciph.Seal(nil, nonce, j, nil)...) + var b []byte + _ = b + if b, e = ciph.Open(nil, nonce, crypted[len(nonce):], nil); E.Chk(e) { + // since it was just created it should not fail to decrypt + panic(e) + // interrupt.Request() + return + } + if e = ioutil.WriteFile(filename, crypted, 0600); E.Chk(e) { + } + if debug { + if e = ioutil.WriteFile(filename+".clear", j, 0600); E.Chk(e) { + } + } + return +} + +// Load in the configuration from the specified file and decrypt using the given password +func (s *State) Load(filename string, pass []byte) (e error) { + D.Ln("loading state...") + var data []byte + var ciph cipher.AEAD + if data, e = ioutil.ReadFile(filename); E.Chk(e) { + return + } + D.Ln("cipher:", string(pass)) + if ciph, e = gcm.GetCipher(pass); E.Chk(e) { + return + } + ns := ciph.NonceSize() + D.Ln("nonce size:", ns) + nonce := data[:ns] + data = data[ns:] + var b []byte + if b, e = ciph.Open(nil, nonce, data, nil); E.Chk(e) { + // interrupt.Request() + return + } + // yay, right password, now unmarshal + ss := &Marshalled{} + if e = json.Unmarshal(b, ss); E.Chk(e) { + return + } + // D.Ln(string(b)) + ss.Unmarshal(s) + return +} + +type Marshalled struct { + LastUpdated time.Time + BestBlockHeight int32 + BestBlockHash chainhash.Hash + Balance float64 + BalanceUnconfirmed float64 + AllTxs []btcjson.ListTransactionsResult + Filter CategoryFilter + ReceivingAddress string + ActivePage string + ReceiveAddressBook []AddressEntry + SendAddressBook []AddressEntry +} + +func (s *State) Marshal() (out *Marshalled) { + out = &Marshalled{ + LastUpdated: s.lastUpdated.Load(), + BestBlockHeight: s.bestBlockHeight.Load(), + BestBlockHash: s.bestBlockHash.Load(), + Balance: s.balance.Load(), + BalanceUnconfirmed: s.balanceUnconfirmed.Load(), + AllTxs: s.allTxs.Load(), + Filter: s.filter, + ReceivingAddress: s.currentReceivingAddress.Load().EncodeAddress(), + ActivePage: s.activePage.Load(), + ReceiveAddressBook: s.receiveAddresses, + SendAddressBook: s.sendAddresses, + } + return +} + +func (m *Marshalled) Unmarshal(s *State) { + s.lastUpdated.Store(m.LastUpdated) + s.bestBlockHeight.Store(m.BestBlockHeight) + s.bestBlockHash.Store(m.BestBlockHash) + s.balance.Store(m.Balance) + s.balanceUnconfirmed.Store(m.BalanceUnconfirmed) + if len(s.allTxs.Load()) < len(m.AllTxs) { + s.allTxs.Store(m.AllTxs) + } + s.receiveAddresses = m.ReceiveAddressBook + s.sendAddresses = m.SendAddressBook + s.filter = m.Filter + + if m.ReceivingAddress != "1111111111111111111114oLvT2" { + var e error + var ra btcaddr.Address + if ra, e = btcaddr.Decode(m.ReceivingAddress, s.currentReceivingAddress.ForNet); E.Chk(e) { + } + s.currentReceivingAddress.Store(ra) + } + s.SetActivePage(m.ActivePage) + return +} + +func (s *State) Goroutines() []l.Widget { + return s.goroutines +} + +func (s *State) SetGoroutines(gr []l.Widget) { + s.goroutines = gr +} + +func (s *State) SetAllTxs(atxs []btcjson.ListTransactionsResult) { + s.allTxs.Store(atxs) + // generate filtered state + filteredTxs := make([]btcjson.ListTransactionsResult, 0, len(s.allTxs.Load())) + for i := range atxs { + if s.filter.Filter(atxs[i].Category) { + filteredTxs = append(filteredTxs, atxs[i]) + } + } + s.filteredTxs.Store(filteredTxs) +} + +func (s *State) LastUpdated() time.Time { + return s.lastUpdated.Load() +} + +func (s *State) BestBlockHeight() int32 { + return s.bestBlockHeight.Load() +} + +func (s *State) BestBlockHash() *chainhash.Hash { + o := s.bestBlockHash.Load() + return &o +} + +func (s *State) Balance() float64 { + return s.balance.Load() +} + +func (s *State) BalanceUnconfirmed() float64 { + return s.balanceUnconfirmed.Load() +} + +func (s *State) ActivePage() string { + return s.activePage.Load() +} + +func (s *State) SetActivePage(page string) { + s.activePage.Store(page) +} + +func (s *State) SetBestBlockHeight(height int32) { + s.BumpLastUpdated() + s.bestBlockHeight.Store(height) +} + +func (s *State) SetBestBlockHash(h *chainhash.Hash) { + s.BumpLastUpdated() + s.bestBlockHash.Store(*h) +} + +func (s *State) SetBalance(total float64) { + s.BumpLastUpdated() + s.balance.Store(total) +} + +func (s *State) SetBalanceUnconfirmed(unconfirmed float64) { + s.BumpLastUpdated() + s.balanceUnconfirmed.Store(unconfirmed) +} diff --git a/cmd/gui/walletunlock.go b/cmd/gui/walletunlock.go new file mode 100644 index 0000000..32f69be --- /dev/null +++ b/cmd/gui/walletunlock.go @@ -0,0 +1,529 @@ +package gui + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "time" + + "golang.org/x/exp/shiny/materialdesign/icons" + "lukechampine.com/blake3" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/p9icons" +) + +func (wg *WalletGUI) unlockWallet(pass string) { + D.Ln("entered password", pass) + // unlock wallet + // wg.cx.Config.Lock() + wg.cx.Config.WalletPass.Set(pass) + wg.cx.Config.WalletOff.F() + // wg.cx.Config.Unlock() + // load config into a fresh variable + // cfg := podcfgs.GetDefaultConfig() + var cfgFile []byte + var e error + if cfgFile, e = ioutil.ReadFile(wg.cx.Config.ConfigFile.V()); E.Chk(e) { + // this should not happen + // TODO: panic-type conditions - for gel should have a notification maybe? + panic("config file does not exist") + } + cfg := wg.cx.Config + D.Ln("loaded config") + if e = json.Unmarshal(cfgFile, &cfg); !E.Chk(e) { + D.Ln("unmarshaled config") + bhb := blake3.Sum256([]byte(pass)) + bh := hex.EncodeToString(bhb[:]) + I.Ln(pass, bh, cfg.WalletPass.V()) + if cfg.WalletPass.V() == bh { + D.Ln("loading previously saved state") + filename := filepath.Join(wg.cx.Config.DataDir.V(), "state.json") + // if log.FileExists(filename) { + I.Ln("#### loading state data...") + if e = wg.State.Load(filename, wg.cx.Config.WalletPass.Bytes()); !E.Chk(e) { + D.Ln("#### loaded state data") + } + // it is as though it is loaded if it didn't exist + wg.stateLoaded.Store(true) + // the entered password matches the stored hash + wg.cx.Config.NodeOff.F() + wg.cx.Config.WalletOff.F() + wg.cx.Config.WalletPass.Set(pass) + if e = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()); E.Chk(e) { + } + wg.cx.Config.WalletPass.Set(pass) + wg.WalletWatcher = wg.Watcher() + // } + // + // qrText := fmt.Sprintf("parallelcoin:%s?amount=%s&message=%s", + // wg.State.currentReceivingAddress.Load().EncodeAddress(), + // wg.inputs["receiveAmount"].GetText(), + // wg.inputs["receiveMessage"].GetText(), + // ) + // var qrc image.Image + // if qrc, e = qrcode.Encode(qrText, 0, qrcode.ECLevelL, 4); !E.Chk(e) { + // iop := paint.NewImageOp(qrc) + // wg.currentReceiveQRCode = &iop + // wg.currentReceiveQR = wg.ButtonLayout(wg.currentReceiveCopyClickable.SetClick(func() { + // D.Ln("clicked qr code copy clicker") + // if e := clipboard.WriteAll(qrText); E.Chk(e) { + // } + // })). + // // CornerRadius(0.5). + // // Corners(gel.NW | gel.SW | gel.NE). + // Background("white"). + // Embed( + // wg.Inset(0.125, + // wg.Image().Src(*wg.currentReceiveQRCode).Scale(1).Fn, + // ).Fn, + // ).Fn + // // *wg.currentReceiveQRCode = iop + // } + } + } else { + D.Ln("failed to unlock the wallet") + } +} + +func (wg *WalletGUI) getWalletUnlockAppWidget() (a *gel.App) { + a = wg.App(wg.Window.Width, wg.State.activePage, Break1). + SetMainDirection(l.Center + 1). + SetLogo(&p9icons.ParallelCoin). + SetAppTitleText("Parallelcoin Wallet") + wg.unlockPage = a + password := wg.cx.Config.WalletPass + exitButton := wg.WidgetPool.GetClickable() + unlockButton := wg.WidgetPool.GetClickable() + wg.unlockPassword = wg.Password( + "enter password", password, "DocText", + "DocBg", "PanelBg", func(pass string) { + I.Ln("wallet unlock initiated", pass) + wg.unlockWallet(pass) + }, + ) + // wg.unlockPage.SetThemeHook( + // func() { + // D.Ln("theme hook") + // // D.Ln(wg.bools) + // wg.cx.Config.DarkTheme.Set(*wg.Dark) + // b := wg.configs["config"]["DarkTheme"].Slot.(*bool) + // *b = *wg.Dark + // if wgb, ok := wg.config.Bools["DarkTheme"]; ok { + // wgb.Value(*wg.Dark) + // } + // var e error + // if e = wg.cx.Config.WriteToFile(wg.cx.Config.ConfigFile.V()); E.Chk(e) { + // } + // }, + // ) + a.Pages( + map[string]l.Widget{ + "home": wg.Page( + "home", gel.Widgets{ + gel.WidgetSize{ + Widget: + func(gtx l.Context) l.Dimensions { + var dims l.Dimensions + return wg.Flex(). + AlignMiddle(). + Flexed( + 1, + wg.VFlex(). + Flexed(0.5, gel.EmptyMaxHeight()). + Rigid( + wg.Flex(). + SpaceEvenly(). + AlignMiddle(). + Rigid( + wg.Fill( + "DocBg", l.Center, wg.TextSize.V, 0, + wg.Inset( + 0.5, + wg.Flex(). + AlignMiddle(). + Rigid( + wg.VFlex(). + AlignMiddle(). + Rigid( + func(gtx l.Context) l.Dimensions { + dims = wg.Flex(). + AlignBaseline(). + Rigid( + wg.Fill( + "Fatal", + l.Center, + wg.TextSize.V/2, + 0, + wg.Inset( + 0.5, + wg.Icon(). + Scale(gel.Scales["H3"]). + Color("DocBg"). + Src(&icons.ActionLock).Fn, + ).Fn, + ).Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace(0, 0), + ).Fn, + ). + Rigid( + wg.H2("locked").Color("DocText").Fn, + ). + Fn(gtx) + return dims + }, + ). + Rigid(wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn). + Rigid( + func(gtx l.Context) l. + Dimensions { + gtx.Constraints.Max. + X = dims.Size.X + return wg. + unlockPassword. + Fn(gtx) + }, + ). + Rigid(wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn). + Rigid( + wg.Body1( + fmt.Sprintf( + "%v idle timeout", + time.Duration(wg.incdecs["idleTimeout"].GetCurrent())*time.Second, + ), + ). + Color("DocText"). + Font("bariol bold"). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + wg.Body1("Idle timeout in seconds:").Color( + "DocText", + ).Fn, + ). + Rigid( + wg.incdecs["idleTimeout"]. + Color("DocText"). + Background("DocBg"). + Scale(gel.Scales["Caption"]). + Fn, + ). + Fn, + ). + Rigid( + wg.Flex(). + Rigid( + wg.Inset( + 0.25, + wg.ButtonLayout( + exitButton.SetClick( + func() { + interrupt.Request() + }, + ), + ). + CornerRadius(0.5). + Corners(0). + Background("PanelBg"). + Embed( + // wg.Fill("DocText", + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale( + gel.Scales["H4"], + ). + Color("DocText"). + Src( + &icons. + MapsDirectionsRun, + ).Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Rigid( + wg.H6("exit").Color("DocText").Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Fn, + ).Fn, + // l.Center, + // wg.TextSize.True/2).Fn, + ).Fn, + ).Fn, + ). + Rigid( + wg.Inset( + 0.25, + wg.ButtonLayout( + unlockButton.SetClick( + func() { + // pass := wg.unlockPassword.Editor().Text() + pass := wg.unlockPassword.GetPassword() + D.Ln( + ">>>>>>>>>>> unlock password", + pass, + ) + wg.unlockWallet(pass) + + }, + ), + ).Background("Primary"). + CornerRadius(0.5). + Corners(0). + Embed( + wg.Inset( + 0.25, + wg.Flex().AlignMiddle(). + Rigid( + wg.Icon(). + Scale(gel.Scales["H4"]). + Color("Light"). + Src(&icons.ActionLockOpen).Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Rigid( + wg.H6("unlock").Color("Light").Fn, + ). + Rigid( + wg.Inset( + 0.5, + gel.EmptySpace( + 0, + 0, + ), + ).Fn, + ). + Fn, + ).Fn, + ).Fn, + ).Fn, + ). + Fn, + ). + Fn, + ). + Fn, + ).Fn, + ).Fn, + ). + Fn, + ).Flexed(0.5, gel.EmptyMaxHeight()).Fn, + ). + Fn(gtx) + }, + }, + }, + ), + "settings": wg.Page( + "settings", gel.Widgets{ + gel.WidgetSize{ + Widget: func(gtx l.Context) l.Dimensions { + return wg.configs.Widget(wg.config)(gtx) + }, + }, + }, + ), + "console": wg.Page( + "console", gel.Widgets{ + gel.WidgetSize{Widget: wg.console.Fn}, + }, + ), + "help": wg.Page( + "help", gel.Widgets{ + gel.WidgetSize{Widget: wg.HelpPage()}, + }, + ), + "log": wg.Page( + "log", gel.Widgets{ + gel.WidgetSize{Widget: a.Placeholder("log")}, + }, + ), + "quit": wg.Page( + "quit", gel.Widgets{ + gel.WidgetSize{ + Widget: func(gtx l.Context) l.Dimensions { + return wg.VFlex(). + SpaceEvenly(). + AlignMiddle(). + Rigid( + wg.H4("are you sure?").Color(wg.unlockPage.BodyColorGet()).Alignment(text.Middle).Fn, + ). + Rigid( + wg.Flex(). + // SpaceEvenly(). + Flexed(0.5, gel.EmptyMaxWidth()). + Rigid( + wg.Button( + wg.clickables["quit"].SetClick( + func() { + wg.gracefulShutdown() + // close(wg.quit) + }, + ), + ).Color("Light").TextScale(5).Text( + "yes!!!", + ).Fn, + ). + Flexed(0.5, gel.EmptyMaxWidth()). + Fn, + ). + Fn(gtx) + }, + }, + }, + ), + // "goroutines": wg.Page( + // "log", p9.Widgets{ + // // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + // + // p9.WidgetSize{ + // Widget: func(gtx l.Context) l.Dimensions { + // le := func(gtx l.Context, index int) l.Dimensions { + // return wg.State.goroutines[index](gtx) + // } + // return func(gtx l.Context) l.Dimensions { + // return wg.ButtonInset( + // 0.25, + // wg.Fill( + // "DocBg", + // wg.lists["recent"]. + // Vertical(). + // // Background("DocBg").Color("DocText").Active("Primary"). + // Length(len(wg.State.goroutines)). + // ListElement(le). + // Fn, + // ).Fn, + // ). + // Fn(gtx) + // }(gtx) + // // wg.NodeRunCommandChan <- "stop" + // // consume.Kill(wg.Worker) + // // consume.Kill(wg.cx.StateCfg.Miner) + // // close(wg.cx.NodeKill) + // // close(wg.cx.KillAll) + // // time.Sleep(time.Second*3) + // // interrupt.Request() + // // os.Exit(0) + // // return l.Dimensions{} + // }, + // }, + // }, + // ), + "mining": wg.Page( + "mining", gel.Widgets{ + gel.WidgetSize{Widget: a.Placeholder("mining")}, + }, + ), + "explorer": wg.Page( + "explorer", gel.Widgets{ + gel.WidgetSize{Widget: a.Placeholder("explorer")}, + }, + ), + }, + ) + // a.SideBar([]l.Widget{ + // wg.SideBarButton("overview", "overview", 0), + // wg.SideBarButton("send", "send", 1), + // wg.SideBarButton("receive", "receive", 2), + // wg.SideBarButton("history", "history", 3), + // wg.SideBarButton("explorer", "explorer", 6), + // wg.SideBarButton("mining", "mining", 7), + // wg.SideBarButton("console", "console", 9), + // wg.SideBarButton("settings", "settings", 5), + // wg.SideBarButton("log", "log", 10), + // wg.SideBarButton("help", "help", 8), + // wg.SideBarButton("quit", "quit", 11), + // }) + a.ButtonBar( + []l.Widget{ + // wg.PageTopBarButton( + // "goroutines", 0, &icons.ActionBugReport, func(name string) { + // wg.unlockPage.ActivePage(name) + // }, wg.unlockPage, "", + // ), + wg.PageTopBarButton( + "help", 1, &icons.ActionHelp, func(name string) { + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, "", + ), + wg.PageTopBarButton( + "home", 4, &icons.ActionLock, func(name string) { + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, "Danger", + ), + // wg.Flex().Rigid(wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn).Fn, + // wg.PageTopBarButton( + // "quit", 3, &icons.ActionExitToApp, func(name string) { + // wg.unlockPage.ActivePage(name) + // }, wg.unlockPage, "", + // ), + }, + ) + a.StatusBar( + []l.Widget{ + // wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + wg.RunStatusPanel, + }, + []l.Widget{ + wg.StatusBarButton( + "console", 3, &p9icons.Terminal, func(name string) { + wg.MainApp.ActivePage(name) + }, a, + ), + wg.StatusBarButton( + "log", 4, &icons.ActionList, func(name string) { + D.Ln("click on button", name) + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, + ), + wg.StatusBarButton( + "settings", 5, &icons.ActionSettings, func(name string) { + wg.unlockPage.ActivePage(name) + }, wg.unlockPage, + ), + // wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn, + }, + ) + // a.PushOverlay(wg.toasts.DrawToasts()) + // a.PushOverlay(wg.dialog.DrawDialog()) + return +} diff --git a/cmd/gui/watcher.go b/cmd/gui/watcher.go new file mode 100644 index 0000000..491dfe3 --- /dev/null +++ b/cmd/gui/watcher.go @@ -0,0 +1,158 @@ +package gui + +import ( + "time" + + "github.com/p9c/p9/pkg/btcjson" + + "github.com/p9c/p9/pkg/qu" +) + +// Watcher keeps the chain and wallet and rpc clients connected +func (wg *WalletGUI) Watcher() qu.C { + var e error + I.Ln("starting up watcher") + quit := qu.T() + // start things up first + if !wg.node.Running() { + D.Ln("watcher starting node") + wg.node.Start() + } + if wg.ChainClient == nil { + D.Ln("chain client is not initialized") + var e error + if e = wg.chainClient(); E.Chk(e) { + } + } + if !wg.wallet.Running() { + D.Ln("watcher starting wallet") + wg.wallet.Start() + D.Ln("now we can open the wallet") + if e = wg.writeWalletCookie(); E.Chk(e) { + } + } + if wg.WalletClient == nil || wg.WalletClient.Disconnected() { + allOut: + for { + if e = wg.walletClient(); !E.Chk(e) { + out: + for { + // keep trying until shutdown or the wallet client connects + I.Ln("attempting to get blockchain info from wallet") + var bci *btcjson.GetBlockChainInfoResult + if bci, e = wg.WalletClient.GetBlockChainInfo(); E.Chk(e) { + select { + case <-time.After(time.Second): + continue + case <-wg.quit: + return nil + } + } + D.S(bci) + break out + } + } + wg.unlockPassword.Wipe() + select { + case <-time.After(time.Second): + break allOut + case <-wg.quit: + return nil + } + } + } + go func() { + + watchTick := time.NewTicker(time.Second) + var e error + totalOut: + for { + disconnected: + for { + D.Ln("top of watcher loop") + select { + case <-watchTick.C: + if e = wg.Advertise(); E.Chk(e) { + } + if !wg.node.Running() { + D.Ln("watcher starting node") + wg.node.Start() + } + if wg.ChainClient.Disconnected() { + if e = wg.chainClient(); E.Chk(e) { + continue + } + } + if !wg.wallet.Running() { + D.Ln("watcher starting wallet") + wg.wallet.Start() + } + if wg.WalletClient == nil { + D.Ln("wallet client is not initialized") + if e = wg.walletClient(); E.Chk(e) { + continue + // } else { + // break disconnected + } + } + if wg.WalletClient.Disconnected() { + if e = wg.WalletClient.Connect(1); D.Chk(e) { + continue + // } else { + // break disconnected + } + } else { + D.Ln( + "chain, chainclient, wallet and client are now connected", + wg.node.Running(), + !wg.ChainClient.Disconnected(), + wg.wallet.Running(), + !wg.WalletClient.Disconnected(), + ) + wg.updateChainBlock() + wg.processWalletBlockNotification() + break disconnected + } + case <-quit.Wait(): + break totalOut + case <-wg.quit.Wait(): + break totalOut + } + } + if wg.cx.Config.Controller.True() { + if wg.ChainClient != nil { + if e = wg.ChainClient.SetGenerate( + wg.cx.Config.Controller.True(), + wg.cx.Config.GenThreads.V(), + ); !E.Chk(e) { + } + } + } + connected: + for { + select { + case <-watchTick.C: + if !wg.wallet.Running() { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> wallet not running, breaking out") + break connected + } + if wg.WalletClient == nil || wg.WalletClient.Disconnected() { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>> wallet client disconnected, breaking out") + break connected + } + case <-quit.Wait(): + break totalOut + case <-wg.quit.Wait(): + break totalOut + } + } + } + D.Ln("shutting down watcher") + if wg.WalletClient != nil { + wg.WalletClient.Disconnect() + wg.WalletClient.Shutdown() + } + wg.wallet.Stop() + }() + return quit +} diff --git a/cmd/kopach/client/log.go b/cmd/kopach/client/log.go new file mode 100644 index 0000000..da3fddb --- /dev/null +++ b/cmd/kopach/client/log.go @@ -0,0 +1,43 @@ +package client + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/kopach/client/wrapper.go b/cmd/kopach/client/wrapper.go new file mode 100644 index 0000000..60b4b54 --- /dev/null +++ b/cmd/kopach/client/wrapper.go @@ -0,0 +1,83 @@ +package client + +import ( + "errors" + "io" + "net/rpc" + + "github.com/p9c/p9/pkg/chainrpc/templates" +) + +type Client struct { + *rpc.Client +} + +// New creates a new client for a kopach_worker. Note that any kind of connection can be used here, other than the +// StdConn +func New(conn io.ReadWriteCloser) *Client { + return &Client{rpc.NewClient(conn)} +} + +// NewJob is a delivery of a new job for the worker, this starts a miner +// note that since this implements net/rpc by default this is gob encoded +func (c *Client) NewJob(templates *templates.Message) (e error) { + // T.Ln("sending new templates") + // D.S(templates) + if templates == nil { + e = errors.New("templates is nil") + return + } + var reply bool + if e = c.Call("Worker.NewJob", templates, &reply); E.Chk(e) { + return + } + if reply != true { + e = errors.New("new templates command not acknowledged") + } + D.Ln("new job delivered to workers") + return +} + +// Pause tells the worker to stop working, this is for when the controlling node +// is not current +func (c *Client) Pause() (e error) { + // D.Ln("sending pause") + var reply bool + e = c.Call("Worker.Pause", 1, &reply) + if e != nil { + return + } + if reply != true { + e = errors.New("pause command not acknowledged") + } + return +} + +// Stop the workers +func (c *Client) Stop() (e error) { + D.Ln("stop working (exit)") + var reply bool + e = c.Call("Worker.Stop", 1, &reply) + if e != nil { + return + } + if reply != true { + e = errors.New("stop command not acknowledged") + } + return +} + +// SendPass sends the multicast PSK to the workers so they can dispatch their +// solutions +func (c *Client) SendPass(pass []byte) (e error) { + D.Ln("sending dispatch password") + var reply bool + e = c.Call("Worker.SendPass", pass, &reply) + if e != nil { + return + } + if reply != true { + e = errors.New("send pass command not acknowledged") + } + return +} diff --git a/cmd/kopach/kopach.go b/cmd/kopach/kopach.go new file mode 100644 index 0000000..5e1b1a4 --- /dev/null +++ b/cmd/kopach/kopach.go @@ -0,0 +1,414 @@ +package kopach + +import ( + "context" + "crypto/rand" + "fmt" + "net" + "os" + "runtime" + "time" + + "github.com/niubaoshu/gotiny" + + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/pkg/chainrpc/p2padvt" + "github.com/p9c/p9/pkg/chainrpc/templates" + "github.com/p9c/p9/pkg/constant" + "github.com/p9c/p9/pkg/pipe" + "github.com/p9c/p9/pod/state" + + "github.com/p9c/p9/pkg/qu" + + "github.com/VividCortex/ewma" + "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/cmd/kopach/client" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/chainrpc/hashrate" + "github.com/p9c/p9/pkg/chainrpc/job" + "github.com/p9c/p9/pkg/chainrpc/pause" + rav "github.com/p9c/p9/pkg/ring" + "github.com/p9c/p9/pkg/transport" +) + +var maxThreads = float32(runtime.NumCPU()) + +type HashCount struct { + uint64 + Time time.Time +} + +type SolutionData struct { + time time.Time + height int + algo string + hash string + indexHash string + version int32 + prevBlock string + merkleRoot string + timestamp time.Time + bits uint32 + nonce uint32 +} + +type Worker struct { + id string + cx *state.State + height int32 + active atomic.Bool + conn *transport.Channel + ctx context.Context + quit qu.C + sendAddresses []*net.UDPAddr + clients []*client.Client + workers []*pipe.Worker + FirstSender atomic.Uint64 + lastSent atomic.Int64 + Status atomic.String + HashTick chan HashCount + LastHash *chainhash.Hash + StartChan, StopChan qu.C + // SetThreads chan int + PassChan chan string + solutions []SolutionData + solutionCount int + Update qu.C + hashCount atomic.Uint64 + hashSampleBuf *rav.BufferUint64 + hashrate float64 + lastNonce uint64 +} + +func (w *Worker) Start() { + D.Ln("starting up kopach workers") + w.workers = []*pipe.Worker{} + w.clients = []*client.Client{} + for i := 0; i < w.cx.Config.GenThreads.V(); i++ { + D.Ln("starting worker", i) + cmd, _ := pipe.Spawn(w.quit, os.Args[0], "worker", w.id, w.cx.ActiveNet.Name, w.cx.Config.LogLevel.V()) + w.workers = append(w.workers, cmd) + w.clients = append(w.clients, client.New(cmd.StdConn)) + } + for i := range w.clients { + T.Ln("sending pass to worker", i) + e := w.clients[i].SendPass(w.cx.Config.MulticastPass.Bytes()) + if e != nil { + } + } + D.Ln("setting workers to active") + w.active.Store(true) + +} + +func (w *Worker) Stop() { + var e error + for i := range w.clients { + if e = w.clients[i].Pause(); E.Chk(e) { + } + if e = w.clients[i].Stop(); E.Chk(e) { + } + if e = w.clients[i].Close(); E.Chk(e) { + } + } + for i := range w.workers { + // if e = w.workers[i].Interrupt(); !E.Chk(e) { + // } + if e = w.workers[i].Kill(); !E.Chk(e) { + } + D.Ln("stopped worker", i) + } + w.active.Store(false) + w.quit.Q() +} + +// Run the miner +func Run(cx *state.State) (e error) { + D.Ln("miner starting") + randomBytes := make([]byte, 4) + if _, e = rand.Read(randomBytes); E.Chk(e) { + } + w := &Worker{ + id: fmt.Sprintf("%x", randomBytes), + cx: cx, + quit: cx.KillAll, + sendAddresses: []*net.UDPAddr{}, + StartChan: qu.T(), + StopChan: qu.T(), + // SetThreads: make(chan int), + solutions: make([]SolutionData, 0, 2048), + Update: qu.T(), + hashSampleBuf: rav.NewBufferUint64(1000), + } + w.lastSent.Store(time.Now().UnixNano()) + w.active.Store(false) + D.Ln("opening broadcast channel listener") + w.conn, e = transport.NewBroadcastChannel( + "kopachmain", w, cx.Config.MulticastPass.Bytes(), + transport.DefaultPort, constant.MaxDatagramSize, handlers, + w.quit, + ) + if e != nil { + return + } + // start up the workers + // if cx.Config.Generate.True() { + I.Ln("starting up miner workers") + w.Start() + interrupt.AddHandler( + func() { + w.Stop() + }, + ) + // } + // controller watcher thread + go func() { + D.Ln("starting controller watcher") + ticker := time.NewTicker(time.Second) + logger := time.NewTicker(time.Second) + out: + for { + select { + case <-ticker.C: + W.Ln("controller watcher ticker") + // if the last message sent was 3 seconds ago the server is almost certainly disconnected or crashed + // so clear FirstSender + since := time.Now().Sub(time.Unix(0, w.lastSent.Load())) + wasSending := since > time.Second*6 && w.FirstSender.Load() != 0 + if wasSending { + D.Ln("previous current controller has stopped broadcasting", since, w.FirstSender.Load()) + // when this string is clear other broadcasts will be listened to + w.FirstSender.Store(0) + // pause the workers + for i := range w.clients { + D.Ln("sending pause to worker", i) + e := w.clients[i].Pause() + if e != nil { + } + } + } + // if interrupt.Requested() { + // w.StopChan <- struct{}{} + // w.quit.Q() + // } + case <-logger.C: + W.Ln("hash report ticker") + w.hashrate = w.HashReport() + // if interrupt.Requested() { + // w.StopChan <- struct{}{} + // w.quit.Q() + // } + case <-w.StartChan.Wait(): + D.Ln("received signal on StartChan") + cx.Config.Generate.T() + // if e = cx.Config.WriteToFile(cx.Config.ConfigFile.V()); E.Chk(e) { + // } + w.Start() + case <-w.StopChan.Wait(): + D.Ln("received signal on StopChan") + cx.Config.Generate.F() + // if e = cx.Config.WriteToFile(cx.Config.ConfigFile.V()); E.Chk(e) { + // } + w.Stop() + case s := <-w.PassChan: + F.Ln("received signal on PassChan", s) + cx.Config.MulticastPass.Set(s) + // if e = cx.Config.WriteToFile(cx.Config.ConfigFile.V()); E.Chk(e) { + // } + w.Stop() + w.Start() + // case n := <-w.SetThreads: + // D.Ln("received signal on SetThreads", n) + // cx.Config.GenThreads.Set(n) + // // if e = cx.Config.WriteToFile(cx.Config.ConfigFile.V()); E.Chk(e) { + // // } + // if cx.Config.Generate.True() { + // // always sanitise + // if n < 0 { + // n = int(maxThreads) + // } + // if n > int(maxThreads) { + // n = int(maxThreads) + // } + // w.Stop() + // w.Start() + // } + case <-w.quit.Wait(): + D.Ln("stopping from quit") + interrupt.Request() + break out + } + } + D.Ln("finished kopach miner work loop") + log.LogChanDisabled.Store(true) + }() + D.Ln("listening on", constant.UDP4MulticastAddress) + <-w.quit + I.Ln("kopach shutting down") // , interrupt.GoroutineDump()) + // <-interrupt.HandlersDone + I.Ln("kopach finished shutdown") + return +} + +// these are the handlers for specific message types. +var handlers = transport.Handlers{ + string(hashrate.Magic): func( + ctx interface{}, src net.Addr, dst string, b []byte, + ) (e error) { + c := ctx.(*Worker) + if !c.active.Load() { + D.Ln("not active") + return + } + var hr hashrate.Hashrate + gotiny.Unmarshal(b, &hr) + // if this is not one of our workers reports ignore it + if hr.ID != c.id { + return + } + count := hr.Count + hc := c.hashCount.Load() + uint64(count) + c.hashCount.Store(hc) + return + }, + string(job.Magic): func( + ctx interface{}, src net.Addr, dst string, + b []byte, + ) (e error) { + w := ctx.(*Worker) + if !w.active.Load() { + T.Ln("not active") + return + } + jr := templates.Message{} + gotiny.Unmarshal(b, &jr) + w.height = jr.Height + cN := jr.UUID + firstSender := w.FirstSender.Load() + otherSent := firstSender != cN && firstSender != 0 + if otherSent { + T.Ln("ignoring other controller job", jr.Nonce, jr.UUID) + // ignore other controllers while one is active and received first + return + } + // if jr.Nonce == w.lastNonce { + // I.Ln("same job again, ignoring (NOT)") + // // return + // } + // w.lastNonce = jr.Nonce + // w.FirstSender.Store(cN) + T.Ln("received job, starting workers on it", jr.Nonce, jr.UUID) + w.lastSent.Store(time.Now().UnixNano()) + for i := range w.clients { + if e = w.clients[i].NewJob(&jr); E.Chk(e) { + } + } + return + }, + string(pause.Magic): func( + ctx interface{}, src net.Addr, dst string, b []byte, + ) (e error) { + w := ctx.(*Worker) + var advt p2padvt.Advertisment + gotiny.Unmarshal(b, &advt) + // p := pause.LoadPauseContainer(b) + fs := w.FirstSender.Load() + ni := advt.IPs + // ni := p.GetIPs()[0].String() + np := advt.UUID + // np := p.GetControllerListenerPort() + // ns := net.JoinHostPort(strings.Split(ni.String(), ":")[0], fmt.Sprint(np)) + D.Ln("received pause from server at", ni, np, "stopping", len(w.clients), "workers stopping") + if fs == np { + for i := range w.clients { + // D.Ln("sending pause to worker", i, fs, np) + e := w.clients[i].Pause() + if e != nil { + } + } + } + w.FirstSender.Store(0) + return + }, + // string(sol.Magic): func( + // ctx interface{}, src net.Addr, dst string, + // b []byte, + // ) (e error) { + // // w := ctx.(*Worker) + // // I.Ln("shuffling work due to solution on network") + // // w.FirstSender.Store(0) + // // D.Ln("solution detected from miner at", src) + // // portSlice := strings.Split(w.FirstSender.Load(), ":") + // // if len(portSlice) < 2 { + // // D.Ln("error with solution", w.FirstSender.Load(), portSlice) + // // return + // // } + // // // port := portSlice[1] + // // // j := sol.LoadSolContainer(b) + // // // senderPort := j.GetSenderPort() + // // // if fmt.Sprint(senderPort) == port { + // // // // W.Ln("we found a solution") + // // // // prepend to list of solutions for GUI display if enabled + // // // if *w.cx.Config.KopachGUI { + // // // // D.Ln("length solutions", len(w.solutions)) + // // // blok := j.GetMsgBlock() + // // // w.solutions = append( + // // // w.solutions, []SolutionData{ + // // // { + // // // time: time.Now(), + // // // height: int(w.height), + // // // algo: fmt.Sprint( + // // // fork.GetAlgoName(blok.Header.Version, w.height), + // // // ), + // // // hash: blok.Header.BlockHashWithAlgos(w.height).String(), + // // // indexHash: blok.Header.BlockHash().String(), + // // // version: blok.Header.Version, + // // // prevBlock: blok.Header.PrevBlock.String(), + // // // merkleRoot: blok.Header.MerkleRoot.String(), + // // // timestamp: blok.Header.Timestamp, + // // // bits: blok.Header.Bits, + // // // nonce: blok.Header.Nonce, + // // // }, + // // // }..., + // // // ) + // // // if len(w.solutions) > 2047 { + // // // w.solutions = w.solutions[len(w.solutions)-2047:] + // // // } + // // // w.solutionCount = len(w.solutions) + // // // w.Update <- struct{}{} + // // // } + // // // } + // // // D.Ln("no longer listening to", w.FirstSender.Load()) + // // // w.FirstSender.Store("") + // return + // }, +} + +func (w *Worker) HashReport() float64 { + W.Ln("generating hash report") + w.hashSampleBuf.Add(w.hashCount.Load()) + av := ewma.NewMovingAverage() + var i int + var prev uint64 + if e := w.hashSampleBuf.ForEach( + func(v uint64) (e error) { + if i < 1 { + prev = v + } else { + interval := v - prev + av.Add(float64(interval)) + prev = v + } + i++ + return nil + }, + ); E.Chk(e) { + } + average := av.Value() + W.Ln("hashrate average", average) + // panic("aaargh") + return average +} diff --git a/cmd/kopach/log.go b/cmd/kopach/log.go new file mode 100644 index 0000000..248c97c --- /dev/null +++ b/cmd/kopach/log.go @@ -0,0 +1,9 @@ +package kopach + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) diff --git a/cmd/kopach/worker/implementation.go b/cmd/kopach/worker/implementation.go new file mode 100644 index 0000000..38cbf21 --- /dev/null +++ b/cmd/kopach/worker/implementation.go @@ -0,0 +1,346 @@ +package worker + +import ( + "crypto/cipher" + "math/rand" + "net" + "os" + "sync" + "time" + + "github.com/p9c/p9/pkg/bits" + "github.com/p9c/p9/pkg/chainrpc/templates" + "github.com/p9c/p9/pkg/constant" + "github.com/p9c/p9/pkg/fork" + "github.com/p9c/p9/pkg/pipe" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainrpc/hashrate" + "github.com/p9c/p9/pkg/chainrpc/sol" + + "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/pkg/ring" + "github.com/p9c/p9/pkg/transport" +) + +const CountPerRound = 81 + +type Worker struct { + mx sync.Mutex + id string + pipeConn *pipe.StdConn + dispatchConn *transport.Channel + dispatchReady atomic.Bool + ciph cipher.AEAD + quit qu.C + templatesMessage *templates.Message + uuid atomic.Uint64 + roller *Counter + startNonce uint32 + startChan qu.C + stopChan qu.C + running atomic.Bool + hashCount atomic.Uint64 + hashSampleBuf *ring.BufferUint64 +} + +type Counter struct { + rpa int32 + C atomic.Int32 + Algos atomic.Value // []int32 + RoundsPerAlgo atomic.Int32 +} + +// NewCounter returns an initialized algorithm rolling counter that ensures each +// miner does equal amounts of every algorithm +func NewCounter(countPerRound int32) (c *Counter) { + // these will be populated when work arrives + var algos []int32 + // Start the counter at a random position + rand.Seed(time.Now().UnixNano()) + c = &Counter{} + c.C.Store(int32(rand.Intn(int(countPerRound)+1) + 1)) + c.Algos.Store(algos) + c.RoundsPerAlgo.Store(countPerRound) + c.rpa = countPerRound + return +} + +// GetAlgoVer returns the next algo version based on the current configuration +func (c *Counter) GetAlgoVer(height int32) (ver int32) { + // the formula below rolls through versions with blocks roundsPerAlgo long for each algorithm by its index + algs := fork.GetAlgoVerSlice(height) + // D.Ln(algs) + if c.RoundsPerAlgo.Load() < 1 { + D.Ln("CountPerRound is", c.RoundsPerAlgo.Load(), len(algs)) + return 0 + } + if len(algs) > 0 { + ver = algs[c.C.Load()%int32(len(algs))] + // ver = algs[(c.C.Load()/ + // c.CountPerRound.Load())% + // int32(len(algs))] + c.C.Add(1) + } + return +} + +// +// func (w *Worker) hashReport() { +// w.hashSampleBuf.Add(w.hashCount.Load()) +// av := ewma.NewMovingAverage(15) +// var i int +// var prev uint64 +// if e := w.hashSampleBuf.ForEach( +// func(v uint64) (e error) { +// if i < 1 { +// prev = v +// } else { +// interval := v - prev +// av.Add(float64(interval)) +// prev = v +// } +// i++ +// return nil +// }, +// ); E.Chk(e) { +// } +// // I.Ln("kopach",w.hashSampleBuf.Cursor, w.hashSampleBuf.Buf) +// Tracef("average hashrate %.2f", av.Value()) +// } + +// NewWithConnAndSemaphore is exposed to enable use an actual network connection while retaining the same RPC API to +// allow a worker to be configured to run on a bare metal system with a different launcher main +func NewWithConnAndSemaphore(id string, conn *pipe.StdConn, quit qu.C, uuid uint64) *Worker { + T.Ln("creating new worker") + // msgBlock := wire.WireBlock{Header: wire.BlockHeader{}} + w := &Worker{ + id: id, + pipeConn: conn, + quit: quit, + roller: NewCounter(CountPerRound), + startChan: qu.T(), + stopChan: qu.T(), + hashSampleBuf: ring.NewBufferUint64(1000), + } + w.uuid.Store(uuid) + w.dispatchReady.Store(false) + // with this we can report cumulative hash counts as well as using it to distribute algorithms evenly + w.startNonce = uint32(w.roller.C.Load()) + interrupt.AddHandler( + func() { + D.Ln("worker", id, "quitting") + w.stopChan <- struct{}{} + // _ = w.pipeConn.Close() + w.dispatchReady.Store(false) + }, + ) + go worker(w) + return w +} + +func worker(w *Worker) { + D.Ln("main work loop starting") + // sampleTicker := time.NewTicker(time.Second) + var nonce uint32 +out: + for { + // Pause state + T.Ln("worker pausing") + pausing: + for { + select { + // case <-sampleTicker.C: + // // w.hashReport() + // break + case <-w.stopChan.Wait(): + D.Ln("received pause signal while paused") + // drain stop channel in pause + break + case <-w.startChan.Wait(): + D.Ln("received start signal") + break pausing + case <-w.quit.Wait(): + D.Ln("quitting") + break out + } + } + // Run state + T.Ln("worker running") + running: + for { + select { + // case <-sampleTicker.C: + // // w.hashReport() + // break + case <-w.startChan.Wait(): + D.Ln("received start signal while running") + // drain start channel in run mode + break + case <-w.stopChan.Wait(): + D.Ln("received pause signal while running") + break running + case <-w.quit.Wait(): + D.Ln("worker stopping while running") + break out + default: + if w.templatesMessage == nil || !w.dispatchReady.Load() { + D.Ln("not ready to work") + } else { + // I.Ln("starting mining round") + newHeight := w.templatesMessage.Height + vers := w.roller.GetAlgoVer(newHeight) + nonce++ + tn := time.Now().Round(time.Second) + if tn.After(w.templatesMessage.Timestamp.Round(time.Second)) { + w.templatesMessage.Timestamp = tn + } + if w.roller.C.Load()%w.roller.RoundsPerAlgo.Load() == 0 { + D.Ln("switching algorithms", w.roller.C.Load()) + // send out broadcast containing worker nonce and algorithm and count of blocks + w.hashCount.Store(w.hashCount.Load() + uint64(w.roller.RoundsPerAlgo.Load())) + hashReport := hashrate.Get(w.roller.RoundsPerAlgo.Load(), vers, newHeight, w.id) + e := w.dispatchConn.SendMany( + hashrate.Magic, + transport.GetShards(hashReport), + ) + if e != nil { + } + // reseed the nonce + rand.Seed(time.Now().UnixNano()) + nonce = rand.Uint32() + select { + case <-w.quit.Wait(): + D.Ln("breaking out of work loop") + break out + case <-w.stopChan.Wait(): + D.Ln("received pause signal while running") + break running + default: + } + } + blockHeader := w.templatesMessage.GenBlockHeader(vers) + blockHeader.Nonce = nonce + // D.S(w.templatesMessage) + // D.S(blockHeader) + hash := blockHeader.BlockHashWithAlgos(newHeight) + bigHash := blockchain.HashToBig(&hash) + if bigHash.Cmp(bits.CompactToBig(blockHeader.Bits)) <= 0 { + D.Ln("found solution", newHeight, w.templatesMessage.Nonce, w.templatesMessage.UUID) + srs := sol.Encode(w.templatesMessage.Nonce, w.templatesMessage.UUID, blockHeader) + e := w.dispatchConn.SendMany( + sol.Magic, + transport.GetShards(srs), + ) + if e != nil { + } + D.Ln("sent solution") + w.templatesMessage = nil + select { + case <-w.quit.Wait(): + D.Ln("breaking out of work loop") + break out + default: + } + break running + } + // D.Ln("completed mining round") + } + } + } + } + D.Ln("worker finished") + interrupt.Request() +} + +// New initialises the state for a worker, loading the work function handler that runs a round of processing between +// checking quit signal and work semaphore +func New(id string, quit qu.C, uuid uint64) (w *Worker, conn net.Conn) { + // log.L.SetLevel("trace", true) + sc := pipe.New(os.Stdin, os.Stdout, quit) + + return NewWithConnAndSemaphore(id, sc, quit, uuid), sc +} + +// NewJob is a delivery of a new job for the worker, this makes the miner start +// mining from pause or pause, prepare the work and restart +func (w *Worker) NewJob(j *templates.Message, reply *bool) (e error) { + // T.Ln("received new job") + if !w.dispatchReady.Load() { + D.Ln("dispatch not ready") + *reply = true + return + } + if w.templatesMessage != nil { + if j.PrevBlock == w.templatesMessage.PrevBlock { + // T.Ln("not a new job") + *reply = true + return + } + } + // D.S(j) + *reply = true + D.Ln("halting current work") + w.stopChan <- struct{}{} + D.Ln("halt signal sent") + // load the job into the template + if w.templatesMessage == nil { + w.templatesMessage = j + } else { + *w.templatesMessage = *j + } + D.Ln("switching to new job") + w.startChan <- struct{}{} + D.Ln("start signal sent") + return +} + +// Pause signals the worker to stop working, releases its semaphore and the worker is then idle +func (w *Worker) Pause(_ int, reply *bool) (e error) { + T.Ln("pausing from IPC") + w.running.Store(false) + w.stopChan <- struct{}{} + *reply = true + return +} + +// Stop signals the worker to quit +func (w *Worker) Stop(_ int, reply *bool) (e error) { + D.Ln("stopping from IPC") + w.stopChan <- struct{}{} + defer w.quit.Q() + *reply = true + // time.Sleep(time.Second * 3) + // os.Exit(0) + return +} + +// SendPass gives the encryption key configured in the kopach controller ( pod) configuration to allow workers to +// dispatch their solutions +func (w *Worker) SendPass(pass []byte, reply *bool) (e error) { + D.Ln("receiving dispatch password", pass) + rand.Seed(time.Now().UnixNano()) + // sp := fmt.Sprint(rand.Intn(32767) + 1025) + // rp := fmt.Sprint(rand.Intn(32767) + 1025) + var conn *transport.Channel + conn, e = transport.NewBroadcastChannel( + "kopachworker", + w, + pass, + transport.DefaultPort, + constant.MaxDatagramSize, + transport.Handlers{}, + w.quit, + ) + if e != nil { + } + w.dispatchConn = conn + w.dispatchReady.Store(true) + *reply = true + return +} diff --git a/cmd/kopach/worker/log.go b/cmd/kopach/worker/log.go new file mode 100644 index 0000000..d35f764 --- /dev/null +++ b/cmd/kopach/worker/log.go @@ -0,0 +1,43 @@ +package worker + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/misc/glom/LICENSE b/cmd/misc/glom/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/cmd/misc/glom/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/cmd/misc/glom/README.md b/cmd/misc/glom/README.md new file mode 100644 index 0000000..395af1e --- /dev/null +++ b/cmd/misc/glom/README.md @@ -0,0 +1,15 @@ +# glom + +Glom is a command line shell, code and script editor and application platform in one. + +It keeps a branching history of commands entered, which are either directly Go expressions or API calls in a arbitrary textual form similar to command line options, and can be executed immediately, or deferred until a testable unit is constructed. + +The branching logs of commands entered can be grouped into functions, and functions grouped into packages, and then published to distributed content addressable repositories. + +Glom stores the content reference to called functions as at the last time they were compiled, ending versioning hell. + +Glom allows you to link any and all content to these packages, one can keep a dev journal, write guides, attach video, photographs and so on, right next to the code using document editors that from an early point will be created. + +Last, but not least, it hosts the content you want to share with the world via IPFS or similar peer to peer protocol, the whole relevant history, or just the current state, the code is fingerprinted by its content, signed by its authors. + +With it's friendly navigation, even non programmers will be able to write simple linear scripts composed of variables and API calls, and with easy auditing the security of apps created and distributed this way, every last statement can be accounted for and malicious code and its authors pruned from the tree. diff --git a/cmd/misc/glom/glom.go b/cmd/misc/glom/glom.go new file mode 100644 index 0000000..e7d0513 --- /dev/null +++ b/cmd/misc/glom/glom.go @@ -0,0 +1,40 @@ +package main + +import ( + l "github.com/p9c/p9/pkg/gel/gio/layout" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/cmd/misc/glom/pkg/pathtree" + "github.com/p9c/p9/pkg/gel" + "github.com/p9c/p9/pkg/interrupt" +) + +type State struct { + *gel.Window +} + +func NewState(quit qu.C) *State { + return &State{ + Window: gel.NewWindowP9(quit), + } +} + +func main() { + quit := qu.T() + state := NewState(quit) + var e error + folderView := pathtree.New(state.Window) + state.Window.SetDarkTheme(folderView.Dark.True()) + if e = state.Window. + Size(48, 32). + Title("glom, the visual code editor"). + Open(). + Run(func(gtx l.Context) l.Dimensions { return folderView.Fn(gtx) }, func() { + interrupt.Request() + quit.Q() + }, quit, + ); E.Chk(e) { + + } +} diff --git a/cmd/misc/glom/log.go b/cmd/misc/glom/log.go new file mode 100644 index 0000000..da27aa5 --- /dev/null +++ b/cmd/misc/glom/log.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/misc/glom/pkg/pathtree/log.go b/cmd/misc/glom/pkg/pathtree/log.go new file mode 100644 index 0000000..59ed7be --- /dev/null +++ b/cmd/misc/glom/pkg/pathtree/log.go @@ -0,0 +1,43 @@ +package pathtree + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/misc/glom/pkg/pathtree/pathtree.go b/cmd/misc/glom/pkg/pathtree/pathtree.go new file mode 100644 index 0000000..5f3b5d8 --- /dev/null +++ b/cmd/misc/glom/pkg/pathtree/pathtree.go @@ -0,0 +1,271 @@ +package pathtree + +import ( + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/text" + uberatomic "go.uber.org/atomic" + "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/opts/binary" + "github.com/p9c/p9/pkg/opts/meta" + + "github.com/p9c/p9/pkg/gel" +) + +type Widget struct { + *gel.Window + *gel.App + activePage *uberatomic.String + sidebarButtons []*gel.Clickable + statusBarButtons []*gel.Clickable + buttonBarButtons []*gel.Clickable + Size *uberatomic.Int32 +} + +func New(w *gel.Window) (wg *Widget) { + activePage := uberatomic.NewString("home") + w.Dark = binary.New(meta.Data{}, false, func(b bool) error { return nil }) + w.Colors.SetDarkTheme(false) + // I.S(w.Colors) + app := w.App(w.Width, activePage, 48) + wg = &Widget{ + Window: w, + App: app, + activePage: uberatomic.NewString("home"), + Size: w.Width, + } + wg.GetButtons() + app.Pages( + map[string]l.Widget{ + "home": wg.Page( + "home", gel.Widgets{ + // p9.WidgetSize{Widget: p9.EmptyMaxHeight()}, + gel.WidgetSize{ + Widget: wg.Flex().Flexed(1, wg.H3("glom").Fn).Fn, + }, + }, + ), + }, + ) + app.SideBar( + []l.Widget{ + // wg.SideBarButton(" ", " ", 11), + wg.SideBarButton("home", "home", 0), + }, + ) + app.ButtonBar( + []l.Widget{ + wg.PageTopBarButton( + "help", 0, &icons.ActionHelp, func(name string) { + }, app, "", + ), + wg.PageTopBarButton( + "home", 1, &icons.ActionLockOpen, func(name string) { + wg.App.ActivePage(name) + }, app, "green", + ), + // wg.Flex().Rigid(wg.Inset(0.5, gel.EmptySpace(0, 0)).Fn).Fn, + // wg.PageTopBarButton( + // "quit", 3, &icons.ActionExitToApp, func(name string) { + // wg.MainApp.ActivePage(name) + // }, a, "", + // ), + }, + ) + app.StatusBar( + []l.Widget{ + wg.StatusBarButton( + "log", 0, &icons.ActionList, func(name string) { + D.Ln("click on button", name) + }, app, + ), + }, + []l.Widget{ + wg.StatusBarButton( + "settings", 1, &icons.ActionSettings, func(name string) { + D.Ln("click on button", name) + }, app, + ), + }, + ) + return +} + +func (w *Widget) Fn(gtx l.Context) l.Dimensions { + return w.App.Fn()(gtx) +} + +func (w *Widget) Page(title string, widget gel.Widgets) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return w.VFlex(). + // SpaceEvenly(). + Rigid( + w.Responsive( + w.Size.Load(), gel.Widgets{ + // p9.WidgetSize{ + // Widget: a.ButtonInset(0.25, a.H5(title).Color(wg.App.BodyColorGet()).Fn).Fn, + // }, + gel.WidgetSize{ + // Size: 800, + Widget: gel.EmptySpace(0, 0), + // a.ButtonInset(0.25, a.Caption(title).Color(wg.BodyColorGet()).Fn).Fn, + }, + }, + ).Fn, + ). + Flexed( + 1, + w.Inset( + 0.25, + w.Responsive(w.Size.Load(), widget).Fn, + ).Fn, + ).Fn(gtx) + } +} + +func (wg *Widget) GetButtons() { + wg.sidebarButtons = make([]*gel.Clickable, 2) + // wg.walletLocked.Store(true) + for i := range wg.sidebarButtons { + wg.sidebarButtons[i] = wg.Clickable() + } + wg.buttonBarButtons = make([]*gel.Clickable, 2) + for i := range wg.buttonBarButtons { + wg.buttonBarButtons[i] = wg.Clickable() + } + wg.statusBarButtons = make([]*gel.Clickable, 2) + for i := range wg.statusBarButtons { + wg.statusBarButtons[i] = wg.Clickable() + } +} + +func (w *Widget) SideBarButton(title, page string, index int) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + var scale float32 + scale = gel.Scales["H6"] + var color string + background := "Transparent" + color = "DocText" + var ins float32 = 0.5 + // var hl = false + if w.App.ActivePageGet() == page || w.App.PreRendering { + background = "PanelBg" + scale = gel.Scales["H6"] + color = "DocText" + // ins = 0.5 + // hl = true + } + if title == " " { + scale = gel.Scales["H6"] / 2 + } + max := int(w.App.SideBarSize.V) + if max > 0 { + gtx.Constraints.Max.X = max + gtx.Constraints.Min.X = max + } + // D.Ln("sideMAXXXXXX!!", max) + return w.Direction().E().Embed( + w.ButtonLayout(w.sidebarButtons[index]). + CornerRadius(scale).Corners(0). + Background(background). + Embed( + w.Inset( + ins, + func(gtx l.Context) l.Dimensions { + return w.H5(title). + Color(color). + Alignment(text.End). + Fn(gtx) + }, + ).Fn, + ). + SetClick( + func() { + if w.App.MenuOpen { + w.App.MenuOpen = false + } + w.App.ActivePage(page) + }, + ). + Fn, + ). + Fn(gtx) + } +} + +func (w *Widget) PageTopBarButton( + name string, index int, ico *[]byte, onClick func(string), app *gel.App, + highlightColor string, +) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + background := "Transparent" + // background := node.TitleBarBackgroundGet() + color := app.MenuColorGet() + + if app.ActivePageGet() == name { + color = "PanelText" + // background = "scrim" + background = "PanelBg" + } + // if name == "home" { + // background = "scrim" + // } + if highlightColor != "" { + color = highlightColor + } + ic := w.Icon(). + Scale(gel.Scales["H5"]). + Color(color). + Src(ico). + Fn + return w.Flex().Rigid( + // wg.ButtonInset(0.25, + w.ButtonLayout(w.buttonBarButtons[index]). + CornerRadius(0). + Embed( + w.Inset( + 0.375, + ic, + ).Fn, + ). + Background(background). + SetClick(func() { onClick(name) }). + Fn, + // ).Fn, + ).Fn(gtx) + } +} + +func (w *Widget) StatusBarButton( + name string, + index int, + ico *[]byte, + onClick func(string), + app *gel.App, +) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + background := app.StatusBarBackgroundGet() + color := app.StatusBarColorGet() + if app.ActivePageGet() == name { + // background, color = color, background + background = "PanelBg" + // color = "Danger" + } + ic := w.Icon(). + Scale(gel.Scales["H5"]). + Color(color). + Src(ico). + Fn + return w.Flex(). + Rigid( + w.ButtonLayout(w.statusBarButtons[index]). + CornerRadius(0). + Embed( + w.Inset(0.25, ic).Fn, + ). + Background(background). + SetClick(func() { onClick(name) }). + Fn, + ).Fn(gtx) + } +} diff --git a/cmd/misc/jrnl/main.go b/cmd/misc/jrnl/main.go new file mode 100644 index 0000000..83598f9 --- /dev/null +++ b/cmd/misc/jrnl/main.go @@ -0,0 +1,50 @@ +// This is just a convenient cli command to automatically generate a new file +// for a journal entry with names based on unix timestamps +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "time" +) + +type jrnlCfg struct { + Root string +} + +func printErrorAndDie(stuff ...interface{}) { + fmt.Fprintln(os.Stderr, stuff) + os.Exit(1) +} + +func main() { + var home string + var e error + if home, e = os.UserHomeDir(); e != nil { + os.Exit(1) + } + var configFile []byte + if configFile, e = ioutil.ReadFile( + filepath.Join(home, ".jrnl")); e != nil { + printErrorAndDie(e, "~/.jrnl configuration file not found") + } + var cfg jrnlCfg + if e = json.Unmarshal(configFile, &cfg); e != nil { + printErrorAndDie(e, "~/.jrnl config file did not unmarshal") + } + filename := filepath.Join(cfg.Root, fmt.Sprintf("jrnl%d.txt", + time.Now().Unix())) + if e = ioutil.WriteFile(filename, + []byte(time.Now().Format(time.RFC1123Z)+"\n\n"), + 0600, + ); e != nil { + printErrorAndDie(e, + "unable to create file (is your keybase filesystem mounted?") + os.Exit(1) + } + exec.Command("gedit", filename).Run() +} diff --git a/cmd/misc/swd/main.go b/cmd/misc/swd/main.go new file mode 100644 index 0000000..0935e33 --- /dev/null +++ b/cmd/misc/swd/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +func main() { + cwd, _ := os.Getwd() + home, _ := os.UserHomeDir() + ioutil.WriteFile(filepath.Join(home, ".cwd"), []byte(cwd), 0600) +} diff --git a/cmd/node/CHANGES b/cmd/node/CHANGES new file mode 100755 index 0000000..f9c8cbd --- /dev/null +++ b/cmd/node/CHANGES @@ -0,0 +1,941 @@ +============================================================================ +User visible changes for pod + A full-node bitcoin implementation written in Go +============================================================================ +Changes in 0.12.0 (Fri Nov 20 2015) + - Protocol and network related changes: + - Add a new checkpoint at block height 382320 (#555) + - Implement BIP0065 which includes support for version 4 blocks, a new + consensus opcode (OP_CHECKLOCKTIMEVERIFY) that enforces transaction + lock times, and a double-threshold switchover mechanism (#535, #459, + #455) + - Implement BIP0111 which provides a new bloom filter service flag and + hence provides support for protocol version 70011 (#499) + - Add a new parameter --nopeerbloomfilters to allow disabling bloom + filter support (#499) + - Reject non-canonically encoded variable length integers (#507) + - Add mainnet peer discovery DNS seed (seed.bitcoin.jonasschnelli.ch) + (#496) + - Correct reconnect handling for persistent peers (#463, #464) + - Ignore requests for block headers if not fully synced (#444) + - Add CLI support for specifying the zone id on IPv6 addresses (#538) + - Fix a couple of issues where the initial block sync could stall (#518, + #229, #486) + - Fix an issue which prevented the --onion option from working as + intended (#446) + - Transaction relay (memory pool) changes: + - Require transactions to only include signatures encoded with the + canonical 'low-s' encoding (#512) + - Add a new parameter --minrelaytxfee to allow the minimum transaction + fee in DUO/kB to be overridden (#520) + - Retain memory pool transactions when they redeem another one that is + removed when a block is accepted (#539) + - Do not send reject messages for a transaction if it is valid but + causes an orphan transaction which depends on it to be determined + as invalid (#546) + - Refrain from attempting to add orphans to the memory pool multiple + times when the transaction they redeem is added (#551) + - Modify minimum transaction fee calculations to scale based on bytes + instead of full kilobyte boundaries (#521, #537) + - Implement signature cache: + - Provides a limited memory cache of validated signatures which is a + huge optimization when verifying blocks for transactions that are + already in the memory pool (#506) + - Add a new parameter '--sigcachemaxsize' which allows the size of the + new cache to be manually changed if desired (#506) + - Mining support changes: + - Notify getblocktemplate long polling clients when a block is pushed + via submitblock (#488) + - Speed up getblocktemplate by making use of the new signature cache + (#506) + - RPC changes: + - Implement getmempoolinfo command (#453) + - Implement getblockheader command (#461) + - Modify createrawtransaction command to accept a new optional parameter + 'locktime' (#529) + - Modify listunspent result to include the 'spendable' field (#440) + - Modify getinfo command to include 'errors' field (#511) + - Add timestamps to blockconnected and blockdisconnected notifications + (#450) + - Several modifications to searchrawtranscations command: + - Accept a new optional parameter 'vinextra' which causes the results + to include information about the outputs referenced by a transaction's + inputs (#485, #487) + - Skip entries in the mempool too (#495) + - Accept a new optional parameter 'reverse' to return the results in + reverse order (most recent to oldest) (#497) + - Accept a new optional parameter 'filteraddrs' which causes the + results to only include inputs and outputs which involve the + provided addresses (#516) + - Change the notification order to notify clients about mined + transactions (recvtx, redeemingtx) before the blockconnected + notification (#449) + - Update verifymessage RPC to use the standard algorithm so it is + compatible with other implementations (#515) + - Improve ping statistics by pinging on an interval (#517) + - Websocket changes: + - Implement session command which returns a per-session unique id (#500, + #503) + - podctl utility changes: + - Add getmempoolinfo command (#453) + - Add getblockheader command (#461) + - Add getwalletinfo command (#471) + - Notable developer-related package changes: + - Introduce a new peer package which acts a common base for creating and + concurrently managing bitcoin network peers (#445) + - Various cleanup of the new peer package (#528, #531, #524, #534, + #549) + - Blocks heights now consistently use int32 everywhere (#481) + - The BlockHeader type in the wire package now provides the BtcDecode + and BtcEncode methods (#467) + - Update wire package to recognize BIP0064 (getutxo) service bit (#489) + - Export LockTimeThreshold constant from txscript package (#454) + - Export MaxDataCarrierSize constant from txscript package (#466) + - Provide new IsUnspendable function from the txscript package (#478) + - Export variable length string functions from the wire package (#514) + - Export DNS Seeds for each network from the chaincfg package (#544) + - Preliminary work towards separating the memory pool into a separate + package (#525, #548) + - Misc changes: + - Various documentation updates (#442, #462, #465, #460, #470, #473, + #505, #530, #545) + - Add installation instructions for gentoo (#542) + - Ensure an error is shown if OS limits can't be set at startup (#498) + - Tighten the standardness checks for multisig scripts (#526) + - Test coverage improvement (#468, #494, #527, #543, #550) + - Several optimizations (#457, #474, #475, #476, #508, #509) + - Minor code cleanup and refactoring (#472, #479, #482, #519, #540) + - Contributors (alphabetical order): + - Ben Echols + - Bruno Clermont + - danda + - Daniel Krawisz + - Dario Nieuwenhuis + - Dave Collins + - David Hill + - Javed Khan + - Jonathan Gillham + - Joseph Becher + - Josh Rickmar + - Justus Ranvier + - Mawuli Adzoe + - Olaoluwa Osuntokun + - Rune T. Aune +Changes in 0.11.1 (Wed May 27 2015) + - Protocol and network related changes: + - Use correct sub-command in reject message for rejected transactions + (#436, #437) + - Add a new parameter --torisolation which forces new circuits for each + connection when using tor (#430) + - Transaction relay (memory pool) changes: + - Reduce the default number max number of allowed orphan transactions + to 1000 (#419) + - Add a new parameter --maxorphantx which allows the maximum number of + orphan transactions stored in the mempool to be specified (#419) + - RPC changes: + - Modify listtransactions result to include the 'involveswatchonly' and + 'vout' fields (#427) + - Update getrawtransaction result to omit the 'confirmations' field + when it is 0 (#420, #422) + - Update signrawtransaction result to include errors (#423) + - podctl utility changes: + - Add gettxoutproof command (#428) + - Add verifytxoutproof command (#428) + - Notable developer-related package changes: + - The btcec package now provides the ability to perform ECDH + encryption and decryption (#375) + - The block and header validation in the blockchain package has been + split to help pave the way toward concurrent downloads (#386) + - Misc changes: + - Minor peer optimization (#433) + - Contributors (alphabetical order): + - Dave Collins + - David Hill + - Federico Bond + - Ishbir Singh + - Josh Rickmar +Changes in 0.11.0 (Wed May 06 2015) + - Protocol and network related changes: + - **IMPORTANT: Update is required due to the following point** + - Correct a few corner cases in script handling which could result in + forking from the network on non-standard transactions (#425) + - Add a new checkpoint at block height 352940 (#418) + - Optimized script execution (#395, #400, #404, #409) + - Fix a case that could lead stalled syncs (#138, #296) + - Network address manager changes: + - Implement eclipse attack countermeasures as proposed in + http://cs-people.bu.edu/heilman/eclipse (#370, #373) + - Optional address indexing changes: + - Fix an issue where a reorg could cause an orderly shutdown when the + address index is active (#340, #357) + - Transaction relay (memory pool) changes: + - Increase maximum allowed space for nulldata transactions to 80 bytes + (#331) + - Implement support for the following rules specified by BIP0062: + - The S value in ECDSA signature must be at most half the curve order + (rule 5) (#349) + - Script execution must result in a single non-zero value on the stack + (rule 6) (#347) + - NOTE: All 7 rules of BIP0062 are now implemented + - Use network adjusted time in finalized transaction checks to improve + consistency across nodes (#332) + - Process orphan transactions on acceptance of new transactions (#345) + - RPC changes: + - Add support for a limited RPC user which is not allowed admin level + operations on the server (#363) + - Implement node command for more unified control over connected peers + (#79, #341) + - Implement generate command for regtest/simnet to support + deterministically mining a specified number of blocks (#362, #407) + - Update searchrawtransactions to return the matching transactions in + order (#354) + - Correct an issue with searchrawtransactions where it could return + duplicates (#346, #354) + - Increase precision of 'difficulty' field in getblock result to 8 + (#414, #415) + - Omit 'nextblockhash' field from getblock result when it is empty + (#416, #417) + - Add 'id' and 'timeoffset' fields to getpeerinfo result (#335) + - Websocket changes: + - Implement new commands stopnotifyspent, stopnotifyreceived, + stopnotifyblocks, and stopnotifynewtransactions to allow clients to + cancel notification registrations (#122, #342) + - podctl utility changes: + - A single dash can now be used as an argument to cause that argument to + be read from stdin (#348) + - Add generate command + - Notable developer-related package changes: + - The new version 2 btcjson package has now replaced the deprecated + version 1 package (#368) + - The btcec package now performs all signing using RFC6979 deterministic + signatures (#358, #360) + - The txscript package has been significantly cleaned up and had a few + API changes (#387, #388, #389, #390, #391, #392, #393, #395, #396, + #400, #403, #404, #405, #406, #408, #409, #410, #412) + - A new PkScriptLocs function has been added to the wire package MsgTx + type which provides callers that deal with scripts optimization + opportunities (#343) + - Misc changes: + - Minor wire hashing optimizations (#366, #367) + - Other minor internal optimizations + - Contributors (alphabetical order): + - Alex Akselrod + - Arne Brutschy + - Chris Jepson + - Daniel Krawisz + - Dave Collins + - David Hill + - Jimmy Song + - Jonas Nick + - Josh Rickmar + - Olaoluwa Osuntokun + - Oleg Andreev +Changes in 0.10.0 (Sun Mar 01 2015) + - Protocol and network related changes: + - Add a new checkpoint at block height 343185 + - Implement BIP066 which includes support for version 3 blocks, a new + consensus rule which prevents non-DER encoded signatures, and a + double-threshold switchover mechanism + - Rather than announcing all known addresses on getaddr requests which + can possibly result in multiple messages, randomize the results and + limit them to the max allowed by a single message (1000 addresses) + - Add more reserved IP spaces to the address manager + - Transaction relay (memory pool) changes: + - Make transactions which contain reserved opcodes nonstandard + - No longer accept or relay free and low-fee transactions that have + insufficient priority to be mined in the next block + - Implement support for the following rules specified by BIP0062: + - ECDSA signature must use strict DER encoding (rule 1) + - The signature script must only contain push operations (rule 2) + - All push operations must use the smallest possible encoding (rule 3) + - All stack values interpreted as a number must be encoding using the + shortest possible form (rule 4) + - NOTE: Rule 1 was already enforced, however the entire script now + evaluates to false rather than only the signature verification as + required by BIP0062 + - Allow transactions with nulldata transaction outputs to be treated as + standard + - Mining support changes: + - Modify the getblocktemplate RPC to generate and return block templates + for version 3 blocks which are compatible with BIP0066 + - Allow getblocktemplate to serve blocks when the current time is + less than the minimum allowed time for a generated block template + (https://github.com/p9c/p9/issues/209) + - Crypto changes: + - Optimize scalar multiplication by the base point by using a + pre-computed table which results in approximately a 35% speedup + (https://github.com/btcsuite/btcec/issues/2) + - Optimize general scalar multiplication by using the secp256k1 + endomorphism which results in approximately a 17-20% speedup + (https://github.com/btcsuite/btcec/issues/1) + - Optimize general scalar multiplication by using non-adjacent form + which results in approximately an additional 8% speedup + (https://github.com/btcsuite/btcec/issues/3) + - Implement optional address indexing: + - Add a new parameter --addrindex which will enable the creation of an + address index which can be queried to determine all transactions which + involve a given address + (https://github.com/p9c/p9/issues/190) + - Add a new logging subsystem for address index related operations + - Support new searchrawtransactions RPC + (https://github.com/p9c/p9/issues/185) + - RPC changes: + - Require TLS version 1.2 as the minimum version for all TLS connections + - Provide support for disabling TLS when only listening on localhost + (https://github.com/p9c/p9/pull/192) + - Modify help output for all commands to provide much more consistent + and detailed information + - Correct case in getrawtransaction which would refuse to serve certain + transactions with invalid scripts + (https://github.com/p9c/p9/issues/210) + - Correct error handling in the getrawtransaction RPC which could lead + to a crash in rare cases + (https://github.com/p9c/p9/issues/196) + - Update getinfo RPC to include the appropriate 'timeoffset' calculated + from the median network time + - Modify listreceivedbyaddress result type to include txids field so it + is compatible + - Add 'iswatchonly' field to validateaddress result + - Add 'startingpriority' and 'currentpriority' fields to getrawmempool + (https://github.com/p9c/p9/issues/178) + - Don't omit the 'confirmations' field from getrawtransaction when it is + zero + - Websocket changes: + - Modify the behavior of the rescan command to automatically register + for notifications about transactions paying to rescanned addresses + or spending outputs from the final rescan utxo set when the rescan + is through the best block in the chain + - podctl utility changes: + - Make the list of commands available via the -l option rather than + dumping the entire list on usage errors + - Alphabetize and categorize the list of commands by chain and wallet + - Make the help option only show the help options instead of also + dumping all of the commands + - Make the usage syntax much more consistent and correct a few cases of + misnamed fields + (https://github.com/p9c/p9/issues/305) + - Improve usage errors to show the specific parameter number, reason, + and error code + - Only show the usage for specific command is shown when a valid command + is provided with invalid parameters + - Add support for a SOCK5 proxy + - Modify output for integer fields (such as timestamps) to display + normally instead in scientific notation + - Add invalidateblock command + - Add reconsiderblock command + - Add createnewaccount command + - Add renameaccount command + - Add searchrawtransactions command + - Add importaddress command + - Add importpubkey command + - showblock utility changes: + - Remove utility in favor of the RPC getblock method + - Notable developer-related package changes: + - Many of the core packages have been relocated into the pod repository + (https://github.com/p9c/p9/issues/214) + - A new version of the btcjson package that has been completely + redesigned from the ground up based based upon how the project has + evolved and lessons learned while using it since it was first written + is now available in the btcjson/v2/btcjson directory + - This will ultimately replace the current version so anyone making + use of this package will need to update their code accordingly + - The btcec package now provides better facilities for working directly + with its public and private keys without having to mix elements from + the ecdsa package + - Update the script builder to ensure all rules specified by BIP0062 are + adhered to when creating scripts + - The blockchain package now provides a MedianTimeSource interface and + concrete implementation for providing time samples from remote peers + and using that data to calculate an offset against the local time + - Misc changes: + - Fix a slow memory leak due to tickers not being stopped + (https://github.com/p9c/p9/issues/189) + - Fix an issue where a mix of orphans and SPV clients could trigger a + condition where peers would no longer be served + (https://github.com/p9c/p9/issues/231) + - The RPC username and password can now contain symbols which previously + conflicted with special symbols used in URLs + - Improve handling of obtaining random nonces to prevent cases where it + could error when not enough entropy was available + - Improve handling of home directory creation errors such as in the case + of unmounted symlinks (https://github.com/p9c/p9/issues/193) + - Improve the error reporting for rejected transactions to include the + inputs which are missing and/or being double spent + - Update sample config file with new options and correct a comment + regarding the fact the RPC server only listens on localhost by default + (https://github.com/p9c/p9/issues/218) + - Update the continuous integration builds to run several tools which + help keep code quality high + - Significant amount of internal code cleanup and improvements + - Other minor internal optimizations + - Code Contributors (alphabetical order): + - Beldur + - Ben Holden-Crowther + - Dave Collins + - David Evans + - David Hill + - Guilherme Salgado + - Javed Khan + - Jimmy Song + - John C. Vernaleo + - Jonathan Gillham + - Josh Rickmar + - Michael Ford + - Michail Kargakis + - kac + - Olaoluwa Osuntokun +Changes in 0.9.0 (Sat Sep 20 2014) + - Protocol and network related changes: + - Add a new checkpoint at block height 319400 + - Add support for BIP0037 bloom filters + (https://github.com/conformal/pod/issues/132) + - Implement BIP0061 reject handling and hence support for protocol + version 70002 (https://github.com/conformal/pod/issues/133) + - Add testnet DNS seeds for peer discovery (testnet-seed.alexykot.me + and testnet-seed.bitcoin.schildbach.de) + - Add mainnet DNS seed for peer discovery (seeds.bitcoin.open-nodes.org) + - Make multisig transactions with non-null dummy data nonstandard + (https://github.com/conformal/pod/issues/131) + - Make transactions with an excessive number of signature operations + nonstandard + - Perform initial DNS lookups concurrently which allows connections + more quickly + - Improve the address manager to significantly reduce memory usage and + add tests + - Remove orphan transactions when they appear in a mined block + (https://github.com/conformal/pod/issues/166) + - Apply incremental back off on connection retries for persistent peers + that give invalid replies to mirror the logic used for failed + connections (https://github.com/conformal/pod/issues/103) + - Correct rate-limiting of free and low-fee transactions + - Mining support changes: + - Implement getblocktemplate RPC with the following support: + (https://github.com/conformal/pod/issues/124) + - BIP0022 Non-Optional Sections + - BIP0022 Long Polling + - BIP0023 Basic Pool Extensions + - BIP0023 Mutation coinbase/append + - BIP0023 Mutations time, time/increment, and time/decrement + - BIP0023 Mutation transactions/add + - BIP0023 Mutations prevblock, coinbase, and generation + - BIP0023 Block Proposals + - Implement built-in concurrent CPU miner + (https://github.com/conformal/pod/issues/137) + NOTE: CPU mining on mainnet is pointless. This has been provided + for testing purposes such as for the new simulation test network + - Add --generate flag to enable CPU mining + - Deprecate the --getworkkey flag in favor of --miningaddr which + specifies which addresses generated blocks will choose from to pay + the subsidy to + - RPC changes: + - Implement gettxout command + (https://github.com/conformal/pod/issues/141) + - Implement validateaddress command + - Implement verifymessage command + - Mark getunconfirmedbalance RPC as wallet-only + - Mark getwalletinfo RPC as wallet-only + - Update getgenerate, setgenerate, gethashespersec, and getmininginfo + to return the appropriate information about new CPU mining status + - Modify getpeerinfo pingtime and pingwait field types to float64 so + they are compatible + - Improve disconnect handling for normal HTTP clients + - Make error code returns for invalid hex more consistent + - Websocket changes: + - Switch to a new more efficient websocket package + (https://github.com/conformal/pod/issues/134) + - Add rescanfinished notification + - Modify the rescanprogress notification to include block hash as well + as height (https://github.com/conformal/pod/issues/151) + - podctl utility changes: + - Accept --simnet flag which automatically selects the appropriate port + and TLS certificates needed to communicate with pod and btcwallet on + the simulation test network + - Fix createrawtransaction command to send amounts denominated in DUO + - Add estimatefee command + - Add estimatepriority command + - Add getmininginfo command + - Add getnetworkinfo command + - Add gettxout command + - Add lockunspent command + - Add signrawtransaction command + - addblock utility changes: + - Accept --simnet flag which automatically selects the appropriate port + and TLS certificates needed to communicate with pod and btcwallet on + the simulation test network + - Notable developer-related package changes: + - Provide a new bloom package in btcutil which allows creating and + working with BIP0037 bloom filters + - Provide a new hdkeychain package in btcutil which allows working with + BIP0032 hierarchical deterministic key chains + - Introduce a new btcnet package which houses network parameters + - Provide new simnet network (--simnet) which is useful for private + simulation testing + - Enforce low S values in serialized signatures as detailed in BIP0062 + - Return errors from all methods on the podb.Db interface + (https://github.com/conformal/podb/issues/5) + - Allow behavior flags to alter btcchain.ProcessBlock + (https://github.com/conformal/btcchain/issues/5) + - Provide a new SerializeSize API for blocks + (https://github.com/conformal/btcwire/issues/19) + - Several of the core packages now work with Google App Engine + - Misc changes: + - Correct an issue where the database could corrupt under certain + circumstances which would require a new chain download + - Slightly optimize deserialization + - Use the correct IP block for he.net + - Fix an issue where it was possible the block manager could hang on + shutdown + - Update sample config file so the comments are on a separate line + rather than the end of a line so they are not interpreted as settings + (https://github.com/conformal/pod/issues/135) + - Correct an issue where getdata requests were not being properly + throttled which could lead to larger than necessary memory usage + - Always show help when given the help flag even when the config file + contains invalid entries + - General code cleanup and minor optimizations +Changes in 0.8.0-beta (Sun May 25 2014) + - Pod is now Beta (https://github.com/conformal/pod/issues/130) + - Add a new checkpoint at block height 300255 + - Protocol and network related changes: + - Lower the minimum transaction relay fee to 1000 satoshi to match + recent reference client changes + (https://github.com/conformal/pod/issues/100) + - Raise the maximum signature script size to support standard 15-of-15 + multi-signature pay-to-sript-hash transactions with compressed pubkeys + to remain compatible with the reference client + (https://github.com/conformal/pod/issues/128) + - Reduce max bytes allowed for a standard nulldata transaction to 40 for + compatibility with the reference client + - Introduce a new btcnet package which houses all of the network params + for each network (mainnet, testnet3, regtest) to ultimately enable + easier addition and tweaking of networks without needing to change + several packages + - Fix several script discrepancies found by reference client test data + - Add new DNS seed for peer discovery (seed.bitnodes.io) + - Reduce the max known inventory cache from 20000 items to 1000 items + - Fix an issue where unknown inventory types could lead to a hung peer + - Implement inventory rebroadcast handler for sendrawtransaction + (https://github.com/conformal/pod/issues/99) + - Update user agent to fully support BIP0014 + (https://github.com/conformal/btcwire/issues/10) + - Implement initial mining support: + - Add a new logging subsystem for mining related operations + - Implement infrastructure for creating block templates + - Provide options to control block template creation settings + - Support the getwork RPC + - Allow address identifiers to apply to more than one network since both + testnet3 and the regression test network unfortunately use the same + identifier + - RPC changes: + - Set the content type for HTTP POST RPC connections to application/json + (https://github.com/conformal/pod/issues/121) + - Modified the RPC server startup so it only requires at least one valid + listen interface + - Correct an error path where it was possible certain errors would not + be returned + - Implement getwork command + (https://github.com/conformal/pod/issues/125) + - Update sendrawtransaction command to reject orphans + - Update sendrawtransaction command to include the reason a transaction + was rejected + - Update getinfo command to populate connection count field + - Update getinfo command to include relay fee field + (https://github.com/conformal/pod/issues/107) + - Allow transactions submitted with sendrawtransaction to bypass the + rate limiter + - Allow the getcurrentnet and getbestblock extensions to be accessed via + HTTP POST in addition to Websockets + (https://github.com/conformal/pod/issues/127) + - Websocket changes: + - Rework notifications to ensure they are delivered in the order they + occur + - Rename notifynewtxs command to notifyreceived (funds received) + - Rename notifyallnewtxs command to notifynewtransactions + - Rename alltx notification to txaccepted + - Rename allverbosetx notification to txacceptedverbose + (https://github.com/conformal/pod/issues/98) + - Add rescan progress notification + - Add recvtx notification + - Add redeemingtx notification + - Modify notifyspent command to accept an array of outpoints + (https://github.com/conformal/pod/issues/123) + - Significantly optimize the rescan command to yield up to a 60x speed + increase + - podctl utility changes: + - Add createencryptedwallet command + - Add getblockchaininfo command + - Add importwallet command + - Add addmultisigaddress command + - Add setgenerate command + - Accept --testnet and --wallet flags which automatically select + the appropriate port and TLS certificates needed to communicate + with pod and btcwallet (https://github.com/conformal/pod/issues/112) + - Allow path expansion from config file entries + (https://github.com/conformal/pod/issues/113) + - Minor refactor simplify handling of options + - addblock utility changes: + - Improve logging by making it consistent with the logging provided by + pod (https://github.com/conformal/pod/issues/90) + - Improve several package APIs for developers: + - Add new amount type for consistently handling monetary values + - Add new coin selector API + - Add new WIF (Wallet Import Format) API + - Add new crypto types for private keys and signatures + - Add new API to sign transactions including script merging and hash + types + - Expose function to extract all pushed data from a script + (https://github.com/conformal/btcscript/issues/8) + - Misc changes: + - Optimize address manager shuffling to do 67% less work on average + - Resolve a couple of benign data races found by the race detector + (https://github.com/conformal/pod/issues/101) + - Add IP address to all peer related errors to clarify which peer is the + cause (https://github.com/conformal/pod/issues/102) + - Fix a UPNP case issue that prevented the --upnp option from working + with some UPNP servers + - Update documentation in the sample config file regarding debug levels + - Adjust some logging levels to improve debug messages + - Improve the throughput of query messages to the block manager + - Several minor optimizations to reduce GC churn and enhance speed + - Other minor refactoring + - General code cleanup +Changes in 0.7.0 (Thu Feb 20 2014) + - Fix an issue when parsing scripts which contain a multi-signature script + which require zero signatures such as testnet block + 000000001881dccfeda317393c261f76d09e399e15e27d280e5368420f442632 + (https://github.com/conformal/btcscript/issues/7) + - Add check to ensure all transactions accepted to mempool only contain + canonical data pushes (https://github.com/conformal/btcscript/issues/6) + - Fix an issue causing excessive memory consumption + - Significantly rework and improve the websocket notification system: + - Each client is now independent so slow clients no longer limit the + speed of other connected clients + - Potentially long-running operations such as rescans are now run in + their own handler and rate-limited to one operation at a time without + preventing simultaneous requests from the same client for the faster + requests or notifications + - A couple of scenarios which could cause shutdown to hang have been + resolved + - Update notifynewtx notifications to support all address types instead + of only pay-to-pubkey-hash + - Provide a --rpcmaxwebsockets option to allow limiting the number of + concurrent websocket clients + - Add a new websocket command notifyallnewtxs to request notifications + (https://github.com/conformal/pod/issues/86) (thanks @flammit) + - Improve podctl utility in the following ways: + - Add getnetworkhashps command + - Add gettransaction command (wallet-specific) + - Add signmessage command (wallet-specific) + - Update getwork command to accept + - Continue cleanup and work on implementing the RPC API: + - Implement getnettotals command + (https://github.com/conformal/pod/issues/84) + - Implement networkhashps command + (https://github.com/conformal/pod/issues/87) + - Update getpeerinfo to always include syncnode field even when false + - Remove help addenda for getpeerinfo now that it supports all fields + - Close standard RPC connections on auth failure + - Provide a --rpcmaxclients option to allow limiting the number of + concurrent RPC clients (https://github.com/conformal/pod/issues/68) + - Include IP address in RPC auth failure log messages + - Resolve a rather harmless data races found by the race detector + (https://github.com/conformal/pod/issues/94) + - Increase block priority size and max standard transaction size to 50k + and 100k, respectively (https://github.com/conformal/pod/issues/71) + - Add rate limiting of free transactions to the memory pool to prevent + penny flooding (https://github.com/conformal/pod/issues/40) + - Provide a --logdir option (https://github.com/conformal/pod/issues/95) + - Change the default log file path to include the network + - Add a new ScriptBuilder interface to btcscript to support creation of + custom scripts (https://github.com/conformal/btcscript/issues/5) + - General code cleanup +Changes in 0.6.0 (Tue Feb 04 2014) + - Fix an issue when parsing scripts which contain invalid signatures that + caused a chain fork on block + 0000000000000001e4241fd0b3469a713f41c5682605451c05d3033288fb2244 + - Correct an issue which could lead to an error in removeBlockNode + (https://github.com/conformal/btcchain/issues/4) + - Improve addblock utility as follows: + - Check imported blocks against all chain rules and checkpoints + - Skip blocks which are already known so you can stop and restart the + import or start the import after you have already downloaded a portion + of the chain + - Correct an issue where the utility did not shutdown cleanly after + processing all blocks + - Add error on attempt to import orphan blocks + - Improve error handling and reporting + - Display statistics after input file has been fully processed + - Rework, optimize, and improve headers-first mode: + - Resuming the chain sync from any point before the final checkpoint + will now use headers-first mode + (https://github.com/conformal/pod/issues/69) + - Verify all checkpoints as opposed to only the final one + - Reduce and bound memory usage + - Rollback to the last known good point when a header does not match a + checkpoint + - Log information about what is happening with headers + - Improve podctl utility in the following ways: + - Add getaddednodeinfo command + - Add getnettotals command + - Add getblocktemplate command (wallet-specific) + - Add getwork command (wallet-specific) + - Add getnewaddress command (wallet-specific) + - Add walletpassphrasechange command (wallet-specific) + - Add walletlock command (wallet-specific) + - Add sendfrom command (wallet-specific) + - Add sendmany command (wallet-specific) + - Add settxfee command (wallet-specific) + - Add listsinceblock command (wallet-specific) + - Add listaccounts command (wallet-specific) + - Add keypoolrefill command (wallet-specific) + - Add getreceivedbyaccount command (wallet-specific) + - Add getrawchangeaddress command (wallet-specific) + - Add gettxoutsetinfo command (wallet-specific) + - Add listaddressgroupings command (wallet-specific) + - Add listlockunspent command (wallet-specific) + - Add listlock command (wallet-specific) + - Add listreceivedbyaccount command (wallet-specific) + - Add validateaddress command (wallet-specific) + - Add verifymessage command (wallet-specific) + - Add sendtoaddress command (wallet-specific) + - Continue cleanup and work on implementing the RPC API: + - Implement submitblock command + (https://github.com/conformal/pod/issues/61) + - Implement help command + - Implement ping command + - Implement getaddednodeinfo command + (https://github.com/conformal/pod/issues/78) + - Implement getinfo command + - Update getpeerinfo to support bytesrecv and bytessent + (https://github.com/conformal/pod/issues/83) + - Improve and correct several RPC server and websocket areas: + - Change the connection endpoint for websockets from /wallet to /ws + (https://github.com/conformal/pod/issues/80) + - Implement an alternative authentication for websockets so clients + such as javascript from browsers that don't support setting HTTP + headers can authenticate (https://github.com/conformal/pod/issues/77) + - Add an authentication deadline for RPC connections + (https://github.com/conformal/pod/issues/68) + - Use standard authentication failure responses for RPC connections + - Make automatically generated certificate more standard so it works + from client such as node.js and Firefox + - Correct some minor issues which could prevent the RPC server from + shutting down in an orderly fashion + - Make all websocket notifications require registration + - Change the data sent over websockets to text since it is JSON-RPC + - Allow connections that do not have an Origin header set + - Expose and track the number of bytes read and written per peer + (https://github.com/conformal/btcwire/issues/6) + - Correct an issue with sendrawtransaction when invoked via websockets + which prevented a minedtx notification from being added + - Rescan operations issued from remote wallets are no stopped when + the wallet disconnects mid-operation + (https://github.com/conformal/pod/issues/66) + - Several optimizations related to fetching block information from the + database + - General code cleanup +Changes in 0.5.0 (Mon Jan 13 2014) + - Optimize initial block download by introducing a new mode which + downloads the block headers first (up to the final checkpoint) + - Improve peer handling to remove the potential for slow peers to cause + sluggishness amongst all peers + (https://github.com/conformal/pod/issues/63) + - Fix an issue where the initial block sync could stall when the sync peer + disconnects (https://github.com/conformal/pod/issues/62) + - Correct an issue where --externalip was doing a DNS lookup on the full + host:port instead of just the host portion + (https://github.com/conformal/pod/issues/38) + - Fix an issue which could lead to a panic on chain switches + (https://github.com/conformal/pod/issues/70) + - Improve podctl utility in the following ways: + - Show getdifficulty output as floating point to 6 digits of precision + - Show all JSON object replies formatted as standard JSON + - Allow podctl getblock to accept optional params + - Add getaccount command (wallet-specific) + - Add getaccountaddress command (wallet-specific) + - Add sendrawtransaction command + - Continue cleanup and work on implementing RPC API calls + - Update getrawmempool to support new optional verbose flag + - Update getrawtransaction to match the reference client + - Update getblock to support new optional verbose flag + - Update raw transactions to fully match the reference client including + support for all transaction types and address types + - Correct getrawmempool fee field to return DUO instead of Satoshi + - Correct getpeerinfo service flag to return 8 digit string so it + matches the reference client + - Correct verifychain to return a boolean + - Implement decoderawtransaction command + - Implement createrawtransaction command + - Implement decodescript command + - Implement gethashespersec command + - Allow RPC handler overrides when invoked via a websocket versus + legacy connection + - Add new DNS seed for peer discovery + - Display user agent on new valid peer log message + (https://github.com/conformal/pod/issues/64) + - Notify wallet when new transactions that pay to registered addresses + show up in the mempool before being mined into a block + - Support a tor-specific proxy in addition to a normal proxy + (https://github.com/conformal/pod/issues/47) + - Remove deprecated sqlite3 imports from utilities + - Remove leftover profile write from addblock utility + - Quite a bit of code cleanup and refactoring to improve maintainability +Changes in 0.4.0 (Thu Dec 12 2013) + - Allow listen interfaces to be specified via --listen instead of only the + port (https://github.com/conformal/pod/issues/33) + - Allow listen interfaces for the RPC server to be specified via + --rpclisten instead of only the port + (https://github.com/conformal/pod/issues/34) + - Only disable listening when --connect or --proxy are used when no + --listen interface are specified + (https://github.com/conformal/pod/issues/10) + - Add several new standard transaction checks to transaction memory pool: + - Support nulldata scripts as standard + - Only allow a max of one nulldata output per transaction + - Enforce a maximum of 3 public keys in multi-signature transactions + - The number of signatures in multi-signature transactions must not + exceed the number of public keys + - The number of inputs to a signature script must match the expected + number of inputs for the script type + - The number of inputs pushed onto the stack by a redeeming signature + script must match the number of inputs consumed by the referenced + public key script + - When a block is connected, remove any transactions from the memory pool + which are now double spends as a result of the newly connected + transactions + - Don't relay transactions resurrected during a chain switch since + other peers will also be switching chains and therefore already know + about them + - Cleanup a few cases where rejected transactions showed as an error + rather than as a rejected transaction + - Ignore the default configuration file when --regtest (regression test + mode) is specified + - Implement TLS support for RPC including automatic certificate generation + - Support HTTP authentication headers for web sockets + - Update address manager to recognize and properly work with Tor + addresses (https://github.com/conformal/pod/issues/36) and + (https://github.com/conformal/pod/issues/37) + - Improve podctl utility in the following ways: + - Add the ability to specify a configuration file + - Add a default entry for the RPC cert to point to the location + it will likely be in the pod home directory + - Implement --version flag + - Provide a --notls option to support non-TLS configurations + - Fix a couple of minor races found by the Go race detector + - Improve logging + - Allow logging level to be specified on a per subsystem basis + (https://github.com/conformal/pod/issues/48) + - Allow logging levels to be dynamically changed via RPC + (https://github.com/conformal/pod/issues/15) + - Implement a rolling log file with a max of 10MB per file and a + rotation size of 3 which results in a max logging size of 30 MB + - Correct a minor issue with the rescanning websocket call + (https://github.com/conformal/pod/issues/54) + - Fix a race with pushing address messages that could lead to a panic + (https://github.com/conformal/pod/issues/58) + - Improve which external IP address is reported to peers based on which + interface they are connected through + (https://github.com/conformal/pod/issues/35) + - Add --externalip option to allow an external IP address to be specified + for cases such as tor hidden services or advanced network configurations + (https://github.com/conformal/pod/issues/38) + - Add --upnp option to support automatic port mapping via UPnP + (https://github.com/conformal/pod/issues/51) + - Update Ctrl+C interrupt handler to properly sync address manager and + remove the UPnP port mapping (if needed) + - Continue cleanup and work on implementing RPC API calls + - Add importprivkey (import private key) command to podctl + - Update getrawtransaction to provide addresses properly, support + new verbose param, and match the reference implementation with the + exception of MULTISIG (thanks @flammit) + - Update getblock with new verbose flag (thanks @flammit) + - Add listtransactions command to podctl + - Add getbalance command to podctl + - Add basic support for pod to run as a native Windows service + (https://github.com/conformal/pod/issues/42) + - Package addblock utility with Windows MSIs + - Add support for TravisCI (continuous build integration) + - Cleanup some documentation and usage + - Several other minor bug fixes and general code cleanup +Changes in 0.3.3 (Wed Nov 13 2013) + - Significantly improve initial block chain download speed + (https://github.com/conformal/pod/issues/20) + - Add a new checkpoint at block height 267300 + - Optimize most recently used inventory handling + (https://github.com/conformal/pod/issues/21) + - Optimize duplicate transaction input check + (https://github.com/conformal/btcchain/issues/2) + - Optimize transaction hashing + (https://github.com/conformal/pod/issues/25) + - Rework and optimize wallet listener notifications + (https://github.com/conformal/pod/issues/22) + - Optimize serialization and deserialization + (https://github.com/conformal/pod/issues/27) + - Add support for minimum transaction fee to memory pool acceptance + (https://github.com/conformal/pod/issues/29) + - Improve leveldb database performance by removing explicit GC call + - Fix an issue where Ctrl+C was not always finishing orderly database + shutdown + - Fix an issue in the script handling for OP_CHECKSIG + - Impose max limits on all variable length protocol entries to prevent + abuse from malicious peers + - Enforce DER signatures for transactions allowed into the memory pool + - Separate the debug profile http server from the RPC server + - Rework of the RPC code to improve performance and make the code cleaner + - The getrawtransaction RPC call now properly checks the memory pool + before consulting the db (https://github.com/conformal/pod/issues/26) + - Add support for the following RPC calls: getpeerinfo, getconnectedcount, + addnode, verifychain + (https://github.com/conformal/pod/issues/13) + (https://github.com/conformal/pod/issues/17) + - Implement rescan websocket extension to allow wallet rescans + - Use correct paths for application data storage for all supported + operating systems (https://github.com/conformal/pod/issues/30) + - Add a default redirect to the http profiling page when accessing the + http profile server + - Add a new --cpuprofile option which can be used to generate CPU + profiling data on platforms that support it + - Several other minor performance optimizations + - Other minor bug fixes and general code cleanup +Changes in 0.3.2 (Tue Oct 22 2013) + - Fix an issue that could cause the download of the block chain to stall + (https://github.com/conformal/pod/issues/12) + - Remove deprecated sqlite as an available database backend + - Close sqlite compile issue as sqlite has now been removed + (https://github.com/conformal/pod/issues/11) + - Change default RPC ports to 11048 (mainnet) and 21048 (testnet) + - Continue cleanup and work on implementing RPC API calls + - Add support for the following RPC calls: getrawmempool, + getbestblockhash, decoderawtransaction, getdifficulty, + getconnectioncount, getpeerinfo, and addnode + - Improve the podctl utility that is used to issue JSON-RPC commands + - Fix an issue preventing pod from cleanly shutting down with the RPC + stop command + - Add a number of database interface tests to ensure backends implement + the expected interface + - Expose some additional information from btcscript to be used for + identifying "standard"" transactions + - Add support for plan9 - thanks @mischief + (https://github.com/conformal/pod/pull/19) + - Other minor bug fixes and general code cleanup +Changes in 0.3.1-alpha (Tue Oct 15 2013) + - Change default database to leveldb + NOTE: This does mean you will have to redownload the block chain. Since we + are still in alpha, we didn't feel writing a converter was worth the time as + it would take away from more important issues at this stage + - Add a warning if there are multiple block chain databases of different types + - Fix issue with unexpected EOF in leveldb -- https://github.com/conformal/pod/issues/18 + - Fix issue preventing block 21066 on testnet -- https://github.com/conformal/btcchain/issues/1 + - Fix issue preventing block 96464 on testnet -- https://github.com/conformal/btcscript/issues/1 + - Optimize transaction lookups + - Correct a few cases of list removal that could result in improper cleanup + of no longer needed orphans + - Add functionality to increase ulimits on non-Windows platforms + - Add support for mempool command which allows remote peers to query the + transaction memory pool via the bitcoin protocol + - Clean up logging a bit + - Add a flag to disable checkpoints for developers + - Add a lot of useful debug logging such as message summaries + - Other minor bug fixes and general code cleanup +Initial Release 0.3.0-alpha (Sat Oct 05 2013): + - Initial release diff --git a/cmd/node/LICENSE b/cmd/node/LICENSE new file mode 100755 index 0000000..ea74d81 --- /dev/null +++ b/cmd/node/LICENSE @@ -0,0 +1,14 @@ +ISC License +Copyright (c) 2018- The Parallelcoin Team +Copyright (c) 2013-2017 The btcsuite developers +Copyright (c) 2015-2016 The Decred developers +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/cmd/node/README.md b/cmd/node/README.md new file mode 100755 index 0000000..c7ab578 --- /dev/null +++ b/cmd/node/README.md @@ -0,0 +1,164 @@ +![](https://gitlab.com/parallelcoin/node/raw/master/assets/logo.png) + +# The Parallelcoin Node [![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/node) + +Next generation full node for Parallelcoin, forked +from [btcd](https://github.com/btcsuite/btcd) + +## Hard Fork 1: Plan 9 from Crypto Space + +**TODO:** update this! + +9 algorithms can be used when mining: + +- Blake14lr (decred) +- ~~Skein (myriadcoin)~~ Cryptonote7v2 +- Lyra2REv2 (sia) +- Keccac (maxcoin, smartcash) +- Scrypt (litecoin) +- SHA256D (bitcoin) +- GOST Stribog \* +- Skein +- X11 (dash) + +### Stochastic Binomial Filter Difficulty Adjustment + +After the upcoming hardfork, Parallelcoin will have the following features in +its difficulty adjustment regime: + +- Exponential curve with power of 3 to respond gently the natural drift while + moving the difficulty fast in below 10% of target and 10x target, to deal with + recovering after a large increase in network hashpower + +- 293 second blocks (7 seconds less than 5 minutes), 1439 block averaging + window (about 4.8 days) that is varied by interpreting byte 0 of the sha256d + hash of newest block hash as a signed 8 bit integer to further disturb any + inherent rhythm (like dithering). + +- Difficulty adjustments are based on a window ending at the previous block of + each algorithm, meaning sharp rises from one algorithm do not immediately + affect the other algorithms, allowing a smoother recovery from a sudden drop + in hashrate, soaking up energetic movements more robustly and resiliently, and + reducing vulnerability to time distortion attacks. + +- Deterministic noise is added to the difficulty adjustment in a similar way as + is done with digital audio and images to improve the effective resolution of + the signal by reducing unwanted artifacts caused by the sampling process. + Miners are random generators, and a block time is like a tuning filter, so the + same principles apply. + +- Rewards will be computed according to a much smoother, satoshi-precision + exponential decay curve that will produce a flat annual 5% supply expansion. + Increasing the precision of the denomination is planned for the next release + cycle, at 0.00000001 as the minimum denomination, there may be issues as + userbase increases. + +- Fair Hardfork - Rewards will slowly rise from the initial hard fork at an + inverse exponential rate to bring the block reward from 0.02 up to 2 in 2000 + blocks, as the adjustment to network capacity takes time, so rewards will + closely match the time interval they relate to until it starts faster from the + minimum target stabilises in response to what miners create. + +## Installation + +For the main full node server: + +```bash +go get github.com/parallelcointeam/parallelcoin +``` + +You probably will also want CLI client (can also speak to other bitcoin protocol +RPC endpoints also): + +```bash +go get github.com/p9c/p9/cmd/podctl +``` + +## Requirements + +[Go](http://golang.org) 1.11 or newer. + +## Installation + +#### Windows not available yet + +When it is, it will be available here: + +https://github.com/p9c/p9/releases + +#### Linux/BSD/MacOSX/POSIX - Build from Source + +- Install Go according to + the [installation instructions](http://golang.org/doc/install) +- Ensure Go was installed properly and is a supported version: + +```bash +$ go version +$ go env GOROOT GOPATH +``` + +NOTE: The `GOROOT` and `GOPATH` above must not be the same path. It is +recommended that `GOPATH` is set to a directory in your home directory such +as `~/goprojects` to avoid write permission issues. It is also recommended to +add `$GOPATH/bin` to your `PATH` at this point. + +- Run the following commands to obtain pod, all dependencies, and install it: + +```bash +$ go get github.com/parallelcointeam/parallelcoin +``` + +- pod (and utilities) will now be installed in `$GOPATH/bin`. If you did not + already add the bin directory to your system path during Go installation, we + recommend you do so now. + +## Updating + +#### Windows + +Install a newer MSI + +#### Linux/BSD/MacOSX/POSIX - Build from Source + +- Run the following commands to update pod, all dependencies, and install it: + +```bash +$ cd $GOPATH/src/github.com/parallelcointeam/parallelcoin +$ git pull && glide install +$ go install . ./cmd/... +``` + +## Getting Started + +pod has several configuration options available to tweak how it runs, but all of +the basic operations described in the intro section work with zero +configuration. + +#### Windows (Installed from MSI) + +Launch pod from your Start menu. + +#### Linux/BSD/POSIX/Source + +```bash +$ ./pod +``` + +## Discord + +Come and chat at our (discord server](https://discord.gg/nJKts94) + +## Issue Tracker + +The [integrated github issue tracker](https://github.com/p9c/p9/issues) +is used for this project. + +## Documentation + +The documentation is a work-in-progress. It is located in +the [docs](https://github.com/p9c/p9/tree/master/docs) +folder. + +## License + +pod is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/cmd/node/active/config.go b/cmd/node/active/config.go new file mode 100644 index 0000000..5de64d3 --- /dev/null +++ b/cmd/node/active/config.go @@ -0,0 +1,28 @@ +package active + +import ( + "net" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/connmgr" + + "github.com/p9c/p9/pkg/chaincfg" +) + +// Config stores current state of the node +type Config struct { + Lookup connmgr.LookupFunc + Oniondial func(string, string, time.Duration) (net.Conn, error) + Dial func(string, string, time.Duration) (net.Conn, error) + AddedCheckpoints []chaincfg.Checkpoint + ActiveMiningAddrs []btcaddr.Address + ActiveMinerKey []byte + ActiveMinRelayTxFee amt.Amount + ActiveWhitelists []*net.IPNet + DropAddrIndex bool + DropTxIndex bool + DropCfIndex bool + Save bool +} diff --git a/cmd/node/docs/README.md b/cmd/node/docs/README.md new file mode 100755 index 0000000..fadd1c1 --- /dev/null +++ b/cmd/node/docs/README.md @@ -0,0 +1,326 @@ +### Table of Contents + +1. [About](#About) + +2. [Getting Started](#GettingStarted) + + 1. [Installation](#Installation) + + 1. [Windows](#WindowsInstallation) + + 2. [Linux/BSD/MacOSX/POSIX](#PosixInstallation) + + 3. [Gentoo Linux](#GentooInstallation) + + 2. [Configuration](#Configuration) + + 3. [Controlling and Querying pod via podctl](#BtcctlConfig) + + 4. [Mining](#Mining) + +3. [Help](#Help) + + 1. [Startup](#Startup) + + 1. [Using bootstrap.dat](#BootstrapDat) + + 2. [Network Configuration](#NetworkConfig) + + 3. [Wallet](#Wallet) + +4. [Contact](#Contact) + + 1. [IRC](#ContactIRC) + + 2. [Mailing Lists](#MailingLists) + +5. [Developer Resources](#DeveloperResources) + + 1. [Code Contribution Guidelines](#ContributionGuidelines) + + 2. [JSON-RPC Reference](#JSONRPCReference) + + 3. [The btcsuite Bitcoin-related Go Packages](#GoPackages) + + + +### 1. About + +pod is a full node bitcoin implementation written in [Go](http://golang.org), licensed under +the [copyfree](http://www.copyfree.org) ISC License. + +This project is currently under active development and is in a Beta state. It is extremely stable and has been in +production use since October 2013, but + +It properly downloads, validates, and serves the block chain using the exact rules (including consensus bugs) for block +acceptance as Bitcoin Core. We have taken great care to avoid pod causing a fork to the block chain. It includes a full +block validation testing framework which contains all of the 'official' block acceptance tests (and some additional +ones) that is run on every pull request to help ensure it properly follows consensus. Also, it passes all of the JSON +test data in the Bitcoin Core code. + +It also properly relays newly mined blocks, maintains a transaction pool, and relays individual transactions that have +not yet made it into a block. It ensures all individual transactions admitted to the pool follow the rules required by +the block chain and also includes more strict checks which filter transactions based on miner requirements ("standard" +transactions). + +One key difference between pod and Bitcoin Core is that pod does _NOT_ include wallet functionality and this was a very +intentional design decision. See the blog entry [here](https://blog.conformal.com/pod-not-your-moms-bitcoin-daemon) for +more details. This means you can't actually make or receive payments directly with pod. That functionality is provided +by the [btcwallet](https://github.com/p9c/p9/walletmain). + + + +### 2. Getting Started + + + +**2.1 Installation** + +The first step is to install pod. See one of the following sections for details on how to install on the supported +operating systems. + + + +**2.1.1 Windows Installation**
+ +- Install the MSI available at: https://github.com/p9c/p9/releases + +- Launch pod from the Start Menu + +
+ +**2.1.2 Linux/BSD/MacOSX/POSIX Installation** + +- Install Go according to the installation instructions here: http://golang.org/doc/install + +- Ensure Go was installed properly and is a supported version: + +```bash +$ go version +$ go env GOROOT GOPATH +``` + +NOTE: The `GOROOT` and `GOPATH` above must not be the same path. It is recommended that `GOPATH` is set to a directory +in your home directory such as `~/goprojects` to avoid write permission issues. It is also recommended to +add `$GOPATH/bin` to your `PATH` at this point. + +- Run the following commands to obtain pod, all dependencies, and install it: + +```bash +$ go get -u github.com/Masterminds/glide +$ git clone https://github.com/parallelcointeam/parallelcoin $GOPATH/src/github.com/parallelcointeam/parallelcoin +$ cd $GOPATH/src/github.com/parallelcointeam/parallelcoin +$ glide install +$ go install . ./cmd/... +``` + +- pod (and utilities) will now be installed in `$GOPATH/bin`. If you did not already add the bin directory to your + system path during Go installation, we recommend you do so now. + +**Updating** + +- Run the following commands to update pod, all dependencies, and install it: + +```bash +$ cd $GOPATH/src/github.com/parallelcointeam/parallelcoin +$ git pull && glide install +$ go install . ./cmd/... +``` + + + +**2.1.2.1 Gentoo Linux Installation** + +- Install Layman and enable the Bitcoin overlay. + + - https://gitlab.com/bitcoin/gentoo + +- Copy or symlink `/var/lib/layman/bitcoin/Documentation/package.keywords/pod-live` to `/etc/portage/package.keywords/` + +- Install pod: `$ emerge net-p2p/pod` + + + +**2.2 Configuration** + +pod has a number of [configuration](http://godoc.org/github.com/parallelcointeam/parallelcoin) options, which can be +viewed by running: `$ pod --help`. + + + +**2.3 Controlling and Querying pod via podctl** + +podctl is a command line utility that can be used to both control and query pod +via [RPC](http://www.wikipedia.org/wiki/Remote_procedure_call). pod does **not** enable its RPC server by default; You +must configure at minimum both an RPC username and password or both an RPC limited username and password: + +- pod.conf configuration file + +``` +[Application Options] +rpcuser=myuser +rpcpass=SomeDecentp4ssw0rd +rpclimituser=mylimituser +rpclimitpass=Limitedp4ssw0rd +``` + +- podctl.conf configuration file + +``` +[Application Options] +rpcuser=myuser +rpcpass=SomeDecentp4ssw0rd +``` + +OR + +``` +[Application Options] +rpclimituser=mylimituser +rpclimitpass=Limitedp4ssw0rd +``` + +For a list of available options, run: `$ podctl --help` + + + +**2.4 Mining** + +pod supports the `getblocktemplate` RPC. The limited user cannot access this RPC. + +**1. Add the payment addresses with the `miningaddr` option.** + +``` +[Application Options] +rpcuser=myuser +rpcpass=SomeDecentp4ssw0rd +miningaddr=12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX +miningaddr=1M83ju3EChKYyysmM2FXtLNftbacagd8FR +``` + +**2. Add pod's RPC TLS certificate to system Certificate Authority list.** + +`cgminer` uses [curl](http://curl.haxx.se/) to fetch data from the RPC server. Since curl validates the certificate by +default, we must install the `pod` RPC certificate into the default system Certificate Authority list. + +**Ubuntu** + +1. Copy rpc.cert to /usr/share/ca-certificates: `# cp /home/user/.pod/rpc.cert /usr/share/ca-certificates/pod.crt` + +2. Add pod.crt to /etc/ca-certificates.conf: `# echo pod.crt >> /etc/ca-certificates.conf` + +3. Update the CA certificate list: `# update-ca-certificates` + +**3. Set your mining software url to use https.** + +`$ cgminer -o https://127.0.0.1:11048 -u rpcuser -p rpcpassword` + + + +### 3. Help + + + +**3.1 Startup** + +Typically pod will run and start downloading the block chain with no extra configuration necessary, however, there is an +optional method to use a `bootstrap.dat` file that may speed up the initial block chain download process. + + + +**3.1.1 bootstrap.dat** + +- [Using bootstrap.dat](https://github.com/p9c/p9/tree/master/docs/using_bootstrap_dat.md) + + + +**3.1.2 Network Configuration** + +- [What Ports Are Used by Default?](https://github.com/p9c/p9/tree/master/docs/default_ports.md) + +- [How To Listen on Specific Interfaces](https://github.com/p9c/p9/tree/master/docs/configure_peer_server_listen_interfaces.md) + +- [How To Configure RPC Server to Listen on Specific Interfaces](https://github.com/p9c/p9/tree/master/docs/configure_rpc_server_listen_interfaces.md) + +- [Configuring pod with Tor](https://github.com/p9c/p9/tree/master/docs/configuring_tor.md) + + + +**3.1 Wallet** + +pod was intentionally developed without an integrated wallet for security reasons. Please +see [btcwallet](https://github.com/btcsuite/btcwallet) for more information. + + + +### 4. Contact + + + +**4.1 IRC** + +- [irc.freenode.net](irc://irc.freenode.net), channel `#pod` + + + +**4.2 Mailing Lists** + +- pod: discussion of pod and its packages. + +- pod-commits: + readonly mail-out of source code changes. + + + +### 5. Developer Resources + + + +- [Code Contribution Guidelines](https://github.com/p9c/p9/tree/master/docs/code_contribution_guidelines.md) + + + +- [JSON-RPC Reference](https://github.com/p9c/p9/tree/master/docs/json_rpc_api.md) + +- [RPC Examples](https://github.com/p9c/p9/tree/master/docs/json_rpc_api.md#ExampleCode) + + + +- The btcsuite Bitcoin-related Go Packages: + + - [btcrpcclient](https://github.com/p9c/p9/tree/master/rpcclient) - Implements a robust and easy to use + Websocket-enabled Bitcoin JSON-RPC client + + - [btcjson](https://github.com/p9c/p9/tree/master/btcjson) - Provides an extensive API for the underlying JSON-RPC + command and return values + + - [wire](https://github.com/p9c/p9/tree/master/wire) - Implements the Bitcoin wire protocol + + - [peer](https://github.com/p9c/p9/tree/master/peer) - Provides a common base for creating and managing Bitcoin + network peers. + + - [blockchain](https://github.com/p9c/p9/tree/master/blockchain) - Implements Bitcoin block handling and chain + selection rules + + - [blockchain/fullblocktests](https://github.com/p9c/p9/tree/master/blockchain/fullblocktests) - Provides a set of + block tests for testing the consensus validation rules + + - [txscript](https://github.com/p9c/p9/tree/master/txscript) - Implements the Bitcoin transaction scripting + language + + - [btcec](https://github.com/p9c/p9/tree/master/btcec) - Implements support for the elliptic curve cryptographic + functions needed for the Bitcoin scripts + + - [database](https://github.com/p9c/p9/tree/master/database) - Provides a database interface for the Bitcoin block + chain + + - [mempool](https://github.com/p9c/p9/tree/master/mempool) - Package mempool provides a policy-enforced pool of + unmined bitcoin transactions. + + - [util](https://github.com/p9c/p9/util) - Provides Bitcoin-specific convenience functions and types + + - [chainhash](https://github.com/p9c/p9/tree/master/chaincfg/chainhash) - Provides a generic hash type and + associated functions that allows the specific hash algorithm to be abstracted. + + - [connmgr](https://github.com/p9c/p9/tree/master/connmgr) - Package connmgr implements a generic Bitcoin network + connection manager. diff --git a/cmd/node/docs/code_contribution_guidelines.md b/cmd/node/docs/code_contribution_guidelines.md new file mode 100755 index 0000000..8205df1 --- /dev/null +++ b/cmd/node/docs/code_contribution_guidelines.md @@ -0,0 +1,308 @@ +### Table of Contents + +1. [Overview](#Overview)
+2. [Minimum Recommended Skillset](#MinSkillset)
+3. [Required Reading](#ReqReading)
+4. [Development Practices](#DevelopmentPractices)
+ 4.1. [Share Early, Share Often](#ShareEarly)
+ 4.2. [Testing](#Testing)
+ 4.3. [Code Documentation and Commenting](#CodeDocumentation)
+ 4.4. [Model Git Commit Messages](#ModelGitCommitMessages)
+5. [Code Approval Process](#CodeApproval)
+ 5.1 [Code Review](#CodeReview)
+ 5.2 [Rework Code (if needed)](#CodeRework)
+ 5.3 [Acceptance](#CodeAcceptance)
+6. [Contribution Standards](#Standards)
+ 6.1. [Contribution Checklist](#Checklist)
+ 6.2. [Licensing of Contributions](#Licensing)
+ +
+ +### 1. Overview + +Developing cryptocurrencies is an exciting endeavor that touches a wide variety of areas such as wire protocols, +peer-to-peer networking, databases, cryptography, language interpretation (transaction scripts), RPC, and websockets. +They also represent a radical shift to the current fiscal system and as a result provide an opportunity to help reshape +the entire financial system. There are few projects that offer this level of diversity and impact all in one code base. + +However, as exciting as it is, one must keep in mind that cryptocurrencies represent real money and introducing bugs and +security vulnerabilities can have far more dire consequences than in typical projects where having a small bug is +minimal by comparison. In the world of cryptocurrencies, even the smallest bug in the wrong area can cost people a +significant amount of money. For this reason, the pod suite has a formalized and rigorous development process which is +outlined on this page. + +We highly encourage code contributions, however it is imperative that you adhere to the guidelines established on this +page. + + + +### 2. Minimum Recommended Skillset + +The following list is a set of core competencies that we recommend you possess before you really start attempting to +contribute code to the project. These are not hard requirements as we will gladly accept code contributions as long as +they follow the guidelines set forth on this page. That said, if you don't have the following basic qualifications you +will likely find it quite difficult to contribute. + +- A reasonable understanding of bitcoin at a high level (see the [Required Reading](#ReqReading) section for the + original white paper) + +- Experience in some type of C-like language. Go is preferable of course. + +- An understanding of data structures and their performance implications + +- Familiarity with unit testing + +- Debugging experience + +- Ability to understand not only the area you are making a change in, but also the code your change relies on, and the + code which relies on your changed code + +Building on top of those core competencies, the recommended skill set largely depends on the specific areas you are +looking to contribute to. For example, if you wish to contribute to the cryptography code, you should have a good +understanding of the various aspects involved with cryptography such as the security and performance implications. + + + +### 3. Required Reading + +- [Effective Go](http://golang.org/doc/effective_go.html) - The entire pod suite follows the guidelines in this + document. For your code to be accepted, it must follow the guidelines therein. + +- [Original Satoshi Whitepaper](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCkQFjAA&url=http%3A%2F%2Fbitcoin.org%2Fbitcoin.pdf&ei=os3VUuH8G4SlsASV74GoAg&usg=AFQjCNEipPLigou_1MfB7DQjXCNdlylrBg&sig2=FaHDuT5z36GMWDEnybDJLg&bvm=bv.59378465,d.b2I) + - This is the white paper that started it all. Having a solid foundation to build on will make the code much more + comprehensible. + + + +### 4. Development Practices + +Developers are expected to work in their own trees and submit pull requests when they feel their feature or bug fix is +ready for integration into the master branch. + + + +### 4.1 Share Early, Share Often + +We firmly believe in the share early, share often approach. The basic premise of the approach is to announce your +plans **before** you start work, and once you have started working, craft your changes into a stream of small and easily +reviewable commits. + +This approach has several benefits: + +- Announcing your plans to work on a feature **before** you begin work avoids duplicate work + +- It permits discussions which can help you achieve your goals in a way that is consistent with the existing + architecture + +- It minimizes the chances of you spending time and energy on a change that might not fit with the consensus of the + community or existing architecture and potentially be rejected as a result + +- Incremental development helps ensure you are on the right track with regards to the rest of the community + +- The quicker your changes are merged to master, the less time you will need to spend rebasing and otherwise trying to + keep up with the main code base + + + +### 4.2 Testing + +One of the major design goals of all core pod packages is to aim for complete test coverage. This is financial software +so bugs and regressions can cost people real money. For this reason every effort must be taken to ensure the code is as +accurate and bug-free as possible. Thorough testing is a good way to help achieve that goal. + +Unless a new feature you submit is completely trivial, it will probably be rejected unless it is also accompanied by +adequate test coverage for both positive and negative conditions. That is to say, the tests must ensure your code works +correctly when it is fed correct data as well as incorrect data (error paths). + +Go provides an excellent test framework that makes writing test code and checking coverage statistics straight forward. +For more information about the test coverage tools, see the [golang cover blog post](http://blog.golang.org/cover). + +A quick summary of test practices follows: + +- All new code should be accompanied by tests that ensure the code behaves correctly when given expected values, and, + perhaps even more importantly, that it handles errors gracefully + +- When you fix a bug, it should be accompanied by tests which exercise the bug to both prove it has been resolved and to + prevent future regressions + + + +### 4.3 Code Documentation and Commenting + +Comments have a way of turning into lies during development, you should not expect readers to depend on it. Much more +important is that names are meaningful, they do not take up excessive space, and comments are only necessary when the +meaning of the code needs clarification. + +- At a minimum every function must be commented with its intended purpose and any assumptions that it makes + + - Function comments must always begin with the name of the function + per [Effective Go](http://golang.org/doc/effective_go.html) + + - Function comments should be complete sentences since they allow a wide variety of automated presentations such + as [godoc.org](https://godoc.org) + + - Comments should be brief, function type signatures should be informative enough that the comment is for + clarification. Comments are not tested by the compiler, and can obscure the intent of the code if the code is + opaque in its semantics. + + - Comments will be parsed by godoc and excess vertical space usage reduces the readability of code, so there is no + sane reason why the comments (and indeed, in documents such as this) should be manually split into lines. That's + what word wrap is for. + + - The general rule of thumb is to look at it as if you were completely unfamiliar with the code and ask yourself, + would this give me enough information to understand what this function does and how I'd probably want to use it? + + - Detailed information in comments should be mainly in the type definitions. Meaningful names in function parameters + are more important than silly long complicated comments and make the code harder to read where the clarity is most + needed. + + - If you need to write a lot of comments about code you probably have not written it well. + + - Variable and constant names should be informative, and where obvious, brief. + + - If the function signature is longer than 80 characters in total, you should change the parameters to be a + structured variable, the structure will explain the parameters better and more visually attractive than a function + call with more than 5 parameters. + + - If you use a constant value more than a few times, and especially, in more than a few source files, you should + give it a meaningful name and place it into separate folders that allows you to avoid circular dependencies. It is + better to define a type independently, and create an alias in the implementation as methods must be defined + locally to the type. However, if you need to access the fields of the type, the definition needs to be isolated + separate from the implementation, otherwise you almost certainly will run into a circular dependency that will + block compilation. + + - The best place for detailed information is a separate `doc.go` file, where the comment that appears before the + package name appears at the very top in the Godoc output, and in the structure and type definitions for exported + types. Functions should not be the place to put this, as it interferes with readability, and scatters the + information, at the same time. + + + +### 4.4 Model Git Commit Messages + +This project prefers to keep a clean commit history with well-formed commit messages. This section illustrates a model +commit message and provides a bit of background for it. This content was originally created by Tim Pope and made +available on his website, however that website is no longer active, so it is being provided here. + +Here’s a model Git commit message: + +``` +Short (50 chars or less) summary of changes + +More detailed explanatory text, if necessary. Wrap it to about 72 +characters or so. In some contexts, the first line is treated as the +subject of an email and the rest of the text as the body. The blank +line separating the summary from the body is critical (unless you omit +the body entirely); tools like rebase can get confused if you run the +two together. + +Write your commit message in the present tense: "Fix bug" and not "Fixed +bug." This convention matches up with commit messages generated by +commands like git merge and git revert. + +Further paragraphs come after blank lines. + +- Bullet points are okay, too +- Typically a hyphen or asterisk is used for the bullet, preceded by a + single space, with blank lines in between, but conventions vary here +- Use a hanging indent +``` + +Prefix the summary with the subsystem/package when possible. Many other projects make use of the code and this makes it +easier for them to tell when something they're using has changed. Have a look +at [past commits](https://github.com/p9c/p9/commits/master) for examples of commit messages. + + + +### 5. Code Approval Process + +This section describes the code approval process that is used for code contributions. This is how to get your changes +into pod. + + + +### 5.1 Code Review + +All code which is submitted will need to be reviewed before inclusion into the master branch. This process is performed +by the project maintainers and usually other committers who are interested in the area you are working in as well. + +##### Code Review Timeframe + +The timeframe for a code review will vary greatly depending on factors such as the number of other pull requests which +need to be reviewed, the size and complexity of the contribution, how well you followed the guidelines presented on this +page, and how easy it is for the reviewers to digest your commits. For example, if you make one monolithic commit that +makes sweeping changes to things in multiple subsystems, it will obviously take much longer to review. You will also +likely be asked to split the commit into several smaller, and hence more manageable, commits. + +Keeping the above in mind, most small changes will be reviewed within a few days, while large or far reaching changes +may take weeks. This is a good reason to stick with the [Share Early, Share Often](#ShareOften) development practice +outlined above. + +##### What is the review looking for? + +The review is mainly ensuring the code follows the [Development Practices](#DevelopmentPractices) +and [Code Contribution Standards](#Standards). However, there are a few other checks which are generally performed as +follows: + +- The code is stable and has no stability or security concerns +- The code is properly using existing APIs and generally fits well into the overall architecture +- The change is not something which is deemed inappropriate by community consensus + + + +### 5.2 Rework Code (if needed) + +After the code review, the change will be accepted immediately if no issues are found. If there are any concerns or +questions, you will be provided with feedback along with the next steps needed to get your contribution merged with +master. In certain cases the code reviewer(s) or interested committers may help you rework the code, but generally you +will simply be given feedback for you to make the necessary changes. + +This process will continue until the code is finally accepted. + + + +### 5.3 Acceptance + +Once your code is accepted, it will be integrated with the master branch. Typically it will be rebased and fast-forward +merged to master as we prefer to keep a clean commit history over a tangled weave of merge commits. However,regardless +of the specific merge method used, the code will be integrated with the master branch and the pull request will be +closed. + +Rejoice as you will now be listed as a [contributor](https://github.com/p9c/p9/graphs/contributors)! + + + +### 6. Contribution Standards + + + +### 6.1. Contribution Checklist + +- [  ] All changes are Go version 1.11 compliant + +- [  ] The code being submitted is commented according to + the [Code Documentation and Commenting](#CodeDocumentation) section + +- [  ] For new code: Code is accompanied by tests which exercise both the positive and negative (error paths) + conditions (if applicable) + +- [  ] For bug fixes: Code is accompanied by new tests which trigger the bug being fixed to prevent + regressions + +- [  ] Any new logging statements use an appropriate subsystem and logging level + +- [  ] Code has been formatted with `go fmt` + +- [  ] Running `go test` does not fail any tests + +- [  ] Running `go vet` does not report any issues + +- [  ] Running [golint](https://github.com/golang/lint) does not report any **new** issues that did not + already exist + + + +### 6.2. Licensing of Contributions + +All contributions must be licensed with the [ISC license](https://github.com/p9c/p9/blob/master/LICENSE). This is the +same license as all of the code in the pod suite. diff --git a/cmd/node/docs/configure_peer_server_listen_interfaces.md b/cmd/node/docs/configure_peer_server_listen_interfaces.md new file mode 100755 index 0000000..4e79f4e --- /dev/null +++ b/cmd/node/docs/configure_peer_server_listen_interfaces.md @@ -0,0 +1,36 @@ +pod allows you to bind to specific interfaces which enables you to setup configurations with varying levels of +complexity. The listen parameter can be specified on the command line as shown below with the -- prefix or in the +configuration file without the -- prefix (as can all long command line options). + +The configuration file takes one entry per line. + +**NOTE:** The listen flag can be specified multiple times to listen on multiple interfaces as a couple of the examples +below illustrate. + +Command Line Examples: + +| Flags | Comment | +| -------------------------------------------- | -------------------------------------------------------------------------------------------- | +| --listen= | all interfaces on default port which is changed by `--testnet` and `--regtest` (** +default**) | +| --listen=0.0.0.0 | all IPv4 interfaces on default port which is changed by `--testnet` and `--regtest` | +| --listen=:: | all IPv6 interfaces on default port which is changed by `--testnet` and `--regtest` | +| --listen=:11047 | all interfaces on port 11047 | +| --listen=0.0.0.0:11047 | all IPv4 interfaces on port 11047 | +| --listen=[::]:11047 | all IPv6 interfaces on port 11047 | +| --listen=127.0.0.1:11047 | only IPv4 localhost on port 11047 | +| --listen=[::1]:11047 | only IPv6 localhost on port 11047 | +| --listen=:8336 | all interfaces on non-standard port 8336 | +| --listen=0.0.0.0:8336 | all IPv4 interfaces on non-standard port 8336 | +| --listen=[::]:8336 | all IPv6 interfaces on non-standard port 8336 | +| --listen=127.0.0.1:8337 --listen=[::1]:11047 | IPv4 localhost on port 8337 and IPv6 localhost on port 11047 | +| --listen=:11047 --listen=:8337 | all interfaces on ports 11047 and 8337 | + +The following config file would configure pod to only listen on localhost for both IPv4 and IPv6: + +```text +[Application Options] + +listen=127.0.0.1:11047 +listen=[::1]:11047 +``` diff --git a/cmd/node/docs/configure_rpc_server_listen_interfaces.md b/cmd/node/docs/configure_rpc_server_listen_interfaces.md new file mode 100755 index 0000000..af488ba --- /dev/null +++ b/cmd/node/docs/configure_rpc_server_listen_interfaces.md @@ -0,0 +1,51 @@ +pod allows you to bind the RPC server to specific interfaces which enables you to setup configurations with varying +levels of complexity. The `rpclisten` parameter can be specified on the command line as shown below with the -- prefix +or in the configuration file without the -- prefix (as can all long command line options). + +The configuration file takes one entry per line. + +A few things to note regarding the RPC server: + +- The RPC server will **not** be enabled unless the `rpcuser` and `rpcpass` options are specified. + +- When the `rpcuser` and `rpcpass` and/or `rpclimituser` and `rpclimitpass` options are specified, the RPC server will + only listen on localhost IPv4 and IPv6 interfaces by default. You will need to override the RPC listen interfaces to + include external interfaces if you want to connect from a remote machine. + +- The RPC server has TLS disabled by default. You may use the `--TLS` option to enable it. + +- The `--rpclisten` flag can be specified multiple times to listen on multiple interfaces as a couple of the examples + below illustrate. + +- The RPC server is disabled by default when using the `--regtest` and + `--simnet` networks. You can override this by specifying listen interfaces. + +Command Line Examples: + +| Flags | Comment | +| ----------------------------------------------- | ------------------------------------------------------------------- | +| --rpclisten= | all interfaces on default port which is changed by `--testnet` | +| --rpclisten=0.0.0.0 | all IPv4 interfaces on default port which is changed by `--testnet` | +| --rpclisten=:: | all IPv6 interfaces on default port which is changed by `--testnet` | +| --rpclisten=:11048 | all interfaces on port 11048 | +| --rpclisten=0.0.0.0:11048 | all IPv4 interfaces on port 11048 | +| --rpclisten=[::]:11048 | all IPv6 interfaces on port 11048 | +| --rpclisten=127.0.0.1:11048 | only IPv4 localhost on port 11048 | +| --rpclisten=[::1]:11048 | only IPv6 localhost on port 11048 | +| --rpclisten=:8336 | all interfaces on non-standard port 8336 | +| --rpclisten=0.0.0.0:8336 | all IPv4 interfaces on non-standard port 8336 | +| --rpclisten=[::]:8336 | all IPv6 interfaces on non-standard port 8336 | +| --rpclisten=127.0.0.1:8337 --listen=[::1]:11048 | IPv4 localhost on port 8337 and IPv6 localhost on port 11048 | +| --rpclisten=:11048 --listen=:8337 | all interfaces on ports 11048 and 8337 | + +The following config file would configure the pod RPC server to listen to all interfaces on the default port, including +external interfaces, for both IPv4 and IPv6: + +```text +[Application Options] + +rpclisten= +``` + +As well as the standard port 11048, which delivers sha256d block templates, there is another 8 RPC ports that open +sequentially up to 11056 with each one providing a specific block template version number. diff --git a/cmd/node/docs/configuring_tor.md b/cmd/node/docs/configuring_tor.md new file mode 100755 index 0000000..143e7fc --- /dev/null +++ b/cmd/node/docs/configuring_tor.md @@ -0,0 +1,201 @@ +### Table of Contents + +1. [Overview](#Overview)
+ +2. [Client-Only](#Client)
+ +2.1 [Description](#ClientDescription)
+ +2.2 [Command Line Example](#ClientCLIExample)
+ +2.3 [Config File Example](#ClientConfigFileExample)
+ +3. [Client-Server via Tor Hidden Service](#HiddenService)
+ +3.1 [Description](#HiddenServiceDescription)
+ +3.2 [Command Line Example](#HiddenServiceCLIExample)
+ +3.3 [Config File Example](#HiddenServiceConfigFileExample)
+ +4. [Bridge Mode (Not Anonymous)](#Bridge)
+ +4.1 [Description](#BridgeDescription)
+ +4.2 [Command Line Example](#BridgeCLIExample)
+ +4.3 [Config File Example](#BridgeConfigFileExample)
+ +5. [Tor Stream Isolation](#TorStreamIsolation)
+ +5.1 [Description](#TorStreamIsolationDescription)
+ +5.2 [Command Line Example](#TorStreamIsolationCLIExample)
+ +5.3 [Config File Example](#TorStreamIsolationFileExample)
+ + + +### 1. Overview + +pod provides full support for anonymous networking via the [Tor Project](https://www.torproject.org/), +including [client-only](#Client) and [hidden service](#HiddenService) configurations along +with [stream isolation](#TorStreamIsolation). In addition, pod supports a hybrid, [bridge mode](#Bridge) which is not +anonymous, but allows it to operate as a bridge between regular nodes and hidden service nodes without routing the +regular connections through Tor. + +While it is easier to only run as a client, it is more beneficial to the Bitcoin network to run as both a client and a +server so others may connect to you to as you are connecting to them. We recommend you take the time to setup a Tor +hidden service for this reason. + + + +### 2. Client-Only + + + +**2.1 Description**
+ +Configuring pod as a Tor client is straightforward. The first step is obviously to install Tor and ensure it is working. +Once that is done, all that typically needs to be done is to specify the `--proxy` flag via the pod command line or in +the pod configuration file. Typically the Tor proxy address will be 127.0.0.1:9050 (if using standalone Tor) or +127.0.0.1:9150 (if using the Tor Browser Bundle). If you have Tor configured to require a username and password, you may +specify them with the `--proxyuser` and `--proxypass` flags. + +By default, pod assumes the proxy specified with `--proxy` is a Tor proxy and hence will send all traffic, including DNS +resolution requests, via the specified proxy. + +NOTE: Specifying the `--proxy` flag disables listening by default since you will not be reachable for inbound +connections unless you also configure a Tor [hidden service](#HiddenService). + + + +**2.2 Command Line Example**
+ +```bash +$ ./pod --proxy=127.0.0.1:9050 +``` + + + +**2.3 Config File Example**
+ +```text +[Application Options] +proxy=127.0.0.1:9050 +``` + + + +### 3. Client-Server via Tor Hidden Service + + + +**3.1 Description**
+ +The first step is to configure Tor to provide a hidden service. Documentation for this can be found on the Tor project +website [here](https://www.torproject.org/docs/tor-hidden-service.html.en). However, there is no need to install a web +server locally as the linked instructions discuss since pod will act as the server. + +In short, the instructions linked above entail modifying your `torrc` file to add something similar to the following, +restarting Tor, and opening the `hostname` file in the `HiddenServiceDir` to obtain your hidden service .onion address. + +```text +HiddenServiceDir /var/tor/pod +HiddenServicePort 11047 127.0.0.1:11047 +``` + +Once Tor is configured to provide the hidden service and you have obtained your generated .onion address, configuring +pod as a Tor hidden service requires three flags: + +- `--proxy` to identify the Tor (SOCKS 5) proxy to use for outgoing traffic. This is typically 127.0.0.1:9050. + +- `--listen` to enable listening for inbound connections since `--proxy` disables listening by default + +- `--externalip` to set the .onion address that is advertised to other peers + + + + **3.2 Command Line Example**
+ +```bash +$ ./pod --proxy=127.0.0.1:9050 --listen=127.0.0.1 --externalip=fooanon.onion +``` + + + +**3.3 Config File Example**
+ +```text +[Application Options] +proxy=127.0.0.1:9050 +listen=127.0.0.1 +externalip=fooanon.onion +``` + + + +### 4. Bridge Mode (Not Anonymous) + + + +**4.1 Description**
+ +pod provides support for operating as a bridge between regular nodes and hidden service nodes. In particular this means +only traffic which is directed to or from a .onion address is sent through Tor while other traffic is sent normally. _As +a result, this mode is **NOT** anonymous._ This mode works by specifying an onion-specific proxy, which is pointed at +Tor, by using the `--onion` flag via the pod command line or in the pod configuration file. If you have Tor configured +to require a username and password, you may specify them with the `--onionuser` and `--onionpass` flags. + +NOTE: This mode will also work in conjunction with a hidden service which means you could accept inbound connections +both via the normal network and to your hidden service through the Tor network. To enable your hidden service in bridge +mode, you only need to specify your hidden service's .onion address via the `--externalip` flag since traffic to and +from .onion addresses are already routed via Tor due to the `--onion` flag. + + + +**4.2 Command Line Example**
+ +```bash +$ ./pod --onion=127.0.0.1:9050 --externalip=fooanon.onion +``` + + + +**4.3 Config File Example**
+ +```text +[Application Options] +onion=127.0.0.1:9050 +externalip=fooanon.onion +``` + + + +### 5. Tor Stream Isolation + + + +**5.1 Description**
+ +Tor stream isolation forces Tor to build a new circuit for each connection making it harder to correlate connections. +pod provides support for Tor stream isolation by using the `--torisolation` flag. This option requires --proxy or +--onionproxy to be set. + + + +**5.2 Command Line Example**
+ +```bash +$ ./pod --proxy=127.0.0.1:9050 --torisolation +``` + + + +**5.3 Config File Example**
+ +```text +[Application Options] +proxy=127.0.0.1:9050 +torisolation=1 +``` diff --git a/cmd/node/docs/default_ports.md b/cmd/node/docs/default_ports.md new file mode 100755 index 0000000..fbcc191 --- /dev/null +++ b/cmd/node/docs/default_ports.md @@ -0,0 +1,12 @@ +While pod is highly configurable when it comes to the network configuration, the following is intended to be a quick +reference for the default ports used so port forwarding can be configured as required. + +pod provides a `--upnp` flag which can be used to automatically map the bitcoin peer-to-peer listening port if your +router supports UPnP. If your router does not support UPnP, or you don't wish to use it, please note that only the +bitcoin peer-to-peer port should be forwarded unless you specifically want to allow RPC access to your pod from external +sources such as in more advanced network configurations. + +| Name | Port | +| --------------------------------- | --------- | +| Default Bitcoin peer-to-peer port | TCP 11047 | +| Default RPC port | TCP 11048 | diff --git a/cmd/node/docs/json_rpc_api.md b/cmd/node/docs/json_rpc_api.md new file mode 100755 index 0000000..5031830 --- /dev/null +++ b/cmd/node/docs/json_rpc_api.md @@ -0,0 +1,1484 @@ +### Table of Contents + +1. [Overview](#Overview)
+ +2. [HTTP POST Versus Websockets](#HttpPostVsWebsockets)
+ +3. [Authentication](#Authentication)
+ + 3.1. [Overview](#AuthenticationOverview)
+ + 3.2. [HTTP Basic Access Authentication](#HTTPAuth)
+ + 3.3. [JSON-RPC Authenticate Command (Websocket-specific)](#JSONAuth)
+ +4. [Command-line Utility](#CLIUtil)
+ +5. [Standard Methods](#Methods)
+ + 5.1. [Method Overview](#MethodOverview)
+ + 5.2. [Method Details](#MethodDetails)
+ +6. [Extension Methods](#ExtensionMethods)
+ + 6.1. [Method Overview](#ExtMethodOverview)
+ + 6.2. [Method Details](#ExtMethodDetails)
+ +7. [Websocket Extension Methods (Websocket-specific)](#WSExtMethods)
+ + 7.1. [Method Overview](#WSExtMethodOverview)
+ + 7.2. [Method Details](#WSExtMethodDetails)
+ +8. [Notifications (Websocket-specific)](#Notifications)
+ + 8.1. [Notification Overview](#NotificationOverview)
+ + 8.2. [Notification Details](#NotificationDetails)
+ +9. [Example Code](#ExampleCode)
+ + 9.1. [Go](#ExampleGoApp)
+ + 9.2. [node.js](#ExampleNodeJsCode)
+ + + +### 1. Overview + +pod provides a [JSON-RPC](http://json-rpc.org/wiki/specification) API that is fully compatible with the original +bitcoind/bitcoin-qt. There are a few key differences between pod and bitcoind as far as how RPCs are serviced: + +- Unlike bitcoind that has the wallet and chain intermingled in the same process which leads to several issues, pod + intentionally splits the wallet and chain services into independent processes. See the blog + post [here](https://blog.conformal.com/pod-not-your-moms-bitcoin-daemon/) for further details on why they were + separated. This means that if you aretalking directly to pod, only chain-related RPCs are available. However both + chain-related and wallet-related RPCs are available via [mod](https://github.com/p9c/p9/walletmain). + +- pod provides access to the API through both [HTTP POST](http://en.wikipedia.org/wiki/POST_%28HTTP%29) requests and + [Websockets](http://en.wikipedia.org/wiki/WebSocket) Websockets are the preferred transport for pod RPC and are used + by applications such as [mod](https://github.com/p9c/p9/walletmain) for inter-process communication with pod. The + websocket connection endpoint for pod is `wss://your_ip_or_domain:11048/ws`. + + In addition to the [standard API](#Methods), an [extension API](#WSExtMethods) has been developed that is exclusive to + clients using Websockets. In its current state, this API attempts to cover features found missing in the standard API + during the development of btcwallet. + + While the [standard API](#Methods) is stable, the [Websocket extension API](#WSExtMethods) should be considered a work + in progress, incomplete, and susceptible to changes (both additions and removals). + + The original bitcoind/bitcoin-qt JSON-RPC API documentation is available + at [https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list](https://en.bitcoin.it/wiki/Original_Bitcoin_client/API_Calls_list) + + +### 2. HTTP POST Versus Websockets + +The pod RPC server supports both [HTTP POST](http://en.wikipedia.org/wiki/POST_%28HTTP%29) requests and the +preferred [Websockets](http://en.wikipedia.org/wiki/WebSocket). All of the [standard](#Methods) +and [extension](#ExtensionMethods) methods described in this documentation can be accessed through both. As the name +indicates, the [Websocket-specific extension](#WSExtMethods) methods can only be accessed when connected via Websockets. + +As mentioned in the [overview](#Overview), the websocket connection endpoint for pod +is `wss://your_ip_or_domain:11048/ws`. The most important differences between the two transports as it pertains to the +JSON-RPC API are: + +| | HTTP POST Requests | Websockets | +| --------------------------------------------------- | ------------------ | ---------- | +| Allows multiple requests across a single connection | No | Yes | +| Supports asynchronous notifications | No | Yes | +| Scales well with large numbers of requests | No | Yes | + + + +### 3. Authentication + + +**3.1 Authentication Overview**
+ +The following authentication details are needed before establishing a connection to a pod RPC server: + +- **rpcuser** is the full-access username configured for the pod RPC server + +- **rpcpass** is the full-access password configured for the pod RPC server + +- **rpclimituser** is the limited username configured for the pod RPC server + +- **rpclimitpass** is the limited password configured for the pod RPC server + +- **rpccert** is the PEM-encoded X.509 certificate (public key) that the pod server is configured with. It is + automatically generated by pod and placed in the pod home directory (which is typically `%LOCALAPPDATA%\Pod` on + Windows and `~/.pod` on POSIX-like OSes) + +Depending on which connection transaction you are using, you can choose one of two, mutually exclusive, methods. + +- [Use HTTP Authorization Header](#HTTPAuth) - HTTP POST requests and Websockets + +- [Use the JSON-RPC "authenticate" command](#JSONAuth) - Websockets only + + +**3.2 HTTP Basic Access Authentication**
+ +The pod RPC server uses HTTP [basic access authentication](http://en.wikipedia.org/wiki/Basic_access_authentication) +with the **rpcuser** and **rpcpass** detailed above. If the supplied credentials are invalid, you will be disconnected +immediately upon making the connection. + + + +**3.3 JSON-RPC Authenticate Command (Websocket-specific)**
+ +While the HTTP basic access authentication method is the preferred method, the ability to set HTTP headers from +websockets is not always available. In that case, you will need to use the [authenticate](#authenticate) JSON-RPC +method. + +The [authenticate](#authenticate) command must be the first command sent after connecting to the websocket. Sending any +other commands before authenticating, supplying invalid credentials, or attempting to authenticate again when already +authenticated will cause the websocket to be closed immediately. + + + +### 4. Command-line Utility + +pod comes with a separate utility named `podctl` which can be used to issue these RPC commands via HTTP POST requests to +pod after configuring it with the information in the [Authentication](#Authentication) section above. It can also be +used to communicate with any server/daemon/service which provides a JSON-RPC API compatible with the original +bitcoind/bitcoin-qt client. + + + +### 5. Standard Methods + + + +**5.1 Method Overview**
+ +The following is an overview of the RPC methods and their current status. Click the method name for further details such +as parameter and return information. + +|#|Method|Safe for limited user?|Description| +|---|------|----------|-----------| +|1|[addnode](#addnode)|N|Attempts to add or remove a persistent peer.| +|2|[createrawtransaction](#createrawtransaction)|Y|Returns a new transaction spending the provided inputs and sending to the provided addresses.| +|3|[decoderawtransaction](#decoderawtransaction)|Y|Returns a JSON object representing the provided serialized, hex-encoded transaction.| +|4|[decodescript](#decodescript)|Y|Returns a JSON object with information about the provided hex-encoded script.| +|5|[getaddednodeinfo](#getaddednodeinfo)|N|Returns information about manually added (persistent) peers.| +|6|[getbestblockhash](#getbestblockhash)|Y|Returns the hash of the of the best (most recent) block in the longest block chain.| +|7|[getblock](#getblock)|Y|Returns information about a block given its hash.| +|8|[getblockcount](#getblockcount)|Y|Returns the number of blocks in the longest block chain.| +|9|[getblockhash](#getblockhash)|Y|Returns hash of the block in best block chain at the given height.| +|10|[getblockheader](#getblockheader)|Y|Returns the block header of the block.| +|11|[getconnectioncount](#getconnectioncount)|N|Returns the number of active connections to other peers.| +|12|[getdifficulty](#getdifficulty)|Y|Returns the proof-of-work difficulty as a multiple of the minimum difficulty.| +|13|[getgenerate](#getgenerate)|N|Return if the server is set to generate coins (mine) or not.| +|14|[gethashespersec](#gethashespersec)|N|Returns a recent hashes per second performance measurement while generating coins (mining).| +|15|[getinfo](#getinfo)|Y|Returns a JSON object containing various state info.| +|16|[getmempoolinfo](#getmempoolinfo)|N|Returns a JSON object containing mempool-related information.| +|17|[getmininginfo](#getmininginfo)|N|Returns a JSON object containing mining-related information.| +|18|[getnettotals](#getnettotals)|Y|Returns a JSON object containing network traffic statistics.| +|19|[getnetworkhashps](#getnetworkhashps)|Y|Returns the estimated network hashes per second for the block heights provided by the parameters.| +|20|[getpeerinfo](#getpeerinfo)|N|Returns information about each connected network peer as an array of json objects.| +|21|[getrawmempool](#getrawmempool)|Y|Returns an array of hashes for all of the transactions currently in the memory pool.| +|22|[getrawtransaction](#getrawtransaction)|Y|Returns information about a transaction given its hash.| +|23|[help](#help)|Y|Returns a list of all commands or help for a specified command.| +|24|[ping](#ping)|N|Queues a ping to be sent to each connected peer.| +|25|[sendrawtransaction](#sendrawtransaction)|Y|Submits the serialized, hex-encoded transaction to the local peer and relays it to the network.
pod does not yet implement the `allowhighfees` parameter, so it has no effect| +|26|[setgenerate](#setgenerate) |N|Set the server to generate coins (mine) or not.
NOTE: Since pod does not have the wallet integrated to provide payment addresses, pod must be configured via the `--miningaddr` option to provide which payment addresses to pay created blocks to for this RPC to function.| +|27|[stop](#stop)|N|Shutdown pod.| +|28|[submitblock](#submitblock)|Y|Attempts to submit a new serialized, hex-encoded block to the network.| +|29|[validateaddress](#validateaddress)|Y|Verifies the given address is valid. NOTE: Since pod does not have a wallet integrated, pod will only return whether the address is valid or not.| +|30|[verifychain](#verifychain)|N|Verifies the block chain database.| + + + +**5.2 Method Details**
+ + + +| | | +|---|---| +|Method|addnode| +|Parameters|1. peer (string, required) - ip address and port of the peer to operate on
2. command (string, required) - `add` to add a persistent peer, `remove` to remove a persistent peer, or `onetry` to try a single connection to a peer| +|Description|Attempts to add or remove a persistent peer.| +|Returns|Nothing| + +[Return to Overview](#MethodOverview)
+ +--- + +
+ +| | | +|---|---| +|Method|createrawtransaction| +|Parameters|1. transaction inputs (JSON array, required) - json array of json objects
`[`
  `{`
    `"txid": "hash", (string, required) the hash of the input transaction`
    `"vout": n (numeric, required) the specific output of the input transaction to redeem`
  `}, ...`
`]`
2. addresses and amounts (JSON object, required) - json object with addresses as keys and amounts as values
`{`
  `"address": n.nnn (numeric, required) the address to send to as the key and the amount in DUO as the value`
  `, ...`
`}`
3. locktime (int64, optional, default=0) - specifies the transaction locktime. If non-zero, the inputs will also have their locktimes activated. | +|Description|Returns a new transaction spending the provided inputs and sending to the provided addresses.
The transaction inputs are not signed in the created transaction.
The `signrawtransaction` RPC command provided by wallet must be used to sign the resulting transaction.| +|Returns|`"transaction" (string) hex-encoded bytes of the serialized transaction`| +|Example Parameters|1. transaction inputs `[{"txid":"e6da89de7a6b8508ce8f371a3d0535b04b5e108cb1a6e9284602d3bfd357c018","vout":1}]`
2. addresses and amounts `{"13cgrTP7wgbZYWrY9BZ22BV6p82QXQT3nY": 0.49213337}`
3. locktime `0`| +|Example Return|`010000000118c057d3bfd3024628e9a6b18c105e4bb035053d1a378fce08856b7ade89dae6010000`
`0000ffffffff0199efee02000000001976a9141cb013db35ecccc156fdfd81d03a11c51998f99388`
`ac00000000`
** +Newlines added for display purposes. The actual return does not contain newlines.**| + +[Return to Overview](#MethodOverview)
+ +*** +
+ +| | | +|---|---| +|Method|decoderawtransaction| +|Parameters|1. data (string, required) - serialized, hex-encoded transaction| +|Description|Returns a JSON object representing the provided serialized, hex-encoded transaction.| +|Returns|`{ (json object)`
  `"txid": "hash", (string) the hash of the transaction`
  `"version": n, (numeric) the transaction version`
  `"locktime": n, (numeric) the transaction lock time`
  `"vin": [ (array of json objects) the transaction inputs as json objects`
  For coinbase transactions:
    `{ (json object)`
      `"coinbase": "data", (string) the hex-encoded bytes of the signature script`
      `"sequence": n, (numeric) the script sequence number`
    `}`
  For non-coinbase transactions:
    `{ (json object)`
      `"txid": "hash", (string) the hash of the origin transaction`
      `"vout": n, (numeric) the index of the output being redeemed from the origin transaction`
      `"scriptSig": { (json object) the signature script used to redeem the origin transaction`
        `"asm": "asm", (string) disassembly of the script`
        `"hex": "data", (string) hex-encoded bytes of the script`
      `}`
      `"sequence": n, (numeric) the script sequence number`
    `}, ...`
  `]`
  `"vout": [ (array of json objects) the transaction outputs as json objects`
    `{ (json object)`
      `"value": n, (numeric) the value in DUO`
      `"n": n, (numeric) the index of this transaction output`
      `"scriptPubKey": { (json object) the public key script used to pay coins`
        `"asm": "asm", (string) disassembly of the script`
        `"hex": "data", (string) hex-encoded bytes of the script`
        `"reqSigs": n, (numeric) the number of required signatures`
        `"type": "scripttype" (string) the type of the script (e.g. 'pubkeyhash')`
        `"addresses": [ (json array of string) the bitcoin addresses associated with this output`
          `"bitcoinaddress", (string) the bitcoin address`
          `...`
        `]`
      `}`
    `}, ...`
  `]`
`}`| +|Example Return|`{`
  `"txid": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",`
  `"version": 1,`
  `"locktime": 0,`
  `"vin": [`
  For coinbase transactions:
    `{ (json object)`
      `"coinbase": "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6...",`
      `"sequence": 4294967295,`
    `}`
  For non-coinbase transactions:
    `{`
      `"txid": "60ac4b057247b3d0b9a8173de56b5e1be8c1d1da970511c626ef53706c66be04",`
      `"vout": 0,`
      `"scriptSig": {`
        `"asm": "3046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8f0...",`
        `"hex": "493046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8...",`
      `}`
      `"sequence": 4294967295,`
    `}`
  `]`
  `"vout": [`
    `{`
      `"value": 50,`
      `"n": 0,`
      `"scriptPubKey": {`
        `"asm": "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4ce...",`
        `"hex": "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4...",`
        `"reqSigs": 1,`
        `"type": "pubkey"`
        `"addresses": [`
          `"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",`
        `]`
      `}`
    `}`
  `]`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|decodescript| +|Parameters|1. script (string, required) - hex-encoded script| +|Description|Returns a JSON object with information about the provided hex-encoded script.| +|Returns|`{ (json object)`
  `"asm": "asm", (string) disassembly of the script`
  `"reqSigs": n, (numeric) the number of required signatures`
  `"type": "scripttype", (string) the type of the script (e.g. 'pubkeyhash')`
  `"addresses": [ (json array of string) the bitcoin addresses associated with this script`
    `"bitcoinaddress", (string) the bitcoin address`
    `...`
  `]`
  `"p2sh": "scripthash", (string) the script hash for use in pay-to-script-hash transactions`
`}`| +|Example Return|`{`
  `"asm": "OP_DUP OP_HASH160 b0a4d8a91981106e4ed85165a66748b19f7b7ad4 OP_EQUALVERIFY OP_CHECKSIG",`
  `"reqSigs": 1,`
  `"type": "pubkeyhash",`
  `"addresses": [`
    `"1H71QVBpzuLTNUh5pewaH3UTLTo2vWgcRJ"`
  `]`
  `"p2sh": "359b84ff799f48231990ff0298206f54117b08b6"`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getaddednodeinfo| +|Parameters|1. dns (boolean, required) - specifies whether the returned data is a JSON object including DNS and connection information, or just a list of added peers
2. node (string, optional) - only return information about this specific peer instead of all added peers.| +|Description|Returns information about manually added (persistent) peers.| +|Returns (dns=false)|`["ip:port", ...]`| +|Returns (dns=true)|`[ (json array of objects)`
  `{ (json object)`
    `"addednode": "ip_or_domain", (string) the ip address or domain of the added peer`
    `"connected": true or false, (boolean) whether or not the peer is currently connected`
    `"addresses": [ (json array or objects) DNS lookup and connection information about the peer`
      `{ (json object)`
        `"address": "ip", (string) the ip address for this DNS entry`
        `"connected": "inbound/outbound/false" (string) the connection 'direction' (if connected)`
      `}, ...`
    `]`
  `}, ...`
`]`| +|Example Return (dns=false)|`["192.168.0.10:11047", "mydomain.org:11047"]`| +|Example Return (dns=true)|`[`
  `{`
    `"addednode": "mydomain.org:11047",`
    `"connected": true,`
    `"addresses": [`
      `{`
        `"address": "1.2.3.4",`
        `"connected": "outbound"`
      `},`
      `{`
        `"address": "5.6.7.8",`
        `"connected": "false"`
      `}`
    `]`
  `}`
`]`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getbestblockhash| +|Parameters|None| +|Description|Returns the hash of the of the best (most recent) block in the longest block chain.| +|Returns|string| +|Example Return|`0000000000000001f356adc6b29ab42b59f913a396e170f80190dba615bd1e60`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getblock| +|Parameters|1. block hash (string, required) - the hash of the block
2. verbose (boolean, optional, default=true) - specifies the block is returned as a JSON object instead of hex-encoded string
3. verbosetx (boolean, optional, default=false) - specifies that each transaction is returned as a JSON object and only applies if the `verbose` flag is true.** +This parameter is a pod extension**| +|Description|Returns information about a block given its hash.| +|Returns (verbose=false)|`"data" (string) hex-encoded bytes of the serialized block`| +|Returns (verbose=true, verbosetx=false)|`{ (json object)`
  `"hash": "blockhash", (string) the hash of the block (same as provided)`
  `"confirmations": n, (numeric) the number of confirmations`
  `"strippedsize", n (numeric) the size of the block without witness data`
  `"size": n, (numeric) the size of the block`
  `"weight": n, (numeric) value of the weight metric`
  `"height": n, (numeric) the height of the block in the block chain`
  `"version": n, (numeric) the block version`
  `"merkleroot": "hash", (string) root hash of the merkle tree`
  `"tx": [ (json array of string) the transaction hashes`
    `"transactionhash", (string) hash of the parent transaction`
    `...`
  `]`
  `"time": n, (numeric) the block time in seconds since 1 Jan 1970 GMT`
  `"nonce": n, (numeric) the block nonce`
  `"bits", n, (numeric) the bits which represent the block difficulty`
  `difficulty: n.nn, (numeric) the proof-of-work difficulty as a multiple of the minimum difficulty`
  `"previousblockhash": "hash", (string) the hash of the previous block`
  `"nextblockhash": "hash", (string) the hash of the next block (only if there is one)`
`}`| +|Returns (verbose=true, verbosetx=true)|`{ (json object)`
  `"hash": "blockhash", (string) the hash of the block (same as provided)`
  `"confirmations": n, (numeric) the number of confirmations`
  `"strippedsize", n (numeric) the size of the block without witness data`
  `"size": n, (numeric) the size of the block`
  `"weight": n, (numeric) value of the weight metric`
  `"height": n, (numeric) the height of the block in the block chain`
  `"version": n, (numeric) the block version`
  `"merkleroot": "hash", (string) root hash of the merkle tree`
  `"rawtx": [ (array of json objects) the transactions as json objects`
    `(see getrawtransaction json object details)`
  `]`
  `"time": n, (numeric) the block time in seconds since 1 Jan 1970 GMT`
  `"nonce": n, (numeric) the block nonce`
  `"bits", n, (numeric) the bits which represent the block difficulty`
  `difficulty: n.nn, (numeric) the proof-of-work difficulty as a multiple of the minimum difficulty`
  `"previousblockhash": "hash", (string) the hash of the previous block`
  `"nextblockhash": "hash", (string) the hash of the next block`
`}`| +|Example Return (verbose=false)|`"010000000000000000000000000000000000000000000000000000000000000000000000`
`3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49`
`ffff001d1dac2b7c01010000000100000000000000000000000000000000000000000000`
`00000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f`
`4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f`
`6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104`
`678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f`
`4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"`
** +Newlines added for display purposes. The actual return does not contain newlines.**| +|Example Return (verbose=true, verbosetx=false)|`{`
  `"hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",`
  `"confirmations": 277113,`
  `"size": 285,`
  `"height": 0,`
  `"version": 1,`
  `"merkleroot": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",`
  `"tx": [`
    `"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"`
  `],`
  `"time": 1231006505,`
  `"nonce": 2083236893,`
  `"bits": "1d00ffff",`
  `"difficulty": 1,`
  `"previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",`
  `"nextblockhash": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getblockcount| +|Parameters|None| +|Description|Returns the number of blocks in the longest block chain.| +|Returns|numeric| +|Example Return|`276820`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getblockhash| +|Parameters|1. block height (numeric, required)| +|Description|Returns hash of the block in best block chain at the given height.| +|Returns|string| +|Example Return|`000000000000000096579458d1c0f1531fcfc58d57b4fce51eb177d8d10e784d`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getblockheader| +|Parameters|1. block hash (string, required) - the hash of the block
2. verbose (boolean, optional, default=true) - specifies the block header is returned as a JSON object instead of a hex-encoded string| +|Description|Returns hex-encoded bytes of the serialized block header.| +|Returns (verbose=false)|`"data" (string) hex-encoded bytes of the serialized block`| +|Returns (verbose=true)|`{ (json object)`
  `"hash": "blockhash", (string) the hash of the block (same as provided)`
  `"confirmations": n, (numeric) the number of confirmations`
  `"height": n, (numeric) the height of the block in the block chain`
  `"version": n, (numeric) the block version`
  `"merkleroot": "hash", (string) root hash of the merkle tree`
  `"time": n, (numeric) the block time in seconds since 1 Jan 1970 GMT`
  `"nonce": n, (numeric) the block nonce`
  `"bits": n, (numeric) the bits which represent the block difficulty`
  `"difficulty": n.nn, (numeric) the proof-of-work difficulty as a multiple of the minimum difficulty`
  `"previousblockhash": "hash", (string) the hash of the previous block`
  `"nextblockhash": "hash", (string) the hash of the next block (only if there is one)`
`}`| +|Example Return (verbose=false)|`"0200000035ab154183570282ce9afc0b494c9fc6a3cfea05aa8c1add2ecc564900000000`
`38ba3d78e4500a5a7570dbe61960398add4410d278b21cd9708e6d9743f374d544fc0552`
`27f1001c29c1ea3b"`
** +Newlines added for display purposes. The actual return does not contain newlines.**| +|Example Return (verbose=true)|`{`
  `"hash": "00000000009e2958c15ff9290d571bf9459e93b19765c6801ddeccadbb160a1e",`
  `"confirmations": 392076,`
  `"height": 100000,`
  `"version": 2,`
  `"merkleroot": "d574f343976d8e70d91cb278d21044dd8a396019e6db70755a0a50e4783dba38",`
  `"time": 1376123972,`
  `"nonce": 1005240617,`
  `"bits": "1c00f127",`
  `"difficulty": 271.75767393,`
  `"previousblockhash": "000000004956cc2edd1a8caa05eacfa3c69f4c490bfc9ace820257834115ab35",`
  `"nextblockhash": "0000000000629d100db387f37d0f37c51118f250fb0946310a8c37316cbc4028"`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getconnectioncount| +|Parameters|None| +|Description|Returns the number of active connections to other peers| +|Returns|numeric| +|Example Return|`8`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getdifficulty| +|Parameters|None| +|Description|Returns the proof-of-work difficulty as a multiple of the minimum difficulty.| +|Returns|numeric| +|Example Return|`1180923195.260000`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getgenerate| +|Parameters|None| +|Description|Return if the server is set to generate coins (mine) or not.| +|Returns|`false` (boolean)| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|gethashespersec| +|Parameters|None| +|Description|Returns a recent hashes per second performance measurement while generating coins (mining).| +|Returns|`0` (numeric)| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getinfo| +|Parameters|None| +|Description|Returns a JSON object containing various state info.| +|Notes|NOTE: Since pod does NOT contain wallet functionality, wallet-related fields are not returned. See getinfo in btcwallet for a version which includes that information.| +|Returns|`{ (json object)`
  `"version": n, (numeric) the version of the server`
  `"protocolversion": n, (numeric) the latest supported protocol version`
  `"blocks": n, (numeric) the number of blocks processed`
  `"timeoffset": n, (numeric) the time offset`
  `"connections": n, (numeric) the number of connected peers`
  `"proxy": "host:port", (string) the proxy used by the server`
  `"difficulty": n.nn, (numeric) the current target difficulty`
  `"testnet": true or false, (boolean) whether or not server is using testnet`
  `"relayfee": n.nn, (numeric) the minimum relay fee for non-free transactions in DUO/KB`
`}`| +|Example Return|`{`
  `"version": 70000`
  `"protocolversion": 70001, `
  `"blocks": 298963,`
  `"timeoffset": 0,`
  `"connections": 17,`
  `"proxy": "",`
  `"difficulty": 8000872135.97,`
  `"testnet": false,`
  `"relayfee": 0.00001,`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getmempoolinfo| +|Parameters|None| +|Description|Returns a JSON object containing mempool-related information.| +|Returns|`{ (json object)`
  `"bytes": n, (numeric) size in bytes of the mempool`
  `"size": n, (numeric) number of transactions in the mempool`
`}`| +Example Return|`{`
  `"bytes": 310768,`
  `"size": 157,`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getmininginfo| +|Parameters|None| +|Description|Returns a JSON object containing mining-related information.| +|Returns|`{ (json object)`
  `"blocks": n, (numeric) latest best block`
  `"currentblocksize": n, (numeric) size of the latest best block`
  `"currentblockweight": n, (numeric) weight of the latest best block`
  `"currentblocktx": n, (numeric) number of transactions in the latest best block`
  `"difficulty": n.nn, (numeric) current target difficulty`
  `"errors": "errors", (string) any current errors`
  `"generate": true or false, (boolean) whether or not server is set to generate coins`
  `"genproclimit": n, (numeric) number of processors to use for coin generation (-1 when disabled)`
  `"hashespersec": n, (numeric) recent hashes per second performance measurement while generating coins`
  `"networkhashps": n, (numeric) estimated network hashes per second for the most recent blocks`
  `"pooledtx": n, (numeric) number of transactions in the memory pool`
  `"testnet": true or false, (boolean) whether or not server is using testnet`
`}`| +|Example Return|`{`
  `"blocks": 236526,`
  `"currentblocksize": 185,`
  `"currentblockweight": 740,`
  `"currentblocktx": 1,`
  `"difficulty": 256,`
  `"errors": "",`
  `"generate": false,`
  `"genproclimit": -1,`
  `"hashespersec": 0,`
  `"networkhashps": 33081554756,`
  `"pooledtx": 8,`
  `"testnet": true,`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getnettotals| +|Parameters|None| +|Description|Returns a JSON object containing network traffic statistics.| +|Returns|`{`
  `"totalbytesrecv": n, (numeric) total bytes received`
  `"totalbytessent": n, (numeric) total bytes sent`
  `"timemillis": n (numeric) number of milliseconds since 1 Jan 1970 GMT`
`}`| +|Example Return|`{`
  `"totalbytesrecv": 1150990,`
  `"totalbytessent": 206739,`
  `"timemillis": 1391626433845`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getnetworkhashps| +|Parameters|1. blocks (numeric, optional, default=120) - The number of blocks, or -1 for blocks since last difficulty change
2. height (numeric, optional, default=-1) - Perform estimate ending with this height or -1 for current best chain block height| +|Description|Returns the estimated network hashes per second for the block heights provided by the parameters.| +|Returns|numeric| +|Example Return|`6573971939`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getpeerinfo| +|Parameters|None| +|Description|Returns data about each connected network peer as an array of json objects.| +|Returns|`[`
  `{`
    `"addr": "host:port", (string) the ip address and port of the peer`
    `"services": "00000001", (string) the services supported by the peer`
    `"lastrecv": n, (numeric) time the last message was received in seconds since 1 Jan 1970 GMT`
    `"lastsend": n, (numeric) time the last message was sent in seconds since 1 Jan 1970 GMT`
    `"bytessent": n, (numeric) total bytes sent`
    `"bytesrecv": n, (numeric) total bytes received`
    `"conntime": n, (numeric) time the connection was made in seconds since 1 Jan 1970 GMT`
    `"pingtime": n, (numeric) number of microseconds the last ping took`
    `"pingwait": n, (numeric) number of microseconds a queued ping has been waiting for a response`
    `"version": n, (numeric) the protocol version of the peer`
    `"subver": "useragent", (string) the user agent of the peer`
    `"inbound": true_or_false, (boolean) whether or not the peer is an inbound connection`
    `"startingheight": n, (numeric) the latest block height the peer knew about when the connection was established`
    `"currentheight": n, (numeric) the latest block height the peer is known to have relayed since connected`
    `"syncnode": true_or_false, (boolean) whether or not the peer is the sync peer`
  `}, ...`
`]`| +|Example Return|`[`
  `{`
    `"addr": "178.172.xxx.xxx:11047",`
    `"services": "00000001",`
    `"lastrecv": 1388183523,`
    `"lastsend": 1388185470,`
    `"bytessent": 287592965,`
    `"bytesrecv": 780340,`
    `"conntime": 1388182973,`
    `"pingtime": 405551,`
    `"pingwait": 183023,`
    `"version": 70001,`
    `"subver": "/pod:0.4.0/",`
    `"inbound": false,`
    `"startingheight": 276921,`
    `"currentheight": 276955,`
    `"syncnode": true,`
  `}`
`]`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getrawtransaction| +|Parameters|1. transaction hash (string, required) - the hash of the transaction
2. verbose (int, optional, default=0) - specifies the transaction is returned as a JSON object instead of hex-encoded string| +|Description|Returns information about a transaction given its hash.| +|Returns (verbose=0)|`"data" (string) hex-encoded bytes of the serialized transaction`| +|Returns (verbose=1)|`{ (json object)`
  `"hex": "data", (string) hex-encoded transaction`
  `"txid": "hash", (string) the hash of the transaction`
  `"version": n, (numeric) the transaction version`
  `"locktime": n, (numeric) the transaction lock time`
  `"vin": [ (array of json objects) the transaction inputs as json objects`
  For coinbase transactions:
    `{ (json object)`
      `"coinbase": "data", (string) the hex-encoded bytes of the signature script`
      `"sequence": n, (numeric) the script sequence number`
    `"txinwitness": “data", (string) the witness stack for the input`
    `}`
  For non-coinbase transactions:
    `{ (json object)`
      `"txid": "hash", (string) the hash of the origin transaction`
      `"vout": n, (numeric) the index of the output being redeemed from the origin transaction`
      `"scriptSig": { (json object) the signature script used to redeem the origin transaction`
        `"asm": "asm", (string) disassembly of the script`
        `"hex": "data", (string) hex-encoded bytes of the script`
      `}`
      `"sequence": n, (numeric) the script sequence number`
    `"txinwitness": “data", (string) the witness stack for the input`
    `}, ...`
  `]`
  `"vout": [ (array of json objects) the transaction outputs as json objects`
    `{ (json object)`
      `"value": n, (numeric) the value in DUO`
      `"n": n, (numeric) the index of this transaction output`
      `"scriptPubKey": { (json object) the public key script used to pay coins`
        `"asm": "asm", (string) disassembly of the script`
        `"hex": "data", (string) hex-encoded bytes of the script`
        `"reqSigs": n, (numeric) the number of required signatures`
        `"type": "scripttype" (string) the type of the script (e.g. 'pubkeyhash')`
        `"addresses": [ (json array of string) the bitcoin addresses associated with this output`
          `"bitcoinaddress", (string) the bitcoin address`
          `...`
        `]`
      `}`
    `}, ...`
  `]`
`}`| +|Example Return (verbose=0)|`"010000000104be666c7053ef26c6110597dad1c1e81b5e6be53d17a8b9d0b34772054bac60000000`
`008c493046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8f`
`022100fbce8d84fcf2839127605818ac6c3e7a1531ebc69277c504599289fb1e9058df0141045a33`
`76eeb85e494330b03c1791619d53327441002832f4bd618fd9efa9e644d242d5e1145cb9c2f71965`
`656e276633d4ff1a6db5e7153a0a9042745178ebe0f5ffffffff0280841e00000000001976a91406`
`f1b6703d3f56427bfcfd372f952d50d04b64bd88ac4dd52700000000001976a9146b63f291c295ee`
`abd9aee6be193ab2d019e7ea7088ac00000000`
** +Newlines added for display purposes. The actual return does not contain newlines.**| +|Example Return (verbose=1)|`{`
  `"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000f...",`
  `"txid": "90743aad855880e517270550d2a881627d84db5265142fd1e7fb7add38b08be9",`
  `"version": 1,`
  `"locktime": 0,`
  `"vin": [`
  For coinbase transactions:
    `{ (json object)`
      `"coinbase": "03708203062f503253482f04066d605108f800080100000ea2122f6f7a636f696e4065757374726174756d2f",`
      `"sequence": 0,`
    `}`
  For non-coinbase transactions:
    `{`
      `"txid": "60ac4b057247b3d0b9a8173de56b5e1be8c1d1da970511c626ef53706c66be04",`
      `"vout": 0,`
      `"scriptSig": {`
        `"asm": "3046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8f0...",`
        `"hex": "493046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8...",`
      `}`
      `"sequence": 4294967295,`
    `}`
  `]`
  `"vout": [`
    `{`
      `"value": 25.1394,`
      `"n": 0,`
      `"scriptPubKey": {`
        `"asm": "OP_DUP OP_HASH160 ea132286328cfc819457b9dec386c4b5c84faa5c OP_EQUALVERIFY OP_CHECKSIG",`
        `"hex": "76a914ea132286328cfc819457b9dec386c4b5c84faa5c88ac",`
        `"reqSigs": 1,`
        `"type": "pubkeyhash"`
        `"addresses": [`
          `"1NLg3QJMsMQGM5KEUaEu5ADDmKQSLHwmyh",`
        `]`
      `}`
    `}`
  `]`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|help| +|Parameters|1. command (string, optional) - the command to get help for| +|Description|Returns a list of all commands or help for a specified command.
When no `command` parameter is specified, a list of avaialable commands is returned
When `command` is a valid method, the help text for that method is returned.| +|Returns|string| +|Example Return|getblockcount
Returns a numeric for the number of blocks in the longest block chain.| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|ping| +|Parameters|None| +|Description|Queues a ping to be sent to each connected peer.
Ping times are provided by [getpeerinfo](#getpeerinfo) via the `pingtime` and `pingwait` fields.| +|Returns|Nothing| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getrawmempool| +|Parameters|1. verbose (boolean, optional, default=false)| +|Description|Returns an array of hashes for all of the transactions currently in the memory pool.
The `verbose` flag specifies that each transaction is returned as a JSON object.| +|Notes|Since pod does not perform any mining, the priority related fields `startingpriority` and `currentpriority` that are available when the `verbose` flag is set are always 0.| +|Returns (verbose=false)|`[ (json array of string)`
  `"transactionhash", (string) hash of the transaction`
  `...`
`]`| +|Returns (verbose=true)|`{ (json object)`
  `"transactionhash": { (json object)`
    `"size": n, (numeric) transaction size in bytes`
    `"vsize": n, (numeric) transaction virtual size`
    `"fee" : n, (numeric) transaction fee in bitcoins`
    `"time": n, (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT`
    `"height": n, (numeric) block height when transaction entered the pool`
    `"startingpriority": n, (numeric) priority when transaction entered the pool`
    `"currentpriority": n, (numeric) current priority`
    `"depends": [ (json array) unconfirmed transactions used as inputs for this transaction`
      `"transactionhash", (string) hash of the parent transaction`
      `...`
    `]`
  `}, ...`
`}`| +|Example Return (verbose=false)|`[`
  `"3480058a397b6ffcc60f7e3345a61370fded1ca6bef4b58156ed17987f20d4e7",`
  `"cbfe7c056a358c3a1dbced5a22b06d74b8650055d5195c1c2469e6b63a41514a"`
`]`| +|Example Return (verbose=true)|`{`
  `"1697a19cede08694278f19584e8dcc87945f40c6b59a942dd8906f133ad3f9cc": {`
    `"size": 226,`
    `"fee" : 0.0001,`
    `"time": 1387992789,`
    `"height": 276836,`
    `"startingpriority": 0,`
    `"currentpriority": 0,`
    `"depends": [`
      `"aa96f672fcc5a1ec6a08a94aa46d6b789799c87bd6542967da25a96b2dee0afb",`
    `]`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|setgenerate| +|Parameters|1. generate (boolean, required) - `true` to enable generation, `false` to disable it
2. genproclimit (numeric, optional) - the number of processors (cores) to limit generation to or `-1` for default| +|Description|Set the server to generate coins (mine) or not.| +|Notes|NOTE: Since pod does not have the wallet integrated to provide payment addresses, pod must be configured via the `--miningaddr` option to provide which payment addresses to pay created blocks to for this RPC to function.| +|Returns|Nothing| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|sendrawtransaction| +|Parameters|1. signedhex (string, required) serialized, hex-encoded signed transaction
2. allowhighfees (boolean, optional, default=false) whether or not to allow insanely high fees| +|Description|Submits the serialized, hex-encoded transaction to the local peer and relays it to the network.| +|Notes|pod does not yet implement the `allowhighfees` parameter, so it has no effect| +|Returns|`"hash" (string) the hash of the transaction`| +|Example Return|`"1697a19cede08694278f19584e8dcc87945f40c6b59a942dd8906f133ad3f9cc"`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|submitblock| +|Parameters|1. data (string, required) serialized, hex-encoded block
2. params (json object, optional, default=nil) this parameter is currently ignored| +|Description|Attempts to submit a new serialized, hex-encoded block to the network.| +|Returns (success)|Success: Nothing
Failure: `"rejected: reason"` (string)| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|stop| +|Parameters|None| +|Description|Shutdown pod.| +|Returns|`"pod stopping."` (string)| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|validateaddress| +|Parameters|1. address (string, required) - bitcoin address| +|Description|Verify an address is valid.| +|Returns|`{ (json object)`
  `"isvalid": true or false, (bool) whether or not the address is valid.`
  `"address": "bitcoinaddress", (string) the bitcoin address validated.`
}| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|verifychain| +|Parameters|1. checklevel (numeric, optional, default=3) - how in-depth the verification is (0=least amount of checks, higher levels are clamped to the highest supported level)
2. numblocks (numeric, optional, default=288) - the number of blocks starting from the end of the chain to verify| +|Description|Verifies the block chain database.
The actual checks performed by the `checklevel` parameter is implementation specific. For pod this is:
`checklevel=0` - Look up each block and ensure it can be loaded from the database.
`checklevel=1` - Perform basic context-free sanity checks on each block.| +|Notes|Pod currently only supports `checklevel` 0 and 1, but the default is still 3 for compatibility. Per the information in the Parameters section above, higher levels are automatically clamped to the highest supported level, so this means the default is effectively 1 for pod.| +|Returns|`true` or `false` (boolean)| +|Example Return|`true`| + +[Return to Overview](#MethodOverview)
+ +
+ +### 6. Extension Methods + + + +**6.1 Method Overview**
+ +The following is an overview of the RPC methods which are implemented by pod, but not the original bitcoind client. +Click the method name for further details such as parameter and return information. + +|#|Method|Safe for limited user?|Description| +|---|------|----------|-----------| +|1|[debuglevel](#debuglevel)|N|Dynamically changes the debug logging level.| +|2|[getbestblock](#getbestblock)|Y|Get block height and hash of best block in the main chain.|None| +|3|[getcurrentnet](#getcurrentnet)|Y|Get bitcoin network pod is running on.|None| +|4|[searchrawtransactions](#searchrawtransactions)|Y|Query for transactions related to a particular address.|None| +|5|[node](#node)|N|Attempts to add or remove a peer. |None| +|6|[generate](#generate)|N|When in simnet or regtest mode, generate a set number of blocks. |None| +|7|[version](#version)|Y|Returns the JSON-RPC API version.| +|8|[getheaders](#getheaders)|Y|Returns block headers starting with the first known block hash from the request.| + + + +**6.2 Method Details**
+ + + +| | | +|---|---| +|Method|debuglevel| +|Parameters|1. _levelspec_ (string)| +|Description|Dynamically changes the debug logging level.
The levelspec can either a debug level or of the form `=,=,...`
The valid debug levels are `trace`, `debug`, `info`, `warn`, `error`, and `critical`.
The valid subsystems are `AMGR`, `ADXR`, `BCDB`, `BMGR`, `NODE`, `CHAN`, `DISC`, `PEER`, `RPCS`, `SCRP`, `SRVR`, and `TXMP`.
Additionally, the special keyword `show` can be used to get a list of the available subsystems.| +|Returns|string| +|Example Return|`Done.`| +|Example `show` Return|`Supported subsystems [AMGR ADXR BCDB BMGR NODE CHAN DISC PEER RPCS SCRP SRVR TXMP]`| + +[Return to Overview](#ExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getbestblock| +|Parameters|None| +|Description|Get block height and hash of best block in the main chain.| +|Returns|`{ (json object)`
 `"hash": "data", (string) the hex-encoded bytes of the best block hash`
 `"height": n (numeric) the block height of the best block`
`}`| + +[Return to Overview](#ExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getcurrentnet| +|Parameters|None| +|Description|Get bitcoin network pod is running on.| +|Returns|numeric| +|Example Return|`3652501241` (mainnet)
`118034699` (testnet3)| + +[Return to Overview](#ExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|searchrawtransactions| +|Parameters|1. address (string, required) - bitcoin address
2. verbose (int, optional, default=true) - specifies the transaction is returned as a JSON object instead of hex-encoded string
3. skip (int, optional, default=0) - the number of leading transactions to leave out of the final response
4. count (int, optional, default=100) - the maximum number of transactions to return
5. vinextra (int, optional, default=0) - Specify that extra data from previous output will be returned in vin
6. reverse (boolean, optional, default=false) - Specifies that the transactions should be returned in reverse chronological order| +|Description|Returns raw data for transactions involving the passed address. Returned transactions are pulled from both the database, and transactions currently in the mempool. Transactions pulled from the mempool will have the `"confirmations"` field set to 0. Usage of this RPC requires the optional `--addrindex` flag to be activated, otherwise all responses will simply return with an error stating the address index has not yet been built up. Similarly, until the address index has caught up with the current best height, all requests will return an error response in order to avoid serving stale data.| +|Returns (verbose=0)|`[ (json array of strings)`
   `"serializedtx", ... hex-encoded bytes of the serialized transaction`
`]` | +|Returns (verbose=1)|`[ (array of json objects)`
   `{ (json object)`
  `"hex": "data", (string) hex-encoded transaction`
  `"txid": "hash", (string) the hash of the transaction`
  `"version": n, (numeric) the transaction version`
  `"locktime": n, (numeric) the transaction lock time`
  `"vin": [ (array of json objects) the transaction inputs as json objects`
  For coinbase transactions:
    `{ (json object)`
      `"coinbase": "data", (string) the hex-encoded bytes of the signature script`
      `"txinwitness": “data", (string) the witness stack for the input`
    `"sequence": n, (numeric) the script sequence number`
    `}`
  For non-coinbase transactions:
    `{ (json object)`
      `"txid": "hash", (string) the hash of the origin transaction`
      `"vout": n, (numeric) the index of the output being redeemed from the origin transaction`
      `"scriptSig": { (json object) the signature script used to redeem the origin transaction`
        `"asm": "asm", (string) disassembly of the script`
        `"hex": "data", (string) hex-encoded bytes of the script`
      `}`
      `"prevOut": { (json object) Data from the origin transaction output with index vout.`
        `"addresses": ["value",...], (array of string) previous output addresses`
        `"value": n.nnn, (numeric) previous output value`
      `}`
      `"txinwitness": “data", (string) the witness stack for the input`
    `"sequence": n, (numeric) the script sequence number`
    `}, ...`
  `]`
  `"vout": [ (array of json objects) the transaction outputs as json objects`
    `{ (json object)`
      `"value": n, (numeric) the value in DUO`
      `"n": n, (numeric) the index of this transaction output`
      `"scriptPubKey": { (json object) the public key script used to pay coins`
        `"asm": "asm", (string) disassembly of the script`
        `"hex": "data", (string) hex-encoded bytes of the script`
        `"reqSigs": n, (numeric) the number of required signatures`
        `"type": "scripttype" (string) the type of the script (e.g. 'pubkeyhash')`
        `"addresses": [ (json array of string) the bitcoin addresses associated with this output`
          `"address", (string) the bitcoin address`
          `...`
        `]`
      `}`
    `}, ...`
   `]`
   `"blockhash":"hash" Hash of the block the transaction is part of.`
   `"confirmations":n, Number of numeric confirmations of block.`
   `"time":t, Transaction time in seconds since the epoch.`
   `"blocktime":t, Block time in seconds since the epoch.`
`},...`
`]`| + +[Return to Overview](#ExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|node| +|Parameters|1. command (string, required) - `connect` to add a peer (defaults to temporary), `remove` to remove a persistent peer, or `disconnect` to remove all matching non-persistent peers
2. peer (string, required) - ip address and port, or ID of the peer to operate on
3. connection type (string, optional) - `perm` indicates the peer should be added as a permanent peer, `temp` indicates a connection should only be attempted once. | +|Description|Attempts to add or remove a peer.| +|Returns|Nothing| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|generate| +|Parameters|1. numblocks (int, required) - The number of blocks to generate | +|Description|When in simnet or regtest mode, generates `numblocks` blocks. If blocks arrive from elsewhere, they are built upon but don't count toward the number of blocks to generate. Only generated blocks are returned. This RPC call will exit with an error if the server is already CPU mining, and will prevent the server from CPU mining for another command while it runs. | +|Returns|`[ (json array of strings)`
   `"blockhash", ... hash of the generated block`
`]` | + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|version| +|Parameters|None| +|Description|Returns the version of the JSON-RPC API built into this release of pod.| +|Returns|`{ (json object)`
  `"podjsonrpcapi": {`
    `"versionstring": "x.y.z", (string) the version of the JSON-RPC API`
    `"major": x, (numeric) the major version of the JSON-RPC API`
    `"minor": y, (numeric) the minor version of the JSON-RPC API`
    `"patch": z, (numeric) the patch version of the JSON-RPC API`
    `"prerelease": "", (string) prerelease info for the JSON-RPC API`
    `"buildmetadata": "" (string) metadata about the server build`
  `}`
`}`| +|Example Return|`{`
  `"podjsonrpcapi": {`
    `"versionstring": "1.0.0",`
    `"major": 1, `
    `"minor": 0,`
    `"patch": 0,`
    `"prerelease": "",`
    `"buildmetadata": ""`
  `}`
`}`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|getheaders| +|Parameters|1. Block Locators (JSON array, required)
 `[ (json array of strings)`
  `"blocklocator", (string) the known block hash`
  `...`
 `]`
2. hashstop (string) - last desired block's hash| +|Description|Returns block headers starting with the first known block hash from the request.| +|Returns|`[ (json array of strings)`
  `"blockheader",`
  `...`
`]`| +|Example Return|`[`
  `"0000002099417930b2ae09feda10e38b58c0f6bb44b4d60fa33f0e000000000000000000d53...",`
  `"000000203ba25a173bfd24d09e0c76002a910b685ca297bd09a17b020000000000000000702..."`
`]`| + +[Return to Overview](#MethodOverview)
+ +*** + +
+ +### 7. Websocket Extension Methods (Websocket-specific) + + + +**7.1 Method Overview**
+ +The following is an overview of the RPC method requests available exclusively to Websocket clients. All of these RPC +methods are available to the limited user. Click the method name for further details such as parameter and return +information. |#|Method|Description|Notifications| |---|------|-----------|-------------| +|1|[authenticate](#authenticate)|Authenticate the connection against the username and passphrase configured for the RPC +server.
NOTE: This is only required if an HTTP Authorization header is not being used. +|None| |2|[notifyblocks](#notifyblocks)|Send notifications when a block is connected or disconnected from the best +chain.|[blockconnected](#blockconnected), [blockdisconnected](#blockdisconnected) +, [filteredblockconnected](#filteredblockconnected), and [filteredblockdisconnected](#filteredblockdisconnected)| +|3|[stopnotifyblocks](#stopnotifyblocks)|Cancel registered notifications for whenever a block is connected or +disconnected from the main (best) chain. |None| |4|[notifyreceived](#notifyreceived)|*DEPRECATED, for similar +functionality see [loadtxfilter](#loadtxfilter)*
Send notifications when a txout spends to an +address.|[recvtx](#recvtx) and [redeemingtx](#redeemingtx)| |5|[stopnotifyreceived](#stopnotifyreceived)|*DEPRECATED, +for similar functionality see [loadtxfilter](#loadtxfilter)*
Cancel registered notifications for when a txout +spends to any of the passed addresses.|None| |6|[notifyspent](#notifyspent)|*DEPRECATED, for similar functionality +see [loadtxfilter](#loadtxfilter)*
Send notification when a txout is spent.|[redeemingtx](#redeemingtx)| +|7|[stopnotifyspent](#stopnotifyspent)|*DEPRECATED, for similar functionality see [loadtxfilter](#loadtxfilter)*
+Cancel registered spending notifications for each passed outpoint.|None| |8|[rescan](#rescan)|*DEPRECATED, for similar +functionality see [rescanblocks](#rescanblocks)*
Rescan block chain for transactions to addresses and spent +transaction outpoints.|[recvtx](#recvtx), [redeemingtx](#redeemingtx), [rescanprogress](#rescanprogress), +and [rescanfinished](#rescanfinished) | |9|[notifynewtransactions](#notifynewtransactions)|Send notifications for all +new transactions as they are accepted into the mempool.|[txaccepted](#txaccepted) +or [txacceptedverbose](#txacceptedverbose)| |10|[stopnotifynewtransactions](#stopnotifynewtransactions)|Stop sending +either a txaccepted or a txacceptedverbose notification when a new transaction is accepted into the mempool.|None| +|11|[session](#session)|Return details regarding a websocket client's current connection.|None| +|12|[loadtxfilter](#loadtxfilter)|Load, add to, or reload a websocket client's transaction filter for mempool +transactions, new blocks and rescanblocks.|[relevanttxaccepted](#relevanttxaccepted)| |13|[rescanblocks](#rescanblocks) +|Rescan blocks for transactions matching the loaded transaction filter.|None| + + + +**7.2 Method Details**
+ + + +| | | +|---|---| +|Method|authenticate| +|Parameters|1. username (string, required)
2. passphrase (string, required)| +|Description|Authenticate the connection against the username and password configured for the RPC server.
Invoking any other method before authenticating with this command will close the connection.
NOTE: This is only required if an HTTP Authorization header is not being used.| +|Returns|Success: Nothing
Failure: Nothing (websocket disconnected)| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|notifyblocks| +|Notifications|[blockconnected](#blockconnected), [blockdisconnected](#blockdisconnected), [filteredblockconnected](#filteredblockconnected), and [filteredblockdisconnected](#filteredblockdisconnected)| +|Parameters|None| +|Description|Request notifications for whenever a block is connected or disconnected from the main (best) chain.
NOTE: If a client subscribes to both block and transaction (recvtx and redeemingtx) notifications, the blockconnected notification will be sent after all transaction notifications have been sent. This allows clients to know when all relevant transactions for a block have been received.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|stopnotifyblocks| +|Notifications|None| +|Parameters|None| +|Description|Cancel sending notifications for whenever a block is connected or disconnected from the main (best) chain.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|notifyreceived| +|Notifications|[recvtx](#recvtx) and [redeemingtx](#redeemingtx)| +|Parameters|1. Addresses (JSON array, required)
 `[ (json array of strings)`
  `"bitcoinaddress", (string) the bitcoin address`
  `...`
 `]`| +|Description|*DEPRECATED, for similar functionality +see [loadtxfilter](#loadtxfilter)*
Send a recvtx notification when a transaction added to mempool or appears in a newly-attached block contains a txout pkScript sending to any of the passed addresses. Matching outpoints are automatically registered for redeemingtx notifications.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|stopnotifyreceived| +|Notifications|None| +|Parameters|1. Addresses (JSON array, required)
 `[ (json array of strings)`
  `"bitcoinaddress", (string) the bitcoin address`
  `...`
 `]`| +|Description|*DEPRECATED, for similar functionality +see [loadtxfilter](#loadtxfilter)*
Cancel registered receive notifications for each passed address.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|notifyspent| +|Notifications|[redeemingtx](#redeemingtx)| +|Parameters|1. Outpoints (JSON array, required)
 `[ (JSON array)`
  `{ (JSON object)`
   `"hash":"data", (string) the hex-encoded bytes of the outpoint hash`
   `"index":n (numeric) the txout index of the outpoint`
  `},`
  `...`
 `]`| +|Description|*DEPRECATED, for similar functionality +see [loadtxfilter](#loadtxfilter)*
Send a redeemingtx notification when a transaction spending an outpoint appears in mempool (if relayed to this pod instance) and when such a transaction first appears in a newly-attached block.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|stopnotifyspent| +|Notifications|None| +|Parameters|1. Outpoints (JSON array, required)
 `[ (JSON array)`
  `{ (JSON object)`
   `"hash":"data", (string) the hex-encoded bytes of the outpoint hash`
   `"index":n (numeric) the txout index of the outpoint`
  `},`
  `...`
 `]`| +|Description|*DEPRECATED, for similar functionality +see [loadtxfilter](#loadtxfilter)*
Cancel registered spending notifications for each passed outpoint.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|rescan| +|Notifications|[recvtx](#recvtx), [redeemingtx](#redeemingtx), [rescanprogress](#rescanprogress), and [rescanfinished](#rescanfinished)| +|Parameters|1. BeginBlock (string, required) block hash to begin rescanning from
2. Addresses (JSON array, required)
 `[ (json array of strings)`
  `"bitcoinaddress", (string) the bitcoin address`
  `...`
 `]`
3. Outpoints (JSON array, required)
 `[ (JSON array)`
  `{ (JSON object)`
   `"hash":"data", (string) the hex-encoded bytes of the outpoint hash`
   `"index":n (numeric) the txout index of the outpoint`
  `},`
  `...`
 `]`
4. EndBlock (string, optional) hash of final block to rescan| +|Description|*DEPRECATED, for similar functionality +see [rescanblocks](#rescanblocks)*
Rescan block chain for transactions to addresses, starting at block BeginBlock and ending at EndBlock. The current known UTXO set for all passed addresses at height BeginBlock should included in the Outpoints argument. If EndBlock is omitted, the rescan continues through the best block in the main chain. Additionally, if no EndBlock is provided, the client is automatically registered for transaction notifications for all rescanned addresses and the final UTXO set. Rescan results are sent as recvtx and redeemingtx notifications. This call returns once the rescan completes.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|notifynewtransactions| +|Notifications|[txaccepted](#txaccepted) or [txacceptedverbose](#txacceptedverbose)| +|Parameters|1. verbose (boolean, optional, default=false) - specifies which type of notification to receive. If verbose is true, then the caller receives [txacceptedverbose](#txacceptedverbose), otherwise the caller receives [txaccepted](#txaccepted)| +|Description|Send either a [txaccepted](#txaccepted) or a [txacceptedverbose](#txacceptedverbose) notification when a new transaction is accepted into the mempool.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|stopnotifynewtransactions| +|Notifications|None| +|Parameters|None| +|Description|Stop sending either a [txaccepted](#txaccepted) or a [txacceptedverbose](#txacceptedverbose) notification when a new transaction is accepted into the mempool.| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|session| +|Notifications|None| +|Parameters|None| +|Description|Return a JSON object with details regarding a websocket client's current connection to the RPC server. This currently only includes the session ID, a random unsigned 64-bit integer that is created for each newly connected client. Session IDs may be used to verify that the current connection was not lost and subsequently reestablished.| +|Returns|`{ (json object)`
  `"sessionid": n (numeric) the session ID`
`}`| +|Example Return|`{`
  `"sessionid": 67089679842`
`}`| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|loadtxfilter| +|Notifications|[relevanttxaccepted](#relevanttxaccepted)| +|Parameters|1. Reload (boolean, required) - Load a new filter instead of adding data to an existing one
2. Addresses (JSON array, required) - Array of addresses to add to the transaction filter
3. Outpoints (JSON array, required) - Array of outpoints to add to the transaction filter| +|Description|Load, add to, or reload a websocket client's transaction filter for mempool transactions, new blocks and [rescanblocks](#rescanblocks).| +|Returns|Nothing| + +[Return to Overview](#WSExtMethodOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|rescanblocks| +|Notifications|None| +|Parameters|1. Blockhashes (JSON array, required) - List of hashes to rescan. Each next block must be a child of the previous.| +|Description|Rescan blocks for transactions matching the loaded transaction filter.| +|Returns|`[ (JSON array)`
  `{ (JSON object)`
    `"hash": "data", (string) Hash of the matching block.`
    `"transactions": [ (JSON array) List of matching transactions, serialized and hex-encoded.`
      `"serializedtx" (string) Serialized and hex-encoded transaction.`
    `]`
  `}`
`]`| +|Example Return|`[`
  `{`
    `"hash": "0000002099417930b2ae09feda10e38b58c0f6bb44b4d60fa33f0e000000000000000000d53...",`
    `"transactions": [`
      `"493046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8..."`
    `]`
  `}`
`]`| + +
+ +### 8. Notifications (Websocket-specific) + +pod uses standard JSON-RPC notifications to notify clients of changes, rather than requiring clients to poll pod for +updates. JSON-RPC notifications are a subset of requests, but do not contain an ID. The notification type is categorized +by the `method` field and additional details are sent as a JSON array in the `params` field. + + + +**8.1 Notification Overview**
+ +The following is an overview of the JSON-RPC notifications used for Websocket connections. Click the method name for +further details of the context(s) in which they are sent and their parameters. |#|Method|Description|Request| +|---|------|-----------|-------| |1|[blockconnected](#blockconnected)|*DEPRECATED, for similar functionality +see [filteredblockconnected](#filteredblockconnected)*
Block connected to the main +chain.|[notifyblocks](#notifyblocks)| |2|[blockdisconnected](#blockdisconnected)|*DEPRECATED, for similar functionality +see [filteredblockdisconnected](#filteredblockdisconnected)*
Block disconnected from the main +chain.|[notifyblocks](#notifyblocks)| |3|[recvtx](#recvtx)|*DEPRECATED, for similar functionality +see [relevanttxaccepted](#relevanttxaccepted) and [filteredblockconnected](#filteredblockconnected)*
Processed a +transaction output spending to a wallet address.|[notifyreceived](#notifyreceived) and [rescan](#rescan)| +|4|[redeemingtx](#redeemingtx)|*DEPRECATED, for similar functionality see [relevanttxaccepted](#relevanttxaccepted) +and [filteredblockconnected](#filteredblockconnected)*
Processed a transaction that spends a registered +outpoint.|[notifyspent](#notifyspent) and [rescan](#rescan)| |5|[txaccepted](#txaccepted)|Received a new transaction +after requesting simple notifications of all new transactions accepted into the +mempool.|[notifynewtransactions](#notifynewtransactions)| |6|[txacceptedverbose](#txacceptedverbose)|Received a new +transaction after requesting verbose notifications of all new transactions accepted into the +mempool.|[notifynewtransactions](#notifynewtransactions)| |7|[rescanprogress](#rescanprogress)|*DEPRECATED, +notifications not used by [rescanblocks](#rescanblocks)*
A rescan operation that is underway has made +progress.|[rescan](#rescan)| |8|[rescanfinished](#rescanfinished)|*DEPRECATED, notifications not used +by [rescanblocks](#rescanblocks)*
A rescan operation has completed.|[rescan](#rescan)| +|9|[relevanttxaccepted](#relevanttxaccepted)|A transaction matching the tx filter has been accepted into the +mempool.|[loadtxfilter](#loadtxfilter)| |10|[filteredblockconnected](#filteredblockconnected)|Block connected to the +main chain; contains any transactions that match the client's tx filter.|[notifyblocks](#notifyblocks) +, [loadtxfilter](#loadtxfilter)| |11|[filteredblockdisconnected](#filteredblockdisconnected)|Block disconnected from the +main chain.|[notifyblocks](#notifyblocks), [loadtxfilter](#loadtxfilter)| + + + +**8.2 Notification Details**
+ + + +| | | +|---|---| +|Method|blockconnected| +|Request|[notifyblocks](#notifyblocks)| +|Parameters|1. BlockHash (string) hex-encoded bytes of the attached block hash
2. BlockHeight (numeric) height of the attached block
3. BlockTime (numeric) unix time of the attached block| +|Description|*DEPRECATED, for similar functionality +see [filteredblockconnected](#filteredblockconnected)*
Notifies when a block has been added to the main chain. Notification is sent to all connected clients.| +|Example|Example blockconnected notification for mainnet block 280330 (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "blockconnected",`
 `"params":`
  `[`
   `"000000000000000004cbdfe387f4df44b914e464ca79838a8ab777b3214dbffd",`
   `280330,`
   `1389636265`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|blockdisconnected| +|Request|[notifyblocks](#notifyblocks)| +|Parameters|1. BlockHash (string) hex-encoded bytes of the disconnected block hash
2. BlockHeight (numeric) height of the disconnected block
3. BlockTime (numeric) unix time of the disconnected block| +|Description|*DEPRECATED, for similar functionality +see [filteredblockdisconnected](#filteredblockdisconnected)*
Notifies when a block has been removed from the main chain. Notification is sent to all connected clients.| +|Example|Example blockdisconnected notification for mainnet block 280330 (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "blockdisconnected",`
 `"params":`
  `[`
   `"000000000000000004cbdfe387f4df44b914e464ca79838a8ab777b3214dbffd",`
   `280330,`
   `1389636265`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|recvtx| +|Request|[rescan](#rescan) or [notifyreceived](#notifyreceived)| +|Parameters|1. Transaction (string) full transaction encoded as a hex string
2. Block details (object, optional) details about a block and the index of the transaction within a block, if the transaction is mined| +|Description|*DEPRECATED, for similar functionality see [relevanttxaccepted](#relevanttxaccepted) +and [filteredblockconnected](#filteredblockconnected)*
Notifies a client when a transaction is processed that contains at least a single output with a pkScript sending to a requested address. If multiple outputs send to requested addresses, a single notification is sent. If a mempool (unmined) transaction is processed, the block details object (second parameter) is excluded.| +|Example|Example recvtx notification for mainnet transaction 61d3696de4c888730cbe06b0ad8ecb6d72d6108e893895aa9bc067bd7eba3fad when processed by mempool (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "recvtx",`
 `"params":`
  `[`
   `"010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb737300000000..."`
  `],`
 `"id": null`
`}`
The recvtx notification for the same txout, after the transaction was mined into block 276425:
`{`
 `"jsonrpc": "1.0",`
 `"method": "recvtx",`
 `"params":`
  `[`
   `"010000000114d9ff358894c486b4ae11c2a8cf7851b1df64c53d2e511278eff17c22fb737300000000...",`
   `{`
    `"height": 276425,`
    `"hash": "000000000000000325474bb799b9e591f965ca4461b72cb7012b808db92bb2fc",`
    `"index": 684,`
    `"time": 1387737310`
   `}`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|redeemingtx| +|Requests|[notifyspent](#notifyspent) and [rescan](#rescan)| +|Parameters|1. Transaction (string) full transaction encoded as a hex string
2. Block details (object, optional) details about a block and the index of the transaction within a block, if the transaction is mined| +|Description|*DEPRECATED, for similar functionality see [relevanttxaccepted](#relevanttxaccepted) +and [filteredblockconnected](#filteredblockconnected)*
Notifies a client when an registered outpoint is spent by a transaction accepted to mempool and/or mined into a block.| +|Example|Example redeemingtx notification for mainnet outpoint 61d3696de4c888730cbe06b0ad8ecb6d72d6108e893895aa9bc067bd7eba3fad:0 after being spent by transaction 4ad0c16ac973ff675dec1f3e5f1273f1c45be2a63554343f21b70240a1e43ece (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "redeemingtx",`
 `"params":`
  `[`
   `"0100000003ad3fba7ebd67c09baa9538898e10d6726dcb8eadb006be0c7388c8e46d69d3610000000..."`
  `],`
 `"id": null`
`}`
The redeemingtx notification for the same txout, after the spending transaction was mined into block 279143:
`{`
 `"jsonrpc": "1.0",`
 `"method": "recvtx",`
 `"params":`
  `[`
   `"0100000003ad3fba7ebd67c09baa9538898e10d6726dcb8eadb006be0c7388c8e46d69d3610000000...",`
   `{`
    `"height": 279143,`
    `"hash": "00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4",`
    `"index": 6,`
    `"time": 1389115004`
   `}`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|txaccepted| +|Request|[notifynewtransactions](#notifynewtransactions)| +|Parameters|1. TxHash (string) hex-encoded bytes of the transaction hash
2. Amount (numeric) sum of the value of all the transaction outpoints| +|Description|Notifies when a new transaction has been accepted and the client has requested standard transaction details.| +|Example|Example txaccepted notification for mainnet transaction id "16c54c9d02fe570b9d41b518c0daefae81cc05c69bbe842058e84c6ed5826261" (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "txaccepted",`
 `"params":`
  `[`
   `"16c54c9d02fe570b9d41b518c0daefae81cc05c69bbe842058e84c6ed5826261",`
   `55838384`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|txacceptedverbose| +|Request|[notifynewtransactions](#notifynewtransactions)| +|Parameters|1. RawTx (json object) the transaction as a json object (see getrawtransaction json object details)| +|Description|Notifies when a new transaction has been accepted and the client has requested verbose transaction details.| +|Example|Example txacceptedverbose notification (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "txacceptedverbose",`
 `"params":`
  `[`
   `{`
    `"hex": "01000000010000000000000000000000000000000000000000000000000000000000000000f...",`
    `"txid": "90743aad855880e517270550d2a881627d84db5265142fd1e7fb7add38b08be9",`
    `"version": 1,`
    `"locktime": 0,`
    `"vin": [`
    For coinbase transactions:
      `{ (json object)`
        `"coinbase": "03708203062f503253482f04066d605108f800080100000ea2122f6f7a636f696e4065757374726174756d2f",`
        `"sequence": 0,`
      `}`
    For non-coinbase transactions:
      `{`
        `"txid": "60ac4b057247b3d0b9a8173de56b5e1be8c1d1da970511c626ef53706c66be04",`
        `"vout": 0,`
        `"scriptSig": {`
          `"asm": "3046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8f0...",`
          `"hex": "493046022100cb42f8df44eca83dd0a727988dcde9384953e830b1f8004d57485e2ede1b9c8...",`
        `}`
        `"sequence": 4294967295,`
      `}`
    `],`
    `"vout": [`
     `{`
      `"value": 25.1394,`
      `"n": 0,`
      `"scriptPubKey": {`
       `"asm": "OP_DUP OP_HASH160 ea132286328cfc819457b9dec386c4b5c84faa5c OP_EQUALVERIFY OP_CHECKSIG",`
       `"hex": "76a914ea132286328cfc819457b9dec386c4b5c84faa5c88ac",`
       `"reqSigs": 1,`
       `"type": "pubkeyhash"`
       `"addresses": [`
        `"1NLg3QJMsMQGM5KEUaEu5ADDmKQSLHwmyh",`
       `]`
     `}`
    `]`
   `}`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|rescanprogress| +|Request|[rescan](#rescan)| +|Parameters|1. Hash (string) hash of the last processed block
2. Height (numeric) height of the last processed block
3. Time (numeric) UNIX time of the last processed block| +|Description|*DEPRECATED, notifications not used +by [rescanblocks](#rescanblocks)*
Notifies a client with the current progress at periodic intervals when a long-running [rescan](#rescan) is underway.| +|Example|`{`
 `"jsonrpc": "1.0",`
 `"method": "rescanprogress",`
 `"params":`
  `[`
   `"0000000000000ea86b49e11843b2ad937ac89ae74a963c7edd36e0147079b89d",`
   `127213,`
   `1306533807`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|rescanfinished| +|Request|[rescan](#rescan)| +|Parameters|1. Hash (string) hash of the last rescanned block
2. Height (numeric) height of the last rescanned block
3. Time (numeric) UNIX time of the last rescanned block | +|Description|*DEPRECATED, notifications not used +by [rescanblocks](#rescanblocks)*
Notifies a client that the [rescan](#rescan) has completed and no further notifications will be sent.| +|Example|`{`
 `"jsonrpc": "1.0",`
 `"method": "rescanfinished",`
 `"params":`
  `[`
   `"0000000000000ea86b49e11843b2ad937ac89ae74a963c7edd36e0147079b89d",`
   `127213,`
   `1306533807`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|relevanttxaccepted| +|Request|[loadtxfilter](#loadtxfilter)| +|Parameters|1. Transaction (string) hex-encoded serialized transaction matching the client's filter loaded ith [loadtxfilter](#loadtxfilter)| +|Description|Notifies a client that a transaction matching the client's tx filter has been accepted into he mempool.| +|Example|Example `relevanttxaccepted` notification (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "relevanttxaccepted",`
 `"params": [`
  `"01000000014221abdcca25c8a3b0c044034875dece048c77d567a806f0c2e7e0f5e25a8f100..."`
 `],`
 `"id": null`
`}`| + +*** + +
+ +| | | +|---|---| +|Method|filteredblockconnected| +|Request|[notifyblocks](#notifyblocks), [loadtxfilter](#loadtxfilter)| +|Parameters|1. BlockHeight (numeric) height of the attached block
2. Header (string) hex-encoded serialized header of the attached block
3. Transactions (JSON array) hex-encoded serialized transactions matching the filter for the client connection loaded with [loadtxfilter](#loadtxfilter)| +|Description|Notifies when a block has been added to the main chain. Notification is sent to all connected clients.| +|Example|Example filteredblockconnected notification for mainnet block 280330 (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "filteredblockconnected",`
 `"params":`
  `[`
   `280330,`
   `"0200000052d1e8813f697293e41942aa230e7e4fcc44832d78a1372202000000000000006aa...",`
   `[`
    `"01000000014221abdcca25c8a3b0c044034875dece048c77d567a806f0c2e7e0f5e25a8f100..."`
   `]`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +*** + +
+ +| | | +|---|---| +|Method|filteredblockdisconnected| +|Request|[notifyblocks](#notifyblocks), [loadtxfilter](#loadtxfilter)| +|Parameters|1. BlockHeight (numeric) height of the disconnected block
2. Header (string) hex-encoded serialized header of the disconnected block| +|Description|Notifies when a block has been removed from the main chain. Notification is sent to all connected clients.| +|Example|Example blockdisconnected notification for mainnet block 280330 (newlines added for readability):
`{`
 `"jsonrpc": "1.0",`
 `"method": "blockdisconnected",`
 `"params":`
  `[`
   `280330,`
   `"0200000052d1e8813f697293e41942aa230e7e4fcc44832d78a1372202000000000000006aa..."`
  `],`
 `"id": null`
`}`| + +[Return to Overview](#NotificationOverview)
+ +
+ +### 9. Example Code + +This section provides example code for interacting with the JSON-RPC API in various languages. + +* [Go](#ExampleGoApp) + +* [node.js](#ExampleNodeJsCode) + + + +**9.1 Go** + +is section provides examples of using the RPC interface using Go and he +[ pcclient](https://github.com/p9c/p9/rpcclient) package. + +* [Using getblockcount to Retrieve the Current Block Height](#ExampleGetBlockCount) + +* [Using getblock to Retrieve the Genesis Block](#ExampleGetBlock) + +* [Using notifyblocks to Receive blockconnected and blockdisconnected Notifications (Websocket-specific)](#ExampleNotifyBlocks) + + + +**9.1.1 Using getblockcount to Retrieve the Current Block Height**
+ +The following is an example Go application which uses the [rpcclient](https://github.com/p9c/p9/rpcclient) package to +connect with a pod instance via Websockets, issues [getblockcount](#getblockcount) to retrieve the current block height, +and displays it. + +```Go +package main +import ( +"io/ioutil" +"log" +"path/filepath" + +"github.com/p9c/p9/btcutil" +"github.com/p9c/p9/pkg/rpc/client" + + + +) +func main( ) { + + // Load the certificate for the TLS connection which is automatically + // generated by pod when it starts the RPC server and doesn't already + // have one. + podHomeDir := btcutil.AppDataDir("pod", false) + certs, e := ioutil.ReadFile(filepath.Join(podHomeDir, "rpc.cert")) + if e != nil { + L.L.ftl.Ln(err) + } + // Create a new RPC client using websockets. Since this example is + // not long-lived, the connection will be closed as soon as the program + // exits. connCfg := pcclient.ConnConfig{ + Host: "local ost:11048", + + Endpoint: "ws", + User: "yourrpcuser", + Pass: "yourrpcpass", + Certificates: certs, + } client, e := pcclient.New(connCfg, nil) + if e != nil { L.ftl.Ln(err) + + } + defer client.Shutdown() + // Query the RPC server for the current block count and display it. + blockCount, e := client.GetBlockCount() + if e != nil { + L.L.ftl.Ln(err) + } + log.Printf("Block count: %d", blockCount) +} +``` + +Which results in: + +```bash +Block count: 276978 +``` + + + +**9.1.2 Using getblock to Retrieve the Genesis Block**
+ +The following is an example Go application which uses the [rpcclient](https://github.com/p9c/p9/rpcclient) package to +connect with a pod instance via Websockets, issues [getblock](#getblock) to retrieve information about the Genesis +block, and display a few details about it. + +```Go +package main +import ( + "github.com/p9c/p9/pkg/rpc/client" "git.parallelcoin.io/btcutil" "github.com/p9c/p9/pkg/chain/hash" + + "github.com/p9c/p9/pkg/chain/wire" + "io/ioutil" + "log" + "path/filepath" + "time" +) +func main( ) { + + // Load the certificate for the TLS connection which is automatically + // generated by pod when it starts the RPC server and doesn't already + // have one. + podHomeDir := btcutil.AppDataDir("pod", false) + certs, e := ioutil.ReadFile(filepath.Join(podHomeDir, "rpc.cert")) + if e != nil { + L.L.ftl.Ln(err) + } + // Create a new RPC client using websockets. Since this example is + // not long-lived, the connection will be closed as soon as the program + // exits. connCfg := pcclient.ConnConfig{ + Host: "local ost:21048", + + Endpoint: "ws", + User: "yourrpcuser", + Pass: "yourrpcpass", + Certificates: certs, + } client, e := pcclient.New(connCfg, nil) + if e != nil { L.ftl.Ln(err) + + } + defer client.Shutdown() + // Query the RPC server for the genesis block using the "getblock" + // command with the verbose flag set to true and the verboseTx flag + // set to false. + genesisHashStr := "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + blockHash, e := chainhash.NewHashFromStr(genesisHashStr) + if e != nil { + L.L.ftl.Ln(err) + } + block, e := client.GetBlockVerbose(blockHash, false) + if e != nil { + L.L.ftl.Ln(err) + } + // Display some details about the returned block. + log.Printf("Hash: %v\n", block.Hash) + log.Printf("Previous Block: %v\n", block.PreviousHash) + log.Printf("Next Block: %v\n", block.NextHash) + log.Printf("Merkle root: %v\n", block.MerkleRoot) + log.Printf("Timestamp: %v\n", time.Unix(block.Time, 0).UTC()) + log.Printf("Confirmations: %v\n", block.Confirmations) + log.Printf("Difficulty: %f\n", block.Difficulty) + log.Printf("Size (in bytes): %v\n", block.Size) + log.Printf("Num transactions: %v\n", len(block.Tx)) +} +``` + +Which results in: + +```bash +Hash: 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f +Previous Block: 0000000000000000000000000000000000000000000000000000000000000000 +Next Block: 00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048 +Merkle root: 4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b +Timestamp: 2009-01-03 18:15:05 +0000 UTC +Confirmations: 277290 +Difficulty: 1.000000 +Size (in bytes): 285 +Num transactions: 1 +``` + + + +**9.1.3 Using notifyblocks to Receive blockconnected and blockdisconnected + +Notifications (Websocket-specific)**
+ +The following is an example Go application which uses the [rpcclient](https://github.com/p9c/p9/rpcclient) package to +connect with a pod instance via Websockets and registers for [blockconnected](#blockconnected) +and [blockdisconnected](#blockdisconnected) notifications with [notifyblocks](#notifyblocks). It also sets up handlers +for the notifications. + +```Go +package main +import ( + "github.com/p9c/p9/pkg/rpc/client" "git.parallelcoin.io/btcutil" "github.com/p9c/p9/pkg/chain/hash" + + "github.com/p9c/p9/pkg/chain/wire" + "io/ioutil" + "log" + "path/filepath" + "time" +) +func main( ) { + + // Setup handlers for blockconnected and blockdisconnected + // notifications. ntfnHandlers := rpcclient.NotificationHa lers{ + OnBlockConnected: func(hash *chainhash.Hash, height int32 { + + log.Printf("Block connected: %v (%d)", hash, height) + }, + OnBlockDisconnected: func(hash *chainhash.Hash, height int32) { + + log.Printf("Block disconnected: %v", hash, height) + }, + } + // Load the certificate for the TLS connection which is automatically + // generated by pod when it starts the RPC server and doesn't already + // have one. + podHomeDir := btcutil.AppDataDir("pod", false) + certs, e := ioutil.ReadFile(filepath.Join(podHomeDir, "rpc.cert")) + if e != nil { + L.L.ftl.Ln(err) + } + // Create a new RPC client using eb ckets. + connCfg := &rpcclient.ConnConfig{ Host: "localhost:11048", + + Endpoint: "ws", + User: "yourrpcuser", + Pass: "yourrpcpass", + Certificates: certs, + } client, e := pcclient.New(connCfg, &ntfnHandlers) + if e != nil { L.ftl.Ln(err) + + } + // Register for blockconnected and blockdisconneted notifications. + if e := client.NotifyBlocks(); dbg.Chk(e) { + client.Shutdown() + L.ftl.Ln(err) + } + // For this example, gracefully shutdown the client after 10 seconds. + // Ordinarily when to shutdown the client is highly application + // specific. + log.Println("Client shutdown in 10 seconds...") + time.AfterFunc(time.Second*10, func() { + + log.Println("Client shutting down...") + client.Shutdown() + log.Println("Client shutdown complete.") + }) + // Wait until the client either shuts down gracefully (or the user + // terminates the process with Ctrl+C). + client.WaitForShutdown() +} +``` + +Example output: + +``` +2014/05/12 20:33:17 Client shutdown in 10 seconds... +2014/05/12 20:33:19 Block connected: 000000000000000007dff1f95f7b3f5eac2892a4123069517caf34e2c417650d (300461) +2014/05/12 20:33:27 Client shutting down... +2014/05/12 20:31:27 Client shutdown complete. +``` + + + +### 9.2. Example node.js Code + + + +**9.2.1 Using notifyblocks to be Notified of Block Connects and Disconnects**
+ +The following is example node.js code which uses [ws](https://github.com/einaros/ws) (can be installed +with `npm install ws`) to connect with a pod instance, issues [notifyblocks](#notifyblocks) to register +for [blockconnected](#blockconnected) and [blockdisconnected](#blockdisconnected) notifications, and displays all +incoming messages. + +```javascript +var fs = require('fs'); +var WebSocket = require('ws'); +// Load the certificate for the TLS connection which is automatically +// generated by pod when it starts the RPC server and doesn't already +// have one. +var cert = fs.readFileSync('/path/to/pod/appdata/rpc.cert'); +var user = "yourusername"; +var password = "yourpassword"; +// Initiate the websocket connection. The pod generated certificate acts as +// its own certificate authority, so it needs to be specified in the 'ca' array +// for the certificate to properly validate. +var ws = new WebSocket('wss://127.0.0.1:11048/ws', { + headers: { + 'Authorization': 'Basic '+new Buffer(user+':'+password).toString('base64') + }, + cert: cert, + ca: [cert] +}); +ws.on('open', function() { + + console.log('CONNECTED'); + // Send a JSON-RPC command to be notified when blocks are connected and + // disconnected from the chain. + ws.send(netparams); +}); +ws.on('message', function(data, flags) { + + console.log(data); +}); +ws.on('error', function(derp) { + + console.log('ERROR:' + derp); +}); +ws.on('close', function(data) { + + console.log('DISCONNECTED'); +}) +``` diff --git a/cmd/node/docs/parabolic-diff-adjustment-filter-formula.png b/cmd/node/docs/parabolic-diff-adjustment-filter-formula.png new file mode 100755 index 0000000..0a7a131 Binary files /dev/null and b/cmd/node/docs/parabolic-diff-adjustment-filter-formula.png differ diff --git a/cmd/node/docs/parabolic-diff-adjustment-filter.png b/cmd/node/docs/parabolic-diff-adjustment-filter.png new file mode 100755 index 0000000..143fe5e Binary files /dev/null and b/cmd/node/docs/parabolic-diff-adjustment-filter.png differ diff --git a/cmd/node/docs/parabolic_filter_difficulty_adjustment.md b/cmd/node/docs/parabolic_filter_difficulty_adjustment.md new file mode 100755 index 0000000..0902910 --- /dev/null +++ b/cmd/node/docs/parabolic_filter_difficulty_adjustment.md @@ -0,0 +1,103 @@ +# Parabolic Filter Difficulty Adjustment + +The difficulty adjustment algorithm implemented on the testnet, which can be found [here](../blockchain/difficulty.go) +uses a simple cubic binomial formula that generates a variance against a straight linear multiplication of the average +divergence from target that has a relatively wide, flat area that acts as a trap due to the minimal adjustment it +computes within the centre 1/3, approximately, though as can be seen it is a little sharper above 1 than below: + +![](parabolic-diff-adjustment-filter.png) + +The formula is as follows: + +![](parabolic-diff-adjustment-filter-formula.png) + +The data used for adjustment is the timestamps of the most recent block of an algorithm, and a fixed averaging window, +which will initially be set at approximately 5 days (1440 blocks). Weighting, removing outliers, and other techniques +were considered, but due to the fact that this essentially both a fuzzy logic control system, and being a poisson point +process, it has been decided that for the first hard fork, as proven in initial testing, is already far superior to the +previous mechanism that captures the most recent 10 blocks of a given algorithm, independently. + +The performance of the original continuous difficulty adjustment system is very problematic. It settles into a very wide +variance, with clusters of short blocks followed by very long gaps. It is further destabilised by wide variance of +network hashpower, and the red line in the chart above roughly illustrates the variance/response that currently exists, +though it fails even to work as well as a simple variance of the target directly by multiplying the divergence ratio by +the previous difficulty, even with only one algorithm running, on a loopback testnet. + +## The problems of existing difficulty adjustment regimes + +Different cryptocurrencies have implemented a wide variety of difficulty adjustment schemes, generally the older coins +with a bigger miner userbase have simpler, not even continuous adjusting, bitcoin simply makes an adjustment every +2016 (2 weeks) blocks. This kind of strategy is adequate in the case of a relatively stable hashrate on the network, but +increasingly miners are using automated systems to mine the most profitable/and/or/easy (difficulty) coins instead, +which often leads to even more volatile hashrates and consequent stress testing of the difficulty adjustments. + +### Aliasing, the number one problem + +The biggest problem with difficulty adjustment systems tends to be based on the low precision of the block timestamps, +the low effective difficulty targeting precision, and due to the square-edged stair-step nature of such coarse +resolution, is very vulnerable to falling into resonance feedback as the sharp harmonics implicitly existing in a coarse +sampling system can rapidly get caught in wildly incorrect adjustments. + +The fundamental fact is that what we call 'square' in mathematics is in fact literally infinite exponents. You can see +this very easily by simply plotting y=xn curves with large values of `n`. In an adjustment, the calculation +can jump dramatically between one point and the next in the grid, and then it can trigger resonances built from common +factors in the coarse granuality, sending difficulty either up or down far beyond the change in actual hashrate. + +The problem grows worse and worse as you try to reduce the block time, and then further compounding the dysregulation, +random network latency, partitioning and partial partitioning such as near-partitioning can cause parts of the network +to desynchronise dramatically, and the resonant cascades are fed with further fluctuating changes in latency, leading to +high levels of orphan blocks, inaccurate adjustments, and dwells on outlying edges of the normal of the target. + +For this, the difficulty adjustment flips the last two bits of the compressed 'bits' value. This is not an excessive +variance, and basically tends to 'wiggle the tail' of the adjustments so that they are far less likely to fall into +dwell points and go nonlinear. Because it is deterministic, all nodes can easily agree on the correct value based on a +block timestamp and previous block difficulty target, but the result, like the source data, is stochastic, which helps +eliminate the effects of aliasing distortion. + +### Parabolic response curve + +Most proof of work difficulty adjustment regimes use linear functions to find the target created in a block for the next +block to hit. This geometry implicitly creates increasing instability because of the aliasing, as an angular edge, as +mentioned before, has very high harmonics (power/parabolic function with a high index). So in this algorithm we use the +nice, smooth cubic binomial as shown above, which basically adjusts the difficulty less the closer it is to target. + +The area has to be reasonably wide, but not too wide. The constant linear adjustment caused by a linear averaging filter +will cause this target to frequenntly be missed, and most often resulting in a periodic variance that never really gets +satisfactorily close to keeping the block time stable. + +But the worst part of a linear adjustment is that its intrinsic harmonics, created by granularity and aliasing, provide +an attack surface for hashrate based attacks. + +So the parabolic filter adjustment will instead converge slowly, and because of the dithering of the lowest bits of the +difficulty target, it will usually avoid dwell points and compounding of common factors. The dithering also helps make +it so that as the divergence starts to grow, if hashrate has significantly changed, its stochastic nature will make it +more possible for the adjustment to more rapidly adjust. + +## Conclusion + +By adding a small amount of extra noise to the computations, we diminish the effect of common factors leading to dwells +and sharp variation, and when wide variance occurs, the curve increases the attack of the adjustment in proportion, +smoothly, with the width of the divergence. + +The issue of difficulty adjustment becomes a bigger and bigger problem for Proof of Work blockchains the greater the +amount of available hashpower becomes. Chains that are on the margins of hashrate distribution between chains are more +and more vulnerable to 51% attacks and more sophisticated timing based attacks that usually aim to freeze the clock in a +high work side chain that passes the chain tip. + +These issues are a huge problem and the only solution can be, without abandoning the anti-spam proof of work system +altogether, to improve the defences against the various attacks. Eliminating resonances is a big part of this, as they +often are the easiest way to lower the real hashpower requuired to launch a 51% attack. + +Further techniques, which will become more relevant with a higher transaction volume, is to diminish the incentive for +very large miners to gobble up all the available transactions far outside of the normal, leaving the rest of the miners, +of which many are loyal to a coin, out of pocket. + +The withholding attack on pools is another issue, and part of the solution with this lies in ensuring that transactions +are more spread out in their placement in blocks, the way that this will be done is inspired by the ideas in Freshcoin, +which raises difficulty target along with the block weight. + +Another approach that will be explored is low reward minimum difficulty blocks. These are tricky to implement in a live +network with a lot of miners because of the problem of network synchronisation. The solution would seem to lie in +setting a boundary but making it fuzzy enough that these blocks do not cause large numbers of orphans. The other problem +with this is to do with block timestamp based attacks. So for this reason, such changes will be put off for future +exploration. diff --git a/cmd/node/integration/README.md b/cmd/node/integration/README.md new file mode 100755 index 0000000..726fc02 --- /dev/null +++ b/cmd/node/integration/README.md @@ -0,0 +1,11 @@ +# integration + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) + +This contains integration tests which make use of +the [rpctest](https://github.com/p9c/p9/tree/master/integration/rpctest) +package to programmatically drive nodes via RPC. + +## License + +This code is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/cmd/node/integration/log.go b/cmd/node/integration/log.go new file mode 100644 index 0000000..5a2d662 --- /dev/null +++ b/cmd/node/integration/log.go @@ -0,0 +1,43 @@ +package integration + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/node/integration/main.go b/cmd/node/integration/main.go new file mode 100644 index 0000000..f6a4e4c --- /dev/null +++ b/cmd/node/integration/main.go @@ -0,0 +1,4 @@ +package integration + +// This file only exists to prevent warnings due to no buildable source files when the build tag for enabling the tests +// is not specified. diff --git a/cmd/node/integration/rpcserver_test.go b/cmd/node/integration/rpcserver_test.go new file mode 100644 index 0000000..04afedd --- /dev/null +++ b/cmd/node/integration/rpcserver_test.go @@ -0,0 +1,144 @@ +package integration + +import ( + "bytes" + "fmt" + "os" + "runtime/debug" + "testing" + + "github.com/p9c/p9/cmd/node/integration/rpctest" + "github.com/p9c/p9/pkg/chaincfg" +) + +func testGetBestBlock(r *rpctest.Harness, t *testing.T) { + _, prevbestHeight, e := r.Node.GetBestBlock() + if e != nil { + t.Fatalf("Call to `getbestblock` failed: %v", e) + } + // Create a new block connecting to the current tip. + generatedBlockHashes, e := r.Node.Generate(1) + if e != nil { + t.Fatalf("Unable to generate block: %v", e) + } + bestHash, bestHeight, e := r.Node.GetBestBlock() + if e != nil { + t.Fatalf("Call to `getbestblock` failed: %v", e) + } + // Hash should be the same as the newly submitted block. + if !bytes.Equal(bestHash[:], generatedBlockHashes[0][:]) { + t.Fatalf( + "Block hashes do not match. Returned hash %v, wanted "+ + "hash %v", bestHash, generatedBlockHashes[0][:], + ) + } + // Block height should now reflect newest height. + if bestHeight != prevbestHeight+1 { + t.Fatalf( + "Block heights do not match. Got %v, wanted %v", + bestHeight, prevbestHeight+1, + ) + } +} + +func testGetBlockCount(r *rpctest.Harness, t *testing.T) { + // Save the current count. + currentCount, e := r.Node.GetBlockCount() + if e != nil { + t.Fatalf("Unable to get block count: %v", e) + } + if _, e = r.Node.Generate(1); E.Chk(e) { + t.Fatalf("Unable to generate block: %v", e) + } + // Count should have increased by one. + newCount, e := r.Node.GetBlockCount() + if e != nil { + t.Fatalf("Unable to get block count: %v", e) + } + if newCount != currentCount+1 { + t.Fatalf( + "Block count incorrect. Got %v should be %v", + newCount, currentCount+1, + ) + } +} + +func testGetBlockHash(r *rpctest.Harness, t *testing.T) { + // Create a new block connecting to the current tip. + generatedBlockHashes, e := r.Node.Generate(1) + if e != nil { + t.Fatalf("Unable to generate block: %v", e) + } + info, e := r.Node.GetInfo() + if e != nil { + t.Fatalf("call to getinfo cailed: %v", e) + } + blockHash, e := r.Node.GetBlockHash(int64(info.Blocks)) + if e != nil { + t.Fatalf("Call to `getblockhash` failed: %v", e) + } + // Block hashes should match newly created block. + if !bytes.Equal(generatedBlockHashes[0][:], blockHash[:]) { + + t.Fatalf( + "Block hashes do not match. Returned hash %v, wanted "+ + "hash %v", blockHash, generatedBlockHashes[0][:], + ) + } +} + +var rpcTestCases = []rpctest.HarnessTestCase{ + testGetBestBlock, + testGetBlockCount, + testGetBlockHash, +} +var primaryHarness *rpctest.Harness + +func TestMain(m *testing.M) { + var e error + // In order to properly test scenarios on as if we were on mainnet, ensure that non-standard transactions aren't + // accepted into the mempool or relayed. + podCfg := []string{"--rejectnonstd"} + primaryHarness, e = rpctest.New(&chaincfg.SimNetParams, nil, podCfg) + if e != nil { + fmt.Println("unable to create primary harness: ", e) + os.Exit(1) + } + // Initialize the primary mining node with a chain of length 125, providing 25 mature coinbases to allow spending + // from for testing purposes. + if e := primaryHarness.SetUp(true, 25); E.Chk(e) { + fmt.Println("unable to setup test chain: ", e) + // Even though the harness was not fully setup, it still needs to be torn down to ensure all resources such as + // temp directories are cleaned up. The error is intentionally ignored since this is already an error path and + // nothing else could be done about it anyways. + _ = primaryHarness.TearDown() + os.Exit(1) + } + exitCode := m.Run() + // Clean up any active harnesses that are still currently running. This includes removing all temporary directories, + // and shutting down any created processes. + if e := rpctest.TearDownAll(); E.Chk(e) { + fmt.Println("unable to tear down all harnesses: ", e) + os.Exit(1) + } + os.Exit(exitCode) +} + +func TestRpcServer(t *testing.T) { + var currentTestNum int + defer func() { + // If one of the integration tests caused a panic within the main goroutine, then tear down all the harnesses in + // order to avoid any leaked pod processes. + if r := recover(); r != nil { + fmt.Println("recovering from test panic: ", r) + if e := rpctest.TearDownAll(); E.Chk(e) { + fmt.Println("unable to tear down all harnesses: ", e) + } + t.Fatalf("test #%v panicked: %s", currentTestNum, debug.Stack()) + } + }() + for _, testCase := range rpcTestCases { + testCase(primaryHarness, t) + currentTestNum++ + } +} diff --git a/cmd/node/integration/rpctest/README.md b/cmd/node/integration/rpctest/README.md new file mode 100755 index 0000000..4ebc22f --- /dev/null +++ b/cmd/node/integration/rpctest/README.md @@ -0,0 +1,27 @@ +# rpctest + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/integration/rpctest) + +Package rpctest provides a pod-specific RPC testing harness crafting and +executing integration tests by driving a `pod` +instance via the `RPC` interface. Each instance of an active harness comes +equipped with a simple in-memory HD wallet capable of properly syncing to the +generated chain, creating new addresses, and crafting fully signed transactions +paying to an arbitrary set of outputs. + +This package was designed specifically to act as an RPC testing harness +for `pod`. However, the constructs presented are general enough to be adapted to +any project wishing to programmatically drive a `pod` instance of its +systems/integration tests. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/integration/rpctest +``` + +## License + +Package rpctest is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/cmd/node/integration/rpctest/blockgen.go b/cmd/node/integration/rpctest/blockgen.go new file mode 100644 index 0000000..44af422 --- /dev/null +++ b/cmd/node/integration/rpctest/blockgen.go @@ -0,0 +1,202 @@ +package rpctest + +import ( + "errors" + "math" + "math/big" + "runtime" + "time" + + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// solveBlock attempts to find a nonce which makes the passed block header hash to a value less than the target +// difficulty. When a successful solution is found true is returned and the nonce field of the passed header is updated +// with the solution. False is returned if no solution exists. +func solveBlock(header *wire.BlockHeader, targetDifficulty *big.Int) bool { + // sbResult is used by the solver goroutines to send results. + type sbResult struct { + found bool + nonce uint32 + } + // solver accepts a block header and a nonce range to test. It is intended to be run as a goroutine. + quit := make(chan bool) + results := make(chan sbResult) + solver := func(hdr wire.BlockHeader, startNonce, stopNonce uint32) { + // We need to modify the nonce field of the header, so make sure we work with a copy of the original header. + for i := startNonce; i >= startNonce && i <= stopNonce; i++ { + select { + case <-quit: + return + default: + hdr.Nonce = i + hash := hdr.BlockHash() + if blockchain.HashToBig(&hash).Cmp(targetDifficulty) <= 0 { + select { + case results <- sbResult{true, i}: + return + case <-quit: + return + } + } + } + } + select { + case results <- sbResult{false, 0}: + case <-quit: + return + } + } + startNonce := uint32(0) + stopNonce := uint32(math.MaxUint32) + numCores := uint32(runtime.NumCPU()) + noncesPerCore := (stopNonce - startNonce) / numCores + for i := uint32(0); i < numCores; i++ { + rangeStart := startNonce + (noncesPerCore * i) + rangeStop := startNonce + (noncesPerCore * (i + 1)) - 1 + if i == numCores-1 { + rangeStop = stopNonce + } + go solver(*header, rangeStart, rangeStop) + } + for i := uint32(0); i < numCores; i++ { + result := <-results + if result.found { + close(quit) + header.Nonce = result.nonce + return true + } + } + return false +} + +// standardCoinbaseScript returns a standard script suitable for use as the signature script of the coinbase transaction +// of a new block. In particular, it starts with the block height that is required by version 2 blocks. +func standardCoinbaseScript(nextBlockHeight int32, extraNonce uint64) ([]byte, error) { + return txscript.NewScriptBuilder().AddInt64(int64(nextBlockHeight)). + AddInt64(int64(extraNonce)).Script() +} + +// createCoinbaseTx returns a coinbase transaction paying an appropriate subsidy based on the passed block height to the +// provided address. +func createCoinbaseTx( + coinbaseScript []byte, + nextBlockHeight int32, + addr btcaddr.Address, + mineTo []wire.TxOut, + net *chaincfg.Params, + version int32, +) (*util.Tx, error) { + // Create the script to pay to the provided payment address. + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + return nil, e + } + tx := wire.NewMsgTx(wire.TxVersion) + tx.AddTxIn( + &wire.TxIn{ + // Coinbase transactions have no inputs, so previous outpoint is zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint( + &chainhash.Hash{}, + wire.MaxPrevOutIndex, + ), + SignatureScript: coinbaseScript, + Sequence: wire.MaxTxInSequenceNum, + }, + ) + if len(mineTo) == 0 { + tx.AddTxOut( + &wire.TxOut{ + Value: blockchain.CalcBlockSubsidy(nextBlockHeight, net, version), + PkScript: pkScript, + }, + ) + } else { + for i := range mineTo { + tx.AddTxOut(&mineTo[i]) + } + } + return util.NewTx(tx), nil +} + +// CreateBlock creates a new block building from the previous block with a specified blockversion and timestamp. If the +// timestamp passed is zero ( not initialized), then the timestamp of the previous block will be used plus 1 second is +// used. Passing nil for the previous block results in a block that builds off of the genesis block for the specified +// chain. +func CreateBlock( + prevBlock *block.Block, inclusionTxs []*util.Tx, + blockVersion int32, blockTime time.Time, miningAddr btcaddr.Address, + mineTo []wire.TxOut, net *chaincfg.Params, +) (*block.Block, error) { + var ( + prevHash *chainhash.Hash + blockHeight int32 + prevBlockTime time.Time + ) + // If the previous block isn't specified, then we'll construct a block that builds off of the genesis block for the + // chain. + if prevBlock == nil { + prevHash = net.GenesisHash + blockHeight = 1 + prevBlockTime = net.GenesisBlock.Header.Timestamp.Add(time.Minute) + } else { + prevHash = prevBlock.Hash() + blockHeight = prevBlock.Height() + 1 + prevBlockTime = prevBlock.WireBlock().Header.Timestamp + } + // If a target block time was specified, then use that as the header's timestamp. Otherwise, add one second to the + // previous block unless it's the genesis block in which case use the current time. + var ts time.Time + switch { + case !blockTime.IsZero(): + ts = blockTime + default: + ts = prevBlockTime.Add(time.Second) + } + extraNonce := uint64(0) + coinbaseScript, e := standardCoinbaseScript(blockHeight, extraNonce) + if e != nil { + return nil, e + } + coinbaseTx, e := createCoinbaseTx( + coinbaseScript, blockHeight, miningAddr, + mineTo, net, blockVersion, + ) + if e != nil { + return nil, e + } + // Create a new block ready to be solved. + blockTxns := []*util.Tx{coinbaseTx} + if inclusionTxs != nil { + blockTxns = append(blockTxns, inclusionTxs...) + } + merkles := blockchain.BuildMerkleTreeStore(blockTxns, false) + var b wire.Block + b.Header = wire.BlockHeader{ + Version: blockVersion, + PrevBlock: *prevHash, + MerkleRoot: *merkles.GetRoot(), + Timestamp: ts, + Bits: net.PowLimitBits, + } + for _, tx := range blockTxns { + if e := b.AddTransaction(tx.MsgTx()); E.Chk(e) { + return nil, e + } + } + found := solveBlock(&b.Header, net.PowLimit) + if !found { + return nil, errors.New("unable to solve block") + } + utilBlock := block.NewBlock(&b) + utilBlock.SetHeight(blockHeight) + return utilBlock, nil +} diff --git a/cmd/node/integration/rpctest/btcd.go b/cmd/node/integration/rpctest/btcd.go new file mode 100644 index 0000000..ea81c92 --- /dev/null +++ b/cmd/node/integration/rpctest/btcd.go @@ -0,0 +1,64 @@ +package rpctest + +import ( + "fmt" + "go/build" + "os/exec" + "path/filepath" + "runtime" + "sync" + + "github.com/p9c/p9/pkg/util/gobin" +) + +var ( + // compileMtx guards access to the executable path so that the project is only compiled once. + compileMtx sync.Mutex + // executablePath is the path to the compiled executable. This is the empty string until pod is compiled. This + // should not be accessed directly; instead use the function podExecutablePath(). + executablePath string +) + +// podExecutablePath returns a path to the pod executable to be used by rpctests. To ensure the code tests against the +// most up-to-date version of pod, this method compiles pod the first time it is called. After that, the generated +// binary is used for subsequent test harnesses. The executable file is not cleaned up, but since it lives at a static +// path in a temp directory, it is not a big deal. +func podExecutablePath() (string, error) { + compileMtx.Lock() + defer compileMtx.Unlock() + // If pod has already been compiled, just use that. + if len(executablePath) != 0 { + return executablePath, nil + } + testDir, e := baseDir() + if e != nil { + return "", e + } + // Determine import path of this package. Not necessarily pod if this is a forked repo. + _, rpctestDir, _, ok := runtime.Caller(1) + if !ok { + return "", fmt.Errorf("cannot get path to pod source code") + } + podPkgPath := filepath.Join(rpctestDir, "..", "..", "..") + podPkg, e := build.ImportDir(podPkgPath, build.FindOnly) + if e != nil { + return "", fmt.Errorf("failed to podbuild pod: %v", e) + } + // Build pod and output an executable in a static temp path. + outputPath := filepath.Join(testDir, "pod") + if runtime.GOOS == "windows" { + outputPath += ".exe" + } + var gb string + if gb, e = gobin.Get(); E.Chk(e) { + return "", e + } + cmd := exec.Command(gb, "podbuild", "-o", outputPath, podPkg.ImportPath) + e = cmd.Run() + if e != nil { + return "", fmt.Errorf("failed to podbuild pod: %v", e) + } + // Save executable path so future calls do not recompile. + executablePath = outputPath + return executablePath, nil +} diff --git a/cmd/node/integration/rpctest/doc.go b/cmd/node/integration/rpctest/doc.go new file mode 100644 index 0000000..7be5086 --- /dev/null +++ b/cmd/node/integration/rpctest/doc.go @@ -0,0 +1,13 @@ +// Package rpctest provides a pod-specific RPC testing harness crafting and executing integration tests by driving a +// `pod` instance via the `RPC` interface. +// +// Each instance of an active harness comes equipped with a simple in-memory HD wallet capable of properly syncing to +// the generated chain, creating new addresses and crafting fully signed transactions paying to an arbitrary set of +// outputs. +// +// This package was designed specifically to act as an RPC testing +// harness for `pod`. +// +// However the constructs presented are general enough to be adapted to any project wishing to programmatically drive a +// `pod` instance of its systems integration tests. +package rpctest diff --git a/cmd/node/integration/rpctest/log.go b/cmd/node/integration/rpctest/log.go new file mode 100644 index 0000000..3133114 --- /dev/null +++ b/cmd/node/integration/rpctest/log.go @@ -0,0 +1,43 @@ +package rpctest + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/node/integration/rpctest/memwallet.go b/cmd/node/integration/rpctest/memwallet.go new file mode 100644 index 0000000..39f0c8b --- /dev/null +++ b/cmd/node/integration/rpctest/memwallet.go @@ -0,0 +1,498 @@ +package rpctest + +import ( + "bytes" + "encoding/binary" + "fmt" + "sync" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/util/hdkeychain" + "github.com/p9c/p9/pkg/wire" +) + +var ( + // hdSeed is the BIP 32 seed used by the memWallet to initialize it's HD root key. This value is hard coded in order + // to ensure deterministic behavior across test runs. + hdSeed = [chainhash.HashSize]byte{ + 0x79, 0xa6, 0x1a, 0xdb, 0xc6, 0xe5, 0xa2, 0xe1, + 0x39, 0xd2, 0x71, 0x3a, 0x54, 0x6e, 0xc7, 0xc8, + 0x75, 0x63, 0x2e, 0x75, 0xf1, 0xdf, 0x9c, 0x3f, + 0xa6, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } +) + +// utxo represents an unspent output spendable by the memWallet. The maturity height of the transaction is recorded in +// order to properly observe the maturity period of direct coinbase outputs. +type utxo struct { + pkScript []byte + value amt.Amount + keyIndex uint32 + maturityHeight int32 + isLocked bool +} + +// isMature returns true if the target utxo is considered "mature" at the passed block height. Otherwise, false is +// returned. +func (u *utxo) isMature(height int32) bool { + return height >= u.maturityHeight +} + +// chainUpdate encapsulates an update to the current main chain. This struct is used to sync up the memWallet each time +// a new block is connected to the main chain. +type chainUpdate struct { + filteredTxns []*util.Tx + blockHeight int32 + isConnect bool // True if connect, false if disconnect +} + +// undoEntry is functionally the opposite of a chainUpdate. An undoEntry is created for each new block received, then +// stored in a log in order to properly handle block re-orgs. +type undoEntry struct { + utxosDestroyed map[wire.OutPoint]*utxo + utxosCreated []wire.OutPoint +} + +// memWallet is a simple in-memory wallet whose purpose is to provide basic wallet functionality to the harness. The +// wallet uses a hard-coded HD key hierarchy which promotes reproducibility between harness test runs. +type memWallet struct { + coinbaseKey *ec.PrivateKey + coinbaseAddr btcaddr.Address + // hdRoot is the root master private key for the wallet. + hdRoot *hdkeychain.ExtendedKey + // hdIndex is the next available key index offset from the hdRoot. + hdIndex uint32 + // currentHeight is the latest height the wallet is known to be synced to. + currentHeight int32 + // addrs tracks all addresses belonging to the wallet. + // The addresses are indexed by their keypath from the hdRoot. + addrs map[uint32]btcaddr.Address + // utxos is the set of utxos spendable by the wallet. + utxos map[wire.OutPoint]*utxo + // reorgJournal is a map storing an undo entry for each new block received. Once a block is disconnected, the undo + // entry for the particular height is evaluated, thereby rewinding the effect of the disconnected block on the + // wallet's set of spendable utxos. + reorgJournal map[int32]*undoEntry + chainUpdates []*chainUpdate + chainUpdateSignal qu.C + chainMtx sync.Mutex + net *chaincfg.Params + rpc *rpcclient.Client + sync.RWMutex +} + +// newMemWallet creates and returns a fully initialized instance of the memWallet given a particular blockchain's +// parameters. +func newMemWallet(net *chaincfg.Params, harnessID uint32) (*memWallet, error) { + // The wallet's final HD seed is: hdSeed || harnessID. This method ensures that each harness instance uses a + // deterministic root seed based on its harness ID. + var harnessHDSeed [chainhash.HashSize + 4]byte + copy(harnessHDSeed[:], hdSeed[:]) + binary.BigEndian.PutUint32(harnessHDSeed[:chainhash.HashSize], harnessID) + hdRoot, e := hdkeychain.NewMaster(harnessHDSeed[:], net) + if e != nil { + return nil, nil + } + // The first child key from the hd root is reserved as the coinbase generation address. + coinbaseChild, e := hdRoot.Child(0) + if e != nil { + return nil, e + } + coinbaseKey, e := coinbaseChild.ECPrivKey() + if e != nil { + return nil, e + } + coinbaseAddr, e := keyToAddr(coinbaseKey, net) + if e != nil { + return nil, e + } + // Track the coinbase generation address to ensure we properly track newly generated DUO we can spend. + addrs := make(map[uint32]btcaddr.Address) + addrs[0] = coinbaseAddr + return &memWallet{ + net: net, + coinbaseKey: coinbaseKey, + coinbaseAddr: coinbaseAddr, + hdIndex: 1, + hdRoot: hdRoot, + addrs: addrs, + utxos: make(map[wire.OutPoint]*utxo), + chainUpdateSignal: qu.T(), + reorgJournal: make(map[int32]*undoEntry), + }, nil +} + +// Start launches all goroutines required for the wallet to function properly. +func (m *memWallet) Start() { + go m.chainSyncer() +} + +// SyncedHeight returns the height the wallet is known to be synced to. This function is safe for concurrent access. +func (m *memWallet) SyncedHeight() int32 { + m.RLock() + defer m.RUnlock() + return m.currentHeight +} + +// SetRPCClient saves the passed rpc connection to pod as the wallet's personal rpc connection. +func (m *memWallet) SetRPCClient(rpcClient *rpcclient.Client) { + m.rpc = rpcClient +} + +// IngestBlock is a call-back which is to be triggered each time a new block is connected to the main chain. It queues +// the update for the chain syncer, calling the private version in sequential order. +func (m *memWallet) IngestBlock(height int32, header *wire.BlockHeader, filteredTxns []*util.Tx) { + // Append this new chain update to the end of the queue of new chain + // updates. + m.chainMtx.Lock() + m.chainUpdates = append( + m.chainUpdates, &chainUpdate{ + filteredTxns, + height, + true, + }, + ) + m.chainMtx.Unlock() + // Launch a goroutine to signal the chainSyncer that a new update is available. We do this in a new goroutine in + // order to avoid blocking the main loop of the rpc client. + go func() { + m.chainUpdateSignal <- struct{}{} + }() +} + +// ingestBlock updates the wallet's internal utxo state based on the outputs created and destroyed within each block. +func (m *memWallet) ingestBlock(update *chainUpdate) { + // Update the latest synced height, then process each filtered transaction in the block creating and destroying + // utxos within the wallet as a result. + m.currentHeight = update.blockHeight + undo := &undoEntry{ + utxosDestroyed: make(map[wire.OutPoint]*utxo), + } + for _, tx := range update.filteredTxns { + mtx := tx.MsgTx() + isCoinbase := blockchain.IsCoinBaseTx(mtx) + txHash := mtx.TxHash() + m.evalOutputs(mtx.TxOut, &txHash, isCoinbase, undo) + m.evalInputs(mtx.TxIn, undo) + } + // Finally, record the undo entry for this block so we can properly update our internal state in response to the + // block being re-org'd from the main chain. + m.reorgJournal[update.blockHeight] = undo +} + +// chainSyncer is a goroutine dedicated to processing new blocks in order to keep the wallet's utxo state up to date. +// NOTE: This MUST be run as a goroutine. +func (m *memWallet) chainSyncer() { + var update *chainUpdate + for range m.chainUpdateSignal { + // A new update is available, so pop the new chain update from the front of the update queue. + m.chainMtx.Lock() + update = m.chainUpdates[0] + m.chainUpdates[0] = nil // Set to nil to prevent GC leak. + m.chainUpdates = m.chainUpdates[1:] + m.chainMtx.Unlock() + m.Lock() + if update.isConnect { + m.ingestBlock(update) + } else { + m.unwindBlock(update) + } + m.Unlock() + } +} + +// evalOutputs evaluates each of the passed outputs, creating a new matching utxo within the wallet if we're able to +// spend the output. +func (m *memWallet) evalOutputs( + outputs []*wire.TxOut, txHash *chainhash.Hash, + isCoinbase bool, undo *undoEntry, +) { + for i, output := range outputs { + pkScript := output.PkScript + // Scan all the addresses we currently control to see if the output is paying to us. + for keyIndex, addr := range m.addrs { + pkHash := addr.ScriptAddress() + if !bytes.Contains(pkScript, pkHash) { + continue + } + // If this is a coinbase output, then we mark the maturity height at the proper block height in the future. + var maturityHeight int32 + if isCoinbase { + maturityHeight = m.currentHeight + int32(m.net.CoinbaseMaturity) + } + op := wire.OutPoint{Hash: *txHash, Index: uint32(i)} + m.utxos[op] = &utxo{ + value: amt.Amount(output.Value), + keyIndex: keyIndex, + maturityHeight: maturityHeight, + pkScript: pkScript, + } + undo.utxosCreated = append(undo.utxosCreated, op) + } + } +} + +// evalInputs scans all the passed inputs, destroying any utxos within the wallet which are spent by an input. +func (m *memWallet) evalInputs(inputs []*wire.TxIn, undo *undoEntry) { + for _, txIn := range inputs { + op := txIn.PreviousOutPoint + oldUtxo, ok := m.utxos[op] + if !ok { + continue + } + undo.utxosDestroyed[op] = oldUtxo + delete(m.utxos, op) + } +} + +// UnwindBlock is a call-back which is to be executed each time a block is disconnected from the main chain. It queues +// the update for the chain syncer, calling the private version in sequential order. +func (m *memWallet) UnwindBlock(height int32, header *wire.BlockHeader) { + // Append this new chain update to the end of the queue of new chain + // updates. + m.chainMtx.Lock() + m.chainUpdates = append( + m.chainUpdates, &chainUpdate{ + nil, + height, + false, + }, + ) + m.chainMtx.Unlock() + // Launch a goroutine to signal the chainSyncer that a new update is available. We do this in a new goroutine in + // order to avoid blocking the main loop of the rpc client. + go func() { + m.chainUpdateSignal <- struct{}{} + }() +} + +// unwindBlock undoes the effect that a particular block had on the wallet's internal utxo state. +func (m *memWallet) unwindBlock(update *chainUpdate) { + undo := m.reorgJournal[update.blockHeight] + for _, utxo := range undo.utxosCreated { + delete(m.utxos, utxo) + } + for outPoint, utxo := range undo.utxosDestroyed { + m.utxos[outPoint] = utxo + } + delete(m.reorgJournal, update.blockHeight) +} + +// newAddress returns a new address from the wallet's hd key chain. It also loads the address into the RPC client's +// transaction filter to ensure any transactions that involve it are delivered via the notifications. +func (m *memWallet) newAddress() (btcaddr.Address, error) { + index := m.hdIndex + childKey, e := m.hdRoot.Child(index) + if e != nil { + return nil, e + } + privKey, e := childKey.ECPrivKey() + if e != nil { + return nil, e + } + addr, e := keyToAddr(privKey, m.net) + if e != nil { + return nil, e + } + e = m.rpc.LoadTxFilter(false, []btcaddr.Address{addr}, nil) + if e != nil { + return nil, e + } + m.addrs[index] = addr + m.hdIndex++ + return addr, nil +} + +// NewAddress returns a fresh address spendable by the wallet. This function is safe for concurrent access. +func (m *memWallet) NewAddress() (btcaddr.Address, error) { + m.Lock() + defer m.Unlock() + return m.newAddress() +} + +// fundTx attempts to fund a transaction sending amt bitcoin. The coins are selected such that the final amount spent +// pays enough fees as dictated by the passed fee rate. The passed fee rate should be expressed in satoshis-per-byte. +// The transaction being funded can optionally include a change output indicated by the change boolean. NOTE: The +// memWallet's mutex must be held when this function is called. +func (m *memWallet) fundTx( + tx *wire.MsgTx, amount amt.Amount, + feeRate amt.Amount, change bool, +) (e error) { + const ( + // spendSize is the largest number of bytes of a sigScript which spends a p2pkh output: OP_DATA_73 + // OP_DATA_33 + spendSize = 1 + 73 + 1 + 33 + ) + var ( + amtSelected amt.Amount + txSize int + ) + for outPoint, utxo := range m.utxos { + // Skip any outputs that are still currently immature or are currently locked. + if !utxo.isMature(m.currentHeight) || utxo.isLocked { + continue + } + amtSelected += utxo.value + // Add the selected output to the transaction, updating the current tx size while accounting for the size of the + // future sigScript. + op := outPoint + tx.AddTxIn(wire.NewTxIn(&op, nil, nil)) + txSize = tx.SerializeSize() + spendSize*len(tx.TxIn) + // Calculate the fee required for the txn at this point observing the specified fee rate. If we don't have + // enough coins from he current amount selected to pay the fee, then continue to grab more coins. + reqFee := amt.Amount(txSize * int(feeRate)) + if amtSelected-reqFee < amount { + continue + } + // If we have any change left over and we should create a change output, then add an additional output to the + // transaction reserved for it. + changeVal := amtSelected - amount - reqFee + if changeVal > 0 && change { + addr, e := m.newAddress() + if e != nil { + return e + } + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + return e + } + changeOutput := &wire.TxOut{ + Value: int64(changeVal), + PkScript: pkScript, + } + tx.AddTxOut(changeOutput) + } + return nil + } + // If we've reached this point, then coin selection failed due to an insufficient amount of coins. + return fmt.Errorf("not enough funds for coin selection") +} + +// SendOutputs creates then sends a transaction paying to the specified output while observing the passed fee rate. The +// passed fee rate should be expressed in satoshis-per-byte. +func (m *memWallet) SendOutputs( + outputs []*wire.TxOut, + feeRate amt.Amount, +) (*chainhash.Hash, error) { + tx, e := m.CreateTransaction(outputs, feeRate, true) + if e != nil { + return nil, e + } + return m.rpc.SendRawTransaction(tx, true) +} + +// SendOutputsWithoutChange creates and sends a transaction that pays to the specified outputs while observing the +// passed fee rate and ignoring a change output. The passed fee rate should be expressed in sat/b. +func (m *memWallet) SendOutputsWithoutChange( + outputs []*wire.TxOut, + feeRate amt.Amount, +) (*chainhash.Hash, error) { + tx, e := m.CreateTransaction(outputs, feeRate, false) + if e != nil { + return nil, e + } + return m.rpc.SendRawTransaction(tx, true) +} + +// CreateTransaction returns a fully signed transaction paying to the specified outputs while observing the desired fee +// rate. The passed fee rate should be expressed in satoshis-per-byte. The transaction being created can optionally +// include a change output indicated by the change boolean. This function is safe for concurrent access. +func (m *memWallet) CreateTransaction( + outputs []*wire.TxOut, + feeRate amt.Amount, change bool, +) (*wire.MsgTx, error) { + m.Lock() + defer m.Unlock() + tx := wire.NewMsgTx(wire.TxVersion) + // Tally up the total amount to be sent in order to perform coin selection shortly below. + var outputAmt amt.Amount + for _, output := range outputs { + outputAmt += amt.Amount(output.Value) + tx.AddTxOut(output) + } + // Attempt to fund the transaction with spendable utxos. + if e := m.fundTx(tx, outputAmt, feeRate, change); E.Chk(e) { + return nil, e + } + // Populate all the selected inputs with valid sigScript for spending. Along the way record all outputs being spent + // in order to avoid a potential double spend. + spentOutputs := make([]*utxo, 0, len(tx.TxIn)) + for i, txIn := range tx.TxIn { + outPoint := txIn.PreviousOutPoint + utxo := m.utxos[outPoint] + extendedKey, e := m.hdRoot.Child(utxo.keyIndex) + if e != nil { + return nil, e + } + privKey, e := extendedKey.ECPrivKey() + if e != nil { + return nil, e + } + sigScript, e := txscript.SignatureScript( + tx, i, utxo.pkScript, + txscript.SigHashAll, privKey, true, + ) + if e != nil { + return nil, e + } + txIn.SignatureScript = sigScript + spentOutputs = append(spentOutputs, utxo) + } + // As these outputs are now being spent by this newly created transaction, mark the outputs are "locked". This + // action ensures these outputs won't be double spent by any subsequent transactions. These locked outputs can be + // freed via a call to UnlockOutputs. + for _, utxo := range spentOutputs { + utxo.isLocked = true + } + return tx, nil +} + +// UnlockOutputs unlocks any outputs which were previously locked due to being selected to fund a transaction via the +// CreateTransaction method. This function is safe for concurrent access. +func (m *memWallet) UnlockOutputs(inputs []*wire.TxIn) { + m.Lock() + defer m.Unlock() + for _, input := range inputs { + utxo, ok := m.utxos[input.PreviousOutPoint] + if !ok { + continue + } + utxo.isLocked = false + } +} + +// ConfirmedBalance returns the confirmed balance of the wallet. This function is safe for concurrent access. +func (m *memWallet) ConfirmedBalance() amt.Amount { + m.RLock() + defer m.RUnlock() + var balance amt.Amount + for _, utxo := range m.utxos { + // Prevent any immature or locked outputs from contributing to the wallet's total confirmed balance. + if !utxo.isMature(m.currentHeight) || utxo.isLocked { + continue + } + balance += utxo.value + } + return balance +} + +// keyToAddr maps the passed private to corresponding p2pkh address. +func keyToAddr(key *ec.PrivateKey, net *chaincfg.Params) (btcaddr.Address, error) { + serializedKey := key.PubKey().SerializeCompressed() + pubKeyAddr, e := btcaddr.NewPubKey(serializedKey, net) + if e != nil { + return nil, e + } + return pubKeyAddr.PubKeyHash(), nil +} diff --git a/cmd/node/integration/rpctest/node.go b/cmd/node/integration/rpctest/node.go new file mode 100644 index 0000000..828dd5d --- /dev/null +++ b/cmd/node/integration/rpctest/node.go @@ -0,0 +1,271 @@ +package rpctest + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + rpc "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/util" +) + +// nodeConfig contains all the args and data required to launch a pod process and connect the rpc client to it. +type nodeConfig struct { + rpcUser string + rpcPass string + listen string + rpcListen string + rpcConnect string + dataDir string + logDir string + profile string + debugLevel string + extra []string + prefix string + exe string + endpoint string + certFile string + keyFile string + certificates []byte +} + +// newConfig returns a newConfig with all default values. +func newConfig(prefix, certFile, keyFile string, extra []string) (*nodeConfig, error) { + podPath, e := podExecutablePath() + if e != nil { + podPath = "pod" + } + a := &nodeConfig{ + listen: "127.0.0.1:41047", + rpcListen: "127.0.0.1:41048", + rpcUser: "user", + rpcPass: "pass", + extra: extra, + prefix: prefix, + exe: podPath, + endpoint: "ws", + certFile: certFile, + keyFile: keyFile, + } + if e := a.setDefaults(); E.Chk(e) { + return nil, e + } + return a, nil +} + +// setDefaults sets the default values of the config. It also creates the temporary data, and log directories which must +// be cleaned up with a call to cleanup(). +func (n *nodeConfig) setDefaults() (e error) { + datadir, e := ioutil.TempDir("", n.prefix+"-data") + if e != nil { + return e + } + n.dataDir = datadir + logdir, e := ioutil.TempDir("", n.prefix+"-logs") + if e != nil { + return e + } + n.logDir = logdir + cert, e := ioutil.ReadFile(n.certFile) + if e != nil { + return e + } + n.certificates = cert + return nil +} + +// arguments returns an array of arguments that be used to launch the pod process. +func (n *nodeConfig) arguments() []string { + args := []string{} + if n.rpcUser != "" { + // --rpcuser + args = append(args, fmt.Sprintf("--rpcuser=%s", n.rpcUser)) + } + if n.rpcPass != "" { + // --rpcpass + args = append(args, fmt.Sprintf("--rpcpass=%s", n.rpcPass)) + } + if n.listen != "" { + // --listen + args = append(args, fmt.Sprintf("--listen=%s", n.listen)) + } + if n.rpcListen != "" { + // --rpclisten + args = append(args, fmt.Sprintf("--rpclisten=%s", n.rpcListen)) + } + if n.rpcConnect != "" { + // --rpcconnect + args = append(args, fmt.Sprintf("--rpcconnect=%s", n.rpcConnect)) + } + // --rpccert + args = append(args, fmt.Sprintf("--rpccert=%s", n.certFile)) + // --rpckey + args = append(args, fmt.Sprintf("--rpckey=%s", n.keyFile)) + if n.dataDir != "" { + // --datadir + args = append(args, fmt.Sprintf("--datadir=%s", n.dataDir)) + } + if n.logDir != "" { + // --logdir + args = append(args, fmt.Sprintf("--logdir=%s", n.logDir)) + } + if n.profile != "" { + // --profile + args = append(args, fmt.Sprintf("--profile=%s", n.profile)) + } + if n.debugLevel != "" { + // --debuglevel + args = append(args, fmt.Sprintf("--debuglevel=%s", n.debugLevel)) + } + args = append(args, n.extra...) + return args +} + +// command returns the exec.Cmd which will be used to start the pod process. +func (n *nodeConfig) command() *exec.Cmd { + return exec.Command(n.exe, n.arguments()...) +} + +// rpcConnConfig returns the rpc connection config that can be used to connect to the pod process that is launched via +// Start(). +func (n *nodeConfig) rpcConnConfig() rpc.ConnConfig { + return rpc.ConnConfig{ + Host: n.rpcListen, + Endpoint: n.endpoint, + User: n.rpcUser, + Pass: n.rpcPass, + Certificates: n.certificates, + DisableAutoReconnect: true, + } +} + +// String returns the string representation of this nodeConfig. +func (n *nodeConfig) String() string { + return n.prefix +} + +// cleanup removes the tmp data and log directories. +func (n *nodeConfig) cleanup() (e error) { + dirs := []string{ + n.logDir, + n.dataDir, + } + for _, dir := range dirs { + if e = os.RemoveAll(dir); E.Chk(e) { + E.F("Cannot remove dir %s: %v", dir, e) + } + } + return e +} + +// node houses the necessary state required to configure, launch, and manage a pod process. +type node struct { + config *nodeConfig + cmd *exec.Cmd + pidFile string + dataDir string +} + +// newNode creates a new node instance according to the passed config. dataDir will be used to hold a file recording the +// pid of the launched process, and as the base for the log and data directories for pod. +func newNode(config *nodeConfig, dataDir string) (*node, error) { + return &node{ + config: config, + dataDir: dataDir, + cmd: config.command(), + }, nil +} + +// start creates a new pod process and writes its pid in a file reserved for recording the pid of the launched process. +// This file can be used to terminate the process in case of a hang or panic. In the case of a failing test case, or +// panic, it is important that the process be stopped via stop( ) otherwise it will persist unless explicitly killed. +func (n *node) start() (e error) { + if e = n.cmd.Start(); E.Chk(e) { + return e + } + pid, e := os.Create( + filepath.Join( + n.dataDir, + fmt.Sprintf("%s.pid", n.config), + ), + ) + if e != nil { + return e + } + n.pidFile = pid.Name() + if _, e = fmt.Fprintf(pid, "%d\n", n.cmd.Process.Pid); E.Chk(e) { + return e + } + if e = pid.Close(); E.Chk(e) { + return e + } + return nil +} + +// stop interrupts the running pod process process, and waits until it exits properly. On windows, interrupt is not +// supported so a kill signal is used instead +func (n *node) stop() (e error) { + if n.cmd == nil || n.cmd.Process == nil { + // return if not properly initialized or error starting the process + return nil + } + defer func() { + e := n.cmd.Wait() + if e != nil { + } + }() + if runtime.GOOS == "windows" { + return n.cmd.Process.Signal(os.Kill) + } + return n.cmd.Process.Signal(os.Interrupt) +} + +// cleanup cleanups process and args files. The file housing the pid of the created process will be deleted as well as +// any directories created by the process. +func (n *node) cleanup() (e error) { + if n.pidFile != "" { + if e := os.Remove(n.pidFile); E.Chk(e) { + E.F("unable to remove file %s: %v", n.pidFile, e) + } + } + return n.config.cleanup() +} + +// shutdown terminates the running pod process and cleans up all file/directories created by node. +func (n *node) shutdown() (e error) { + if e := n.stop(); E.Chk(e) { + return e + } + if e := n.cleanup(); E.Chk(e) { + return e + } + return nil +} + +// genCertPair generates a key/cert pair to the paths provided. +func genCertPair(certFile, keyFile string) (e error) { + org := "rpctest autogenerated cert" + validUntil := time.Now().Add(10 * 365 * 24 * time.Hour) + var key []byte + var cert []byte + cert, key, e = util.NewTLSCertPair(org, validUntil, nil) + if e != nil { + return e + } + // Write cert and key files. + if e = ioutil.WriteFile(certFile, cert, 0666); E.Chk(e) { + return e + } + if e = ioutil.WriteFile(keyFile, key, 0600); E.Chk(e) { + defer func() { + if e = os.Remove(certFile); E.Chk(e) { + } + }() + return e + } + return nil +} diff --git a/cmd/node/integration/rpctest/rpc_harness.go b/cmd/node/integration/rpctest/rpc_harness.go new file mode 100644 index 0000000..127e9a0 --- /dev/null +++ b/cmd/node/integration/rpctest/rpc_harness.go @@ -0,0 +1,405 @@ +package rpctest + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "strconv" + "sync" + "testing" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // These constants define the minimum and maximum p2p and rpc port numbers used by a test harness. The min port is + // inclusive while the max port is exclusive. + minPeerPort = 10000 + maxPeerPort = 35000 + minRPCPort = maxPeerPort + maxRPCPort = 60000 + // BlockVersion is the default block version used when generating blocks. + BlockVersion = 4 +) + +var ( + // current number of active test nodes. + numTestInstances = 0 + // processID is the process ID of the current running process, it is used to calculate ports based upon it when + // launching an rpc harnesses. The intent is to allow multiple process to run in parallel without port collisions. + // It should be noted however that there is still some small probability that there will be port collisions either + // due to other processes running or simply due to the stars aligning on the process IDs. + processID = os.Getpid() + // testInstances is a private package-level slice used to keep track of all active test harnesses. This global can + // be used to perform various "joins" shutdown several active harnesses after a test, etc. + testInstances = make(map[string]*Harness) + // Used to protest concurrent access to above declared variables. + harnessStateMtx sync.RWMutex +) + +// HarnessTestCase represents a test-case which utilizes an instance of the Harness to exercise functionality. +type HarnessTestCase func(r *Harness, t *testing.T) + +// Harness fully encapsulates an active pod process to provide a unified platform for creating rpc driven integration +// tests involving pod. The active pod node will typically be run in simnet mode in order to allow for easy generation +// of test blockchains. The active pod process is fully managed by Harness, which handles the necessary initialization, +// and teardown of the process along with any temporary directories created as a result. Multiple Harness instances may +// be run concurrently, in order to allow for testing complex scenarios involving multiple nodes. The harness also +// includes an in-memory wallet to streamline various classes of tests. +type Harness struct { + // ActiveNet is the parameters of the blockchain the Harness belongs to. + ActiveNet *chaincfg.Params + Node *rpcclient.Client + node *node + handlers *rpcclient.NotificationHandlers + wallet *memWallet + testNodeDir string + maxConnRetries int + nodeNum int + sync.Mutex +} + +// New creates and initializes new instance of the rpc test harness. Optionally, websocket handlers and a specified +// configuration may be passed. In the case that a nil config is passed, a default configuration will be used. NOTE: +// This function is safe for concurrent access. +func New( + activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, + extraArgs []string, +) (*Harness, error) { + harnessStateMtx.Lock() + defer harnessStateMtx.Unlock() + // Add a flag for the appropriate network type based on the provided chain chaincfg. + switch activeNet.Net { + case wire.MainNet: + // No extra flags since mainnet is the default + case wire.TestNet3: + extraArgs = append(extraArgs, "--testnet") + case wire.TestNet: + extraArgs = append(extraArgs, "--regtest") + case wire.SimNet: + extraArgs = append(extraArgs, "--simnet") + default: + return nil, fmt.Errorf( + "rpctest.New must be called with one " + + "of the supported chain networks", + ) + } + testDir, e := baseDir() + if e != nil { + return nil, e + } + harnessID := strconv.Itoa(numTestInstances) + nodeTestData, e := ioutil.TempDir(testDir, "harness-"+harnessID) + if e != nil { + return nil, e + } + certFile := filepath.Join(nodeTestData, "rpc.cert") + keyFile := filepath.Join(nodeTestData, "rpc.key") + if e = genCertPair(certFile, keyFile); E.Chk(e) { + return nil, e + } + wallet, e := newMemWallet(activeNet, uint32(numTestInstances)) + if e != nil { + return nil, e + } + miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr) + extraArgs = append(extraArgs, miningAddr) + config, e := newConfig("rpctest", certFile, keyFile, extraArgs) + if e != nil { + return nil, e + } + // Generate p2p+rpc listening addresses. + config.listen, config.rpcListen = generateListeningAddresses() + // Create the testing node bounded to the simnet. + node, e := newNode(config, nodeTestData) + if e != nil { + return nil, e + } + nodeNum := numTestInstances + numTestInstances++ + if handlers == nil { + handlers = &rpcclient.NotificationHandlers{} + } + // If a handler for the OnFilteredBlock{Connected, Disconnected} callback callback has already been set, then create + // a wrapper callback which executes both the currently registered callback and the mem wallet's callback. + if handlers.OnFilteredBlockConnected != nil { + obc := handlers.OnFilteredBlockConnected + handlers.OnFilteredBlockConnected = func(height int32, header *wire.BlockHeader, filteredTxns []*util.Tx) { + wallet.IngestBlock(height, header, filteredTxns) + obc(height, header, filteredTxns) + } + } else { + // Otherwise, we can claim the callback ourselves. + handlers.OnFilteredBlockConnected = wallet.IngestBlock + } + if handlers.OnFilteredBlockDisconnected != nil { + obd := handlers.OnFilteredBlockDisconnected + handlers.OnFilteredBlockDisconnected = func(height int32, header *wire.BlockHeader) { + wallet.UnwindBlock(height, header) + obd(height, header) + } + } else { + handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock + } + h := &Harness{ + handlers: handlers, + node: node, + maxConnRetries: 20, + testNodeDir: nodeTestData, + ActiveNet: activeNet, + nodeNum: nodeNum, + wallet: wallet, + } + // Track this newly created test instance within the package level global map of all active test instances. + testInstances[h.testNodeDir] = h + return h, nil +} + +// SetUp initializes the rpc test state. Initialization includes: starting up a simnet node, creating a websockets +// client and connecting to the started node, and finally: optionally generating and submitting a testchain with a +// configurable number of mature coinbase outputs coinbase outputs. NOTE: This method and TearDown should always be +// called from the same goroutine as they are not concurrent safe. +func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) (e error) { + // Start the pod node itself. This spawns a new process which will be managed + if e = h.node.start(); E.Chk(e) { + return e + } + if e = h.connectRPCClient(); E.Chk(e) { + return e + } + h.wallet.Start() + // Filter transactions that pay to the coinbase associated with the wallet. + filterAddrs := []btcaddr.Address{h.wallet.coinbaseAddr} + if e = h.Node.LoadTxFilter(true, filterAddrs, nil); E.Chk(e) { + return e + } + // Ensure pod properly dispatches our registered call-back for each new block. Otherwise, the memWallet won't + // function properly. + if e = h.Node.NotifyBlocks(); E.Chk(e) { + return e + } + // Create a test chain with the desired number of mature coinbase outputs. + if createTestChain && numMatureOutputs != 0 { + numToGenerate := uint32(h.ActiveNet.CoinbaseMaturity) + + numMatureOutputs + _, e = h.Node.Generate(numToGenerate) + if e != nil { + return e + } + } + // Block until the wallet has fully synced up to the tip of the main chain. + _, height, e := h.Node.GetBestBlock() + if e != nil { + return e + } + ticker := time.NewTicker(time.Millisecond * 100) + for range ticker.C { + walletHeight := h.wallet.SyncedHeight() + if walletHeight == height { + break + } + } + ticker.Stop() + return nil +} + +// tearDown stops the running rpc test instance. All created processes are killed, and temporary directories removed. +// This function MUST be called with the harness state mutex held (for writes). +func (h *Harness) tearDown() (e error) { + if h.Node != nil { + h.Node.Shutdown() + } + if e := h.node.shutdown(); E.Chk(e) { + return e + } + if e := os.RemoveAll(h.testNodeDir); E.Chk(e) { + return e + } + delete(testInstances, h.testNodeDir) + return nil +} + +// TearDown stops the running rpc test instance. All created processes are killed, and temporary directories removed. +// NOTE: This method and SetUp should always be called from the same goroutine as they are not concurrent safe. +func (h *Harness) TearDown() (e error) { + harnessStateMtx.Lock() + defer harnessStateMtx.Unlock() + return h.tearDown() +} + +// connectRPCClient attempts to establish an RPC connection to the created pod process belonging to this Harness +// instance. If the initial connection attempt fails, this function will retry h. maxConnRetries times, backing off the +// time between subsequent attempts. If after h.maxConnRetries attempts we're not able to establish a connection, this +// function returns with an error. +func (h *Harness) connectRPCClient() (e error) { + var client *rpcclient.Client + rpcConf := h.node.config.rpcConnConfig() + for i := 0; i < h.maxConnRetries; i++ { + if client, e = rpcclient.New(&rpcConf, h.handlers, qu.T()); E.Chk(e) { + time.Sleep(time.Duration(i) * 50 * time.Millisecond) + continue + } + break + } + if client == nil { + return fmt.Errorf("connection timeout") + } + h.Node = client + h.wallet.SetRPCClient(client) + return nil +} + +// NewAddress returns a fresh address spendable by the Harness' internal wallet. This function is safe for concurrent +// access. +func (h *Harness) NewAddress() (btcaddr.Address, error) { + return h.wallet.NewAddress() +} + +// ConfirmedBalance returns the confirmed balance of the Harness' internal wallet. This function is safe for concurrent +// access. +func (h *Harness) ConfirmedBalance() amt.Amount { + return h.wallet.ConfirmedBalance() +} + +// SendOutputs creates signs and finally broadcasts a transaction spending the harness' available mature coinbase +// outputs creating new outputs according to targetOutputs. This function is safe for concurrent access. +func (h *Harness) SendOutputs( + targetOutputs []*wire.TxOut, + feeRate amt.Amount, +) (*chainhash.Hash, error) { + return h.wallet.SendOutputs(targetOutputs, feeRate) +} + +// SendOutputsWithoutChange creates and sends a transaction that pays to the specified outputs while observing the +// passed fee rate and ignoring a change output. The passed fee rate should be expressed in sat/b. This function is safe +// for concurrent access. +func (h *Harness) SendOutputsWithoutChange( + targetOutputs []*wire.TxOut, + feeRate amt.Amount, +) (*chainhash.Hash, error) { + return h.wallet.SendOutputsWithoutChange(targetOutputs, feeRate) +} + +// CreateTransaction returns a fully signed transaction paying to the specified outputs while observing the desired fee +// rate. The passed fee rate should be expressed in satoshis-per-byte. The transaction being created can optionally +// include a change output indicated by the change boolean. Any unspent outputs selected as inputs for the crafted +// transaction are marked as unspendable in order to avoid potential double-spends by future calls to this method. If +// the created transaction is cancelled for any reason then the selected inputs MUST be freed via a call to +// UnlockOutputs. Otherwise, the locked inputs won't be returned to the pool of spendable outputs. This function is safe +// for concurrent access. +func (h *Harness) CreateTransaction( + targetOutputs []*wire.TxOut, + feeRate amt.Amount, change bool, +) (*wire.MsgTx, error) { + return h.wallet.CreateTransaction(targetOutputs, feeRate, change) +} + +// UnlockOutputs unlocks any outputs which were previously marked as unspendabe due to being selected to fund a +// transaction via the CreateTransaction method. This function is safe for concurrent access. +func (h *Harness) UnlockOutputs(inputs []*wire.TxIn) { + h.wallet.UnlockOutputs(inputs) +} + +// RPCConfig returns the harnesses current rpc configuration. This allows other potential RPC clients created within +// tests to connect to a given test harness instance. +func (h *Harness) RPCConfig() rpcclient.ConnConfig { + return h.node.config.rpcConnConfig() +} + +// P2PAddress returns the harness' P2P listening address. This allows potential peers ( such as SPV peers) created +// within tests to connect to a given test harness instance. +func (h *Harness) P2PAddress() string { + return h.node.config.listen +} + +// GenerateAndSubmitBlock creates a block whose contents include the passed transactions and submits it to the running +// simnet node. For generating blocks with only a coinbase tx, callers can simply pass nil instead of transactions to be +// mined. Additionally, a custom block version can be set by the caller. A blockVersion of -1 indicates that the current +// default block version should be used. An uninitialized time.Time should be used for the blockTime parameter if one +// doesn't wish to set a custom time. This function is safe for concurrent access. +func (h *Harness) GenerateAndSubmitBlock( + txns []*util.Tx, blockVersion uint32, + blockTime time.Time, +) (*block.Block, error) { + return h.GenerateAndSubmitBlockWithCustomCoinbaseOutputs( + txns, + blockVersion, blockTime, []wire.TxOut{}, + ) +} + +// GenerateAndSubmitBlockWithCustomCoinbaseOutputs creates a block whose contents include the passed coinbase outputs +// and transactions and submits it to the running simnet node. For generating blocks with only a coinbase tx, callers +// can simply pass nil instead of transactions to be mined. Additionally, a custom block version can be set by the +// caller. A blockVersion of -1 indicates that the current default block version should be used. An uninitialized +// time.Time should be used for the blockTime parameter if one doesn't wish to set a custom time. The mineTo list of +// outputs will be added to the coinbase; this is not checked for correctness until the block is submitted; thus, it is +// the caller's responsibility to ensure that the outputs are correct. If the list is empty, the coinbase reward goes to +// the wallet managed by the Harness. This function is safe for concurrent access. +func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs( + txns []*util.Tx, blockVersion uint32, blockTime time.Time, + mineTo []wire.TxOut, +) (*block.Block, error) { + h.Lock() + defer h.Unlock() + if blockVersion == ^uint32(0) { + blockVersion = BlockVersion + } + prevBlockHash, prevBlockHeight, e := h.Node.GetBestBlock() + if e != nil { + return nil, e + } + mBlock, e := h.Node.GetBlock(prevBlockHash) + if e != nil { + return nil, e + } + prevBlock := block.NewBlock(mBlock) + prevBlock.SetHeight(prevBlockHeight) + // Create a new block including the specified transactions + newBlock, e := CreateBlock( + prevBlock, txns, int32(blockVersion), + blockTime, h.wallet.coinbaseAddr, mineTo, h.ActiveNet, + ) + if e != nil { + return nil, e + } + // Submit the block to the simnet node. + if e := h.Node.SubmitBlock(newBlock, nil); E.Chk(e) { + return nil, e + } + return newBlock, nil +} + +// generateListeningAddresses returns two strings representing listening addresses designated for the current rpc test. +// If there haven't been any test instances created, the default ports are used. Otherwise in order to support multiple +// test nodes running at once the p2p and rpc port are incremented after each initialization. +func generateListeningAddresses() (string, string) { + localhost := "127.0.0.1" + portString := func(minPort, maxPort int) string { + port := minPort + numTestInstances + ((20 * processID) % + (maxPort - minPort)) + return strconv.Itoa(port) + } + p2p := net.JoinHostPort(localhost, portString(minPeerPort, maxPeerPort)) + rpc := net.JoinHostPort(localhost, portString(minRPCPort, maxRPCPort)) + return p2p, rpc +} + +// baseDir is the directory path of the temp directory for all rpctest files. +func baseDir() (string, error) { + dirPath := filepath.Join(os.TempDir(), "pod", "rpctest") + e := os.MkdirAll(dirPath, 0755) + return dirPath, e +} diff --git a/cmd/node/integration/rpctest/rpc_harness_test.go b/cmd/node/integration/rpctest/rpc_harness_test.go new file mode 100644 index 0000000..af8717f --- /dev/null +++ b/cmd/node/integration/rpctest/rpc_harness_test.go @@ -0,0 +1,594 @@ +package rpctest + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +func testSendOutputs(r *Harness, t *testing.T) { + genSpend := func(amt amt.Amount) *chainhash.Hash { + // Grab a fresh address from the wallet. + addr, e := r.NewAddress() + if e != nil { + t.Fatalf("unable to get new address: %v", e) + } + // Next, send amt DUO to this address, + // spending from one of our mature coinbase outputs. + addrScript, e := txscript.PayToAddrScript(addr) + if e != nil { + t.Fatalf("unable to generate pkscript to addr: %v", e) + } + output := wire.NewTxOut(int64(amt), addrScript) + txid, e := r.SendOutputs([]*wire.TxOut{output}, 10) + if e != nil { + t.Fatalf("coinbase spend failed: %v", e) + } + return txid + } + assertTxMined := func(txid *chainhash.Hash, blockHash *chainhash.Hash) { + block, e := r.Node.GetBlock(blockHash) + if e != nil { + t.Fatalf("unable to get block: %v", e) + } + numBlockTxns := len(block.Transactions) + if numBlockTxns < 2 { + t.Fatalf( + "crafted transaction wasn't mined, block should have "+ + "at least %v transactions instead has %v", 2, numBlockTxns, + ) + } + minedTx := block.Transactions[1] + txHash := minedTx.TxHash() + if txHash != *txid { + t.Fatalf("txid's don't match, %v vs %v", txHash, txid) + } + } + // First, generate a small spend which will require only a single input. + txid := genSpend(5 * amt.SatoshiPerBitcoin) + // Generate a single block, the transaction the wallet created should be found in this block. + blockHashes, e := r.Node.Generate(1) + if e != nil { + t.Fatalf("unable to generate single block: %v", e) + } + assertTxMined(txid, blockHashes[0]) + // Next, generate a spend much greater than the block reward. This transaction should also have been mined properly. + txid = genSpend(500 * amt.SatoshiPerBitcoin) + blockHashes, e = r.Node.Generate(1) + if e != nil { + t.Fatalf("unable to generate single block: %v", e) + } + assertTxMined(txid, blockHashes[0]) +} + +func assertConnectedTo(t *testing.T, nodeA *Harness, nodeB *Harness) { + nodeAPeers, e := nodeA.Node.GetPeerInfo() + if e != nil { + t.Fatalf("unable to get nodeA's peer info") + } + nodeAddr := nodeB.node.config.listen + addrFound := false + for _, peerInfo := range nodeAPeers { + if peerInfo.Addr == nodeAddr { + addrFound = true + break + } + } + if !addrFound { + t.Fatal("nodeA not connected to nodeB") + } +} + +func testConnectNode(r *Harness, t *testing.T) { + // Create a fresh test harness. + harness, e := New(&chaincfg.SimNetParams, nil, nil) + if e != nil { + t.Fatal(e) + } + if e := harness.SetUp(false, 0); E.Chk(e) { + t.Fatalf("unable to complete rpctest setup: %v", e) + } + defer func() { + if e := harness.TearDown(); E.Chk(e) { + } + }() + // Establish a p2p connection from our new local harness to the main harness. + if e := ConnectNode(harness, r); E.Chk(e) { + t.Fatalf("unable to connect local to main harness: %v", e) + } + // The main harness should show up in our local harness' peer's list, and vice verse. + assertConnectedTo(t, harness, r) +} + +func testTearDownAll(t *testing.T) { + // Grab a local copy of the currently active harnesses before attempting to tear them all down. + initialActiveHarnesses := ActiveHarnesses() + // Tear down all currently active harnesses. + if e := TearDownAll(); E.Chk(e) { + t.Fatalf("unable to teardown all harnesses: %v", e) + } + // The global testInstances map should now be fully purged with no active test harnesses remaining. + if len(ActiveHarnesses()) != 0 { + t.Fatalf("test harnesses still active after TearDownAll") + } + for _, harness := range initialActiveHarnesses { + // Ensure all test directories have been deleted. + var e error + if _, e = os.Stat(harness.testNodeDir); e == nil { + t.Errorf("created test datadir was not deleted.") + } + } +} +func testActiveHarnesses(r *Harness, t *testing.T) { + numInitialHarnesses := len(ActiveHarnesses()) + // Create a single test harness. + harness1, e := New(&chaincfg.SimNetParams, nil, nil) + if e != nil { + t.Fatal(e) + } + defer func() { + if e := harness1.TearDown(); E.Chk(e) { + } + }() + // With the harness created above, a single harness should be detected as active. + numActiveHarnesses := len(ActiveHarnesses()) + if !(numActiveHarnesses > numInitialHarnesses) { + t.Fatalf( + "ActiveHarnesses not updated, should have an " + + "additional test harness listed.", + ) + } +} +func testJoinMempools(r *Harness, t *testing.T) { + // Assert main test harness has no transactions in its mempool. + pooledHashes, e := r.Node.GetRawMempool() + if e != nil { + t.Fatalf("unable to get mempool for main test harness: %v", e) + } + if len(pooledHashes) != 0 { + t.Fatal("main test harness mempool not empty") + } + // Create a local test harness with only the genesis block. The nodes will be synced below so the same transaction + // can be sent to both nodes without it being an orphan. + var harness *Harness + harness, e = New(&chaincfg.SimNetParams, nil, nil) + if e != nil { + t.Fatal(e) + } + if e = harness.SetUp(false, 0); E.Chk(e) { + t.Fatalf("unable to complete rpctest setup: %v", e) + } + defer func() { + if e = harness.TearDown(); E.Chk(e) { + } + }() + nodeSlice := []*Harness{r, harness} + // Both mempools should be considered synced as they are empty. Therefore, this should return instantly. + if e = JoinNodes(nodeSlice, Mempools); E.Chk(e) { + t.Fatalf("unable to join node on mempools: %v", e) + } + // Generate a coinbase spend to a new address within the main harness' mempool. + var addr btcaddr.Address + if addr, e = r.NewAddress(); E.Chk(e) { + } + var addrScript []byte + addrScript, e = txscript.PayToAddrScript(addr) + if e != nil { + t.Fatalf("unable to generate pkscript to addr: %v", e) + } + output := wire.NewTxOut(5e8, addrScript) + var testTx *wire.MsgTx + testTx, e = r.CreateTransaction([]*wire.TxOut{output}, 10, true) + if e != nil { + t.Fatalf("coinbase spend failed: %v", e) + } + if _, e = r.Node.SendRawTransaction(testTx, true); E.Chk(e) { + t.Fatalf("send transaction failed: %v", e) + } + // Wait until the transaction shows up to ensure the two mempools are not the same. + harnessSynced := qu.T() + go func() { + for { + var poolHashes []*chainhash.Hash + poolHashes, e = r.Node.GetRawMempool() + if e != nil { + t.Fatalf("failed to retrieve harness mempool: %v", e) + } + if len(poolHashes) > 0 { + break + } + time.Sleep(time.Millisecond * 100) + } + harnessSynced <- struct{}{} + }() + select { + case <-harnessSynced.Wait(): + case <-time.After(time.Minute): + t.Fatalf("harness node never received transaction") + } + // This select case should fall through to the default as the goroutine should be blocked on the JoinNodes call. + poolsSynced := qu.T() + go func() { + if e = JoinNodes(nodeSlice, Mempools); E.Chk(e) { + t.Fatalf("unable to join node on mempools: %v", e) + } + poolsSynced <- struct{}{} + }() + select { + case <-poolsSynced.Wait(): + t.Fatalf("mempools detected as synced yet harness has a new tx") + default: + } + // Establish an outbound connection from the local harness to the main harness and wait for the chains to be synced. + if e = ConnectNode(harness, r); E.Chk(e) { + t.Fatalf("unable to connect harnesses: %v", e) + } + if e = JoinNodes(nodeSlice, Blocks); E.Chk(e) { + t.Fatalf("unable to join node on blocks: %v", e) + } + // Send the transaction to the local harness which will result in synced mempools. + if _, e = harness.Node.SendRawTransaction(testTx, true); E.Chk(e) { + t.Fatalf("send transaction failed: %v", e) + } + // Select once again with a special timeout case after 1 minute. The goroutine above should now be blocked on + // sending into the unbuffered channel. The send should immediately succeed. In order to avoid the test hanging + // indefinitely, a 1 minute timeout is in place. + select { + case <-poolsSynced.Wait(): + case <-time.After(time.Minute): + t.Fatalf("mempools never detected as synced") + } +} +func testJoinBlocks(r *Harness, t *testing.T) { + // Create a second harness with only the genesis block so it is behind the main harness. + harness, e := New(&chaincfg.SimNetParams, nil, nil) + if e != nil { + t.Fatal(e) + } + if e := harness.SetUp(false, 0); E.Chk(e) { + t.Fatalf("unable to complete rpctest setup: %v", e) + } + defer func() { + if e := harness.TearDown(); E.Chk(e) { + } + }() + nodeSlice := []*Harness{r, harness} + blocksSynced := qu.T() + go func() { + if e := JoinNodes(nodeSlice, Blocks); E.Chk(e) { + t.Fatalf("unable to join node on blocks: %v", e) + } + blocksSynced <- struct{}{} + }() + // This select case should fall through to the default as the goroutine should be blocked on the JoinNodes calls. + select { + case <-blocksSynced.Wait(): + t.Fatalf("blocks detected as synced yet local harness is behind") + default: + } + // Connect the local harness to the main harness which will sync the chains. + if e := ConnectNode(harness, r); E.Chk(e) { + t.Fatalf("unable to connect harnesses: %v", e) + } + // Select once again with a special timeout case after 1 minute. The goroutine above should now be blocked on + // sending into the unbuffered channel. The send should immediately succeed. In order to avoid the test hanging + // indefinitely, a 1 minute timeout is in place. + select { + case <-blocksSynced.Wait(): + case <-time.After(time.Minute): + t.Fatalf("blocks never detected as synced") + } +} +func testGenerateAndSubmitBlock(r *Harness, t *testing.T) { + // Generate a few test spend transactions. + addr, e := r.NewAddress() + if e != nil { + t.Fatalf("unable to generate new address: %v", e) + } + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + t.Fatalf("unable to create script: %v", e) + } + output := wire.NewTxOut(amt.SatoshiPerBitcoin.Int64(), pkScript) + const numTxns = 5 + txns := make([]*util.Tx, 0, numTxns) + var tx *wire.MsgTx + for i := 0; i < numTxns; i++ { + tx, e = r.CreateTransaction([]*wire.TxOut{output}, 10, true) + if e != nil { + t.Fatalf("unable to create tx: %v", e) + } + txns = append(txns, util.NewTx(tx)) + } + // Now generate a block with the default block version, and a zeroed out time. + block, e := r.GenerateAndSubmitBlock(txns, ^uint32(0), time.Time{}) + if e != nil { + t.Fatalf("unable to generate block: %v", e) + } + // Ensure that all created transactions were included, and that the block version was properly set to the default. + numBlocksTxns := len(block.Transactions()) + if numBlocksTxns != numTxns+1 { + t.Fatalf( + "block did not include all transactions: "+ + "expected %v, got %v", numTxns+1, numBlocksTxns, + ) + } + blockVersion := block.WireBlock().Header.Version + if blockVersion != BlockVersion { + t.Fatalf( + "block version is not default: expected %v, got %v", + BlockVersion, blockVersion, + ) + } + // Next generate a block with a "non-standard" block version along with time stamp a minute after the previous + // block's timestamp. + timestamp := block.WireBlock().Header.Timestamp.Add(time.Minute) + targetBlockVersion := uint32(1337) + block, e = r.GenerateAndSubmitBlock(nil, targetBlockVersion, timestamp) + if e != nil { + t.Fatalf("unable to generate block: %v", e) + } + // Finally ensure that the desired block version and timestamp were set properly. + header := block.WireBlock().Header + blockVersion = header.Version + if blockVersion != int32(targetBlockVersion) { + t.Fatalf( + "block version mismatch: expected %v, got %v", + targetBlockVersion, blockVersion, + ) + } + if !timestamp.Equal(header.Timestamp) { + t.Fatalf( + "header time stamp mismatch: expected %v, got %v", + timestamp, header.Timestamp, + ) + } +} +func testGenerateAndSubmitBlockWithCustomCoinbaseOutputs( + r *Harness, + t *testing.T, +) { + // Generate a few test spend transactions. + addr, e := r.NewAddress() + if e != nil { + t.Fatalf("unable to generate new address: %v", e) + } + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + t.Fatalf("unable to create script: %v", e) + } + output := wire.NewTxOut(amt.SatoshiPerBitcoin.Int64(), pkScript) + const numTxns = 5 + txns := make([]*util.Tx, 0, numTxns) + for i := 0; i < numTxns; i++ { + var tx *wire.MsgTx + tx, e = r.CreateTransaction([]*wire.TxOut{output}, 10, true) + if e != nil { + t.Fatalf("unable to create tx: %v", e) + } + txns = append(txns, util.NewTx(tx)) + } + // Now generate a block with the default block version, a zero'd out time, and a burn output. + block, e := r.GenerateAndSubmitBlockWithCustomCoinbaseOutputs( + txns, + ^uint32(0), time.Time{}, []wire.TxOut{ + { + Value: 0, + PkScript: []byte{}, + }, + }, + ) + if e != nil { + t.Fatalf("unable to generate block: %v", e) + } + // Ensure that all created transactions were included, and that the block version was properly set to the default. + numBlocksTxns := len(block.Transactions()) + if numBlocksTxns != numTxns+1 { + t.Fatalf( + "block did not include all transactions: "+ + "expected %v, got %v", numTxns+1, numBlocksTxns, + ) + } + blockVersion := block.WireBlock().Header.Version + if blockVersion != BlockVersion { + t.Fatalf( + "block version is not default: expected %v, got %v", + BlockVersion, blockVersion, + ) + } + // Next generate a block with a "non-standard" block version along with time stamp a minute after the previous + // block's timestamp. + timestamp := block.WireBlock().Header.Timestamp.Add(time.Minute) + targetBlockVersion := uint32(1337) + block, e = r.GenerateAndSubmitBlockWithCustomCoinbaseOutputs( + nil, + targetBlockVersion, timestamp, []wire.TxOut{ + { + Value: 0, + PkScript: []byte{}, + }, + }, + ) + if e != nil { + t.Fatalf("unable to generate block: %v", e) + } + // Finally ensure that the desired block version and timestamp were set properly. + header := block.WireBlock().Header + blockVersion = header.Version + if blockVersion != int32(targetBlockVersion) { + t.Fatalf( + "block version mismatch: expected %v, got %v", + targetBlockVersion, blockVersion, + ) + } + if !timestamp.Equal(header.Timestamp) { + t.Fatalf( + "header time stamp mismatch: expected %v, got %v", + timestamp, header.Timestamp, + ) + } +} +func testMemWalletReorg(r *Harness, t *testing.T) { + // Create a fresh harness, we'll be using the main harness to force a re-org on this local harness. + harness, e := New(&chaincfg.SimNetParams, nil, nil) + if e != nil { + t.Fatal(e) + } + if e := harness.SetUp(true, 5); E.Chk(e) { + t.Fatalf("unable to complete rpctest setup: %v", e) + } + defer func() { + if e := harness.TearDown(); E.Chk(e) { + } + }() + // The internal wallet of this harness should now have 250 DUO. + expectedBalance := 250 * amt.SatoshiPerBitcoin + walletBalance := harness.ConfirmedBalance() + if expectedBalance != walletBalance { + t.Fatalf( + "wallet balance incorrect: expected %v, got %v", + expectedBalance, walletBalance, + ) + } + // Now connect this local harness to the main harness then wait for their chains to synchronize. + if e := ConnectNode(harness, r); E.Chk(e) { + t.Fatalf("unable to connect harnesses: %v", e) + } + nodeSlice := []*Harness{r, harness} + if e := JoinNodes(nodeSlice, Blocks); E.Chk(e) { + t.Fatalf("unable to join node on blocks: %v", e) + } + // The original wallet should now have a balance of 0 DUO as its entire chain should have been decimated in favor of + // the main harness' chain. + expectedBalance = amt.Amount(0) + walletBalance = harness.ConfirmedBalance() + if expectedBalance != walletBalance { + t.Fatalf( + "wallet balance incorrect: expected %v, got %v", + expectedBalance, walletBalance, + ) + } +} +func testMemWalletLockedOutputs(r *Harness, t *testing.T) { + // Obtain the initial balance of the wallet at this point. + startingBalance := r.ConfirmedBalance() + // First, create a signed transaction spending some outputs. + addr, e := r.NewAddress() + if e != nil { + t.Fatalf("unable to generate new address: %v", e) + } + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + t.Fatalf("unable to create script: %v", e) + } + outputAmt := 50 * amt.SatoshiPerBitcoin + output := wire.NewTxOut(int64(outputAmt), pkScript) + tx, e := r.CreateTransaction([]*wire.TxOut{output}, 10, true) + if e != nil { + t.Fatalf("unable to create transaction: %v", e) + } + // The current wallet balance should now be at least 50 DUO less (accounting for fees) than the period balance + currentBalance := r.ConfirmedBalance() + if !(currentBalance <= startingBalance-outputAmt) { + t.Fatalf( + "spent outputs not locked: previous balance %v, "+ + "current balance %v", startingBalance, currentBalance, + ) + } + // Now unlocked all the spent inputs within the unbroadcast signed transaction. The current balance should now be + // exactly that of the starting balance. + r.UnlockOutputs(tx.TxIn) + currentBalance = r.ConfirmedBalance() + if currentBalance != startingBalance { + t.Fatalf( + "current and starting balance should now match: "+ + "expected %v, got %v", startingBalance, currentBalance, + ) + } +} + +var harnessTestCases = []HarnessTestCase{ + testSendOutputs, + testConnectNode, + testActiveHarnesses, + testJoinBlocks, + testJoinMempools, // Depends on results of testJoinBlocks + testGenerateAndSubmitBlock, + testGenerateAndSubmitBlockWithCustomCoinbaseOutputs, + testMemWalletReorg, + testMemWalletLockedOutputs, +} + +var mainHarness *Harness + +const ( + numMatureOutputs = 25 +) + +func TestMain(m *testing.M) { + var e error + mainHarness, e = New(&chaincfg.SimNetParams, nil, nil) + if e != nil { + fmt.Println("unable to create main harness: ", e) + os.Exit(1) + } + // Initialize the main mining node with a chain of length 125, providing 25 mature coinbases to allow spending from + // for testing purposes. + if e = mainHarness.SetUp(true, numMatureOutputs); E.Chk(e) { + fmt.Println("unable to setup test chain: ", e) + // Even though the harness was not fully setup, it still needs to be torn down to ensure all resources such as + // temp directories are cleaned up. The error is intentionally ignored since this is already an error path and + // nothing else could be done about it anyways. + _ = mainHarness.TearDown() + os.Exit(1) + } + exitCode := m.Run() + // Clean up any active harnesses that are still currently running. + if len(ActiveHarnesses()) > 0 { + if e := TearDownAll(); E.Chk(e) { + fmt.Println("unable to tear down chain: ", e) + os.Exit(1) + } + } + os.Exit(exitCode) +} +func TestHarness(t *testing.T) { + // We should have (numMatureOutputs * 50 DUO) of mature unspendable + // outputs. + expectedBalance := numMatureOutputs * 50 * amt.SatoshiPerBitcoin + harnessBalance := mainHarness.ConfirmedBalance() + if harnessBalance != expectedBalance { + t.Fatalf( + "expected wallet balance of %v instead have %v", + expectedBalance, harnessBalance, + ) + } + // Current tip should be at a height of numMatureOutputs plus the required number of blocks for coinbase maturity. + nodeInfo, e := mainHarness.Node.GetInfo() + if e != nil { + t.Fatalf("unable to execute getinfo on node: %v", e) + } + expectedChainHeight := numMatureOutputs + uint32(mainHarness.ActiveNet.CoinbaseMaturity) + if uint32(nodeInfo.Blocks) != expectedChainHeight { + t.Errorf( + "Chain height is %v, should be %v", + nodeInfo.Blocks, expectedChainHeight, + ) + } + for _, testCase := range harnessTestCases { + testCase(mainHarness, t) + } + testTearDownAll(t) +} diff --git a/cmd/node/integration/rpctest/utils.go b/cmd/node/integration/rpctest/utils.go new file mode 100644 index 0000000..2da3f75 --- /dev/null +++ b/cmd/node/integration/rpctest/utils.go @@ -0,0 +1,141 @@ +package rpctest + +import ( + "reflect" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/rpcclient" +) + +// JoinType is an enum representing a particular type of "node join". A node +// join is a synchronization tool used to wait until a subset of nodes have a +// consistent state with respect to an attribute. +type JoinType uint8 + +const ( + // Blocks is a JoinType which waits until all nodes share the same block + // height. + Blocks JoinType = iota + // Mempools is a JoinType which blocks until all nodes have identical mempool. + Mempools +) + +// JoinNodes is a synchronization tool used to block until all passed nodes are +// fully synced with respect to an attribute. This function will block for a +// period of time, finally returning once all nodes are synced according to the +// passed JoinType. This function be used to to ensure all active test harnesses +// are at a consistent state before proceeding to an assertion or check within +// rpc tests. +func JoinNodes(nodes []*Harness, joinType JoinType) (e error) { + switch joinType { + case Blocks: + return syncBlocks(nodes) + case Mempools: + return syncMempools(nodes) + } + return nil +} + +// syncMempools blocks until all nodes have identical mempools. +func syncMempools(nodes []*Harness) (e error) { + poolsMatch := false +retry: + for !poolsMatch { + firstPool, e := nodes[0].Node.GetRawMempool() + if e != nil { + return e + } + // If all nodes have an identical mempool with respect to the first node, + // then we're done. Otherwise drop back to the top of the loop and retry + // after a short wait period. + for _, node := range nodes[1:] { + nodePool, e := node.Node.GetRawMempool() + if e != nil { + return e + } + if !reflect.DeepEqual(firstPool, nodePool) { + time.Sleep(time.Millisecond * 100) + continue retry + } + } + poolsMatch = true + } + return nil +} + +// syncBlocks blocks until all nodes report the same best chain. +func syncBlocks(nodes []*Harness) (e error) { + blocksMatch := false +retry: + for !blocksMatch { + var prevHash *chainhash.Hash + var prevHeight int32 + + for _, node := range nodes { + blockHash, blockHeight, e := node.Node.GetBestBlock() + if e != nil { + return e + } + if prevHash != nil && (*blockHash != *prevHash || + blockHeight != prevHeight) { + time.Sleep(time.Millisecond * 100) + continue retry + } + prevHash, prevHeight = blockHash, blockHeight + } + blocksMatch = true + } + return nil +} + +// ConnectNode establishes a new peer-to-peer connection between the "from" harness and the "to" harness. The connection +// made is flagged as persistent therefore in the case of disconnects, "from" will attempt to reestablish a connection +// to the "to" harness. +func ConnectNode(from *Harness, to *Harness) (e error) { + peerInfo, e := from.Node.GetPeerInfo() + if e != nil { + return e + } + numPeers := len(peerInfo) + targetAddr := to.node.config.listen + if e = from.Node.AddNode(targetAddr, rpcclient.ANAdd); E.Chk(e) { + return e + } + // Block until a new connection has been established. + peerInfo, e = from.Node.GetPeerInfo() + if e != nil { + return e + } + for len(peerInfo) <= numPeers { + peerInfo, e = from.Node.GetPeerInfo() + if e != nil { + return e + } + } + return nil +} + +// TearDownAll tears down all active test harnesses. +func TearDownAll() (e error) { + harnessStateMtx.Lock() + defer harnessStateMtx.Unlock() + for _, harness := range testInstances { + if e := harness.tearDown(); E.Chk(e) { + return e + } + } + return nil +} + +// ActiveHarnesses returns a slice of all currently active test harnesses. A test harness if considered "active" if it +// has been created, but not yet torn down. +func ActiveHarnesses() []*Harness { + harnessStateMtx.RLock() + defer harnessStateMtx.RUnlock() + activeNodes := make([]*Harness, 0, len(testInstances)) + for _, harness := range testInstances { + activeNodes = append(activeNodes, harness) + } + return activeNodes +} diff --git a/cmd/node/log.go b/cmd/node/log.go new file mode 100644 index 0000000..d585a50 --- /dev/null +++ b/cmd/node/log.go @@ -0,0 +1,9 @@ +package node + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) diff --git a/cmd/node/node/_service_windows.go_ b/cmd/node/node/_service_windows.go_ new file mode 100755 index 0000000..d63f4f7 --- /dev/null +++ b/cmd/node/node/_service_windows.go_ @@ -0,0 +1,295 @@ +package node + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "github.com/p9c/p9/pod/podconfig" + + "github.com/btcsuite/winsvc/eventlog" + "github.com/btcsuite/winsvc/mgr" + "github.com/btcsuite/winsvc/svc" +) + +const ( + // svcName is the name of pod service. + svcName = "podsvc" + // svcDisplayName is the service name that will be shown in the windows + // services list. Not the svcName is the "real" name which is used to + // control the service. This is only for display purposes. + svcDisplayName = "Pod Service" + // svcDesc is the description of the service. + svcDesc = "Downloads and stays synchronized with the bitcoin block " + + "chain and provides chain services to applications." +) + +// elog is used to send messages to the Windows event log. +var elog *eventlog.Log + +// logServiceStartOfDay logs information about pod when the main server has +// been started to the Windows event log. +func logServiceStartOfDay(srvr *server) { + var message string + message += fmt.Sprintf("Version %s\n", version()) + message += fmt.Sprintf("Configuration directory: %s\n", defaultHomeDir) + message += fmt.Sprintf("Configuration file: %s\n", cfg.ConfigFile) + message += fmt.Sprintf("Data directory: %s\n", cfg.DataDir) + eL.inf.Ln(1, message) +} + +// podService houses the main service handler which handles all service +// updates and launching podMain. +type podService struct{} + +// Execute is the main entry point the winsvc package calls when receiving +// information from the Windows service control manager. +// It launches the long-running podMain (which is the real meat of pod), +// handles service change requests, +// and notifies the service control manager of changes. +func (s *podService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + // Service start is pending. + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + // Start podMain in a separate goroutine so the service can start + // quickly. Shutdown (along with a potential error) is reported via + // doneChan. serverChan is notified with the main server instance once + // it is started so it can be gracefully stopped. + doneChan := make(chan error) + serverChan := make(chan *server) + go func() { + e := podMain(serverChan) + doneChan <- err + }() + // Service is now started. + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + var mainServer *server +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + // Service stop is pending. + // Don't accept any more commands while pending. + changes <- svc.Status{State: svc.StopPending} + // Signal the main function to exit. + shutdownRequestChannel <- struct{}{} + default: + eL.Error(1, fmt.Sprintf( + "Unexpected control request #%d.", c, + ), + ) + } + case srvr := <-serverChan: + mainServer = srvr + logServiceStartOfDay(mainServer) + case e := <-doneChan: + if e != nil { + L.eL.Error(1, err.Error()) + } + break loop + } + } + // Service is now stopped. + changes <- svc.Status{State: svc.Stopped} + return false, 0 +} + +// installService attempts to install the pod service. +// Typically this should be done by the msi installer, +// but it is provided here since it can be useful for development. +func installService() (e error) { + // Get the path of the current executable. This is needed because os. + // Args[0] can vary depending on how the application was launched. + // For example, under cmd.exe it will only be the name of the app without + // the path or extension but under mingw it will be the full path + // including the extension. + exePath, e := filepath.Abs(os.Args[0]) + if e != nil { + L. + return err + } + if filepath.Ext(exePath) == "" { + exePath += ".exe" + } + // Connect to the windows service manager. + serviceManager, e := mgr.Connect() + if e != nil { + L. + return err + } + defer serviceManager.Disconnect() + // Ensure the service doesn't already exist. + service, e := serviceManager.OpenService(svcName) + if e == nil { + service.Close() + return fmt.Errorf("service %s already exists", svcName) + } + // Install the service. + service, e = serviceManager.CreateService(svcName, exePath, mgr.Config{ + DisplayName: svcDisplayName, + Description: svcDesc, + }, + ) + if e != nil { + L. + return err + } + defer service.Close() + // Support events to the event log using the standard "standard" Windows + // EventCreate.exe message file. + // This allows easy logging of custom messges instead of needing to + // create our own message catalog. + eventlog.Remove(svcName) + eventsSupported := uint32(eventL.Error | eventlog.Warning | eventlog.Info) + return eventlog.InstallAsEventCreate(svcName, eventsSupported) +} + +// removeService attempts to uninstall the pod service. +// Typically this should be done by the msi uninstaller, +// but it is provided here since it can be useful for development. +// Not the eventlog entry is intentionally not removed since it would +// invalidate any existing event log messages. +func removeService() (e error) { + // Connect to the windows service manager. + serviceManager, e := mgr.Connect() + if e != nil { + L. + return err + } + defer serviceManager.Disconnect() + // Ensure the service exists. + service, e := serviceManager.OpenService(svcName) + if e != nil { + L. + return fmt.Errorf("service %s is not installed", svcName) + } + defer service.Close() + // Remove the service. + return service.Delete() +} + +// startService attempts to start the pod service. +func startService() (e error) { + // Connect to the windows service manager. + serviceManager, e := mgr.Connect() + if e != nil { + L. + return err + } + defer serviceManager.Disconnect() + service, e := serviceManager.OpenService(svcName) + if e != nil { + L. + return fmt.Errorf("could not access service: %v", err) + } + defer service.Close() + e = service.Start(os.Args) + if e != nil { + L. + return fmt.Errorf("could not start service: %v", err) + } + return nil +} + +// controlService allows commands which change the status of the service. +// It also waits for up to 10 seconds for the service to change to the passed +// state. +func controlService(c svc.Cmd, to svc.State) (e error) { + // Connect to the windows service manager. + serviceManager, e := mgr.Connect() + if e != nil { + L. + return err + } + defer serviceManager.Disconnect() + service, e := serviceManager.OpenService(svcName) + if e != nil { + L. + return fmt.Errorf("could not access service: %v", err) + } + defer service.Close() + status, e := service.Control(c) + if e != nil { + L. + return fmt.Errorf("could not send control=%d: %v", c, err) + } + // Send the control message. + timeout := time.Now().Add(10 * time.Second) + for status.State != to { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for service to go "+ + "to state=%d", to, + ) + } + time.Sleep(300 * time.Millisecond) + status, e = service.Query() + if e != nil { + L. + return fmt.Errorf("could not retrieve service "+ + "status: %v", err, + ) + } + } + return nil +} + +// performServiceCommand attempts to run one of the supported service +// commands provided on the command line via the service command flag. +// An appropriate error is returned if an invalid command is specified. +func performServiceCommand(command string) (e error) { + var e error + switch command { + case "install": + e = installService() + case "remove": + e = removeService() + case "start": + e = startService() + case "stop": + e = controlService(svc.Stop, svc.Stopped) + default: + e = fmt.Errorf("invalid service command [%s]", command) + } + return err +} + +// serviceMain checks whether we're being invoked as a service, +// and if so uses the service control manager to start the long-running +// server. +// A flag is returned to the caller so the application can determine whether +// to exit (when running as a service) or launch in normal interactive mode. +func serviceMain() (bool, error) { + // Don't run as a service if we're running interactively + // (or that can't be determined due to an error). + isInteractive, e := svc.IsAnInteractiveSession() + if e != nil { + L. + return false, err + } + if isInteractive { + return false, nil + } + elog, e = eventlog.Open(svcName) + if e != nil { + L. + return false, err + } + defer elog.Close() + e = svc.Run(svcName, &podService{}) + if e != nil { + L.eL.Error(1, fmt.Sprintf("Service start failed: %v", err)) + return true, err + } + return true, nil +} + +// Set windows specific functions to real functions. +func init() { + podconfig.runServiceCommand = performServiceCommand + winServiceMain = serviceMain +} diff --git a/cmd/node/node/_signal.go_ b/cmd/node/node/_signal.go_ new file mode 100755 index 0000000..98154de --- /dev/null +++ b/cmd/node/node/_signal.go_ @@ -0,0 +1,54 @@ +package node + +import ( + "os" + "os/signal" + "runtime/trace" + + "github.com/p9c/p9/pkg/util/cl" +) + +// shutdownRequestChannel is used to initiate shutdown from one of the subsystems using the same code paths as when an interrupt signal is received. +var shutdownRequestChannel = make(qu.C) + +// interruptSignals defines the default signals to catch in order to do a proper shutdown. This may be modified during init depending on the platform. +var interruptSignals = []os.Signal{os.Interrupt} + +// interruptListener listens for OS Signals such as SIGINT (Ctrl+C) and shutdown requests from shutdownRequestChannel. It returns a channel that is closed when either signal is received. +func interruptListener() <- qu.C { + c := make(qu.C) + go func() { + + interruptChannel := make(chan os.Signal, 1) + signal.Notify(interruptChannel, interruptSignals...) + // Listen for initial shutdown signal and close the returned channel to notify the caller. + select { + case sig := <-interruptChannel: + log <- cl.Infof{"received signal (%s) - shutting down...", sig} + trace.Stop() + case <-shutdownRequestChannel: + log <- cl.Inf("shutdown requested - shutting down...") + } + close(c) + // Listen for repeated signals and display a message so the user knows the shutdown is in progress and the process is not hung. + for { + select { + case sig := <-interruptChannel: + log <- cl.Infof{"received signal (%s) - already shutting down...", sig} + case <-shutdownRequestChannel: + log <- cl.Inf("shutdown requested - already shutting down...") + } + } + }() + return c +} + +// interruptRequested returns true when the channel returned by interruptListener was closed. This simplifies early shutdown slightly since the caller can just use an if statement instead of a select. +func interruptRequested(interrupted <- qu.C) bool { + select { + case <-interrupted: + return true + default: + } + return false +} diff --git a/cmd/node/node/_signalsigterm.go_ b/cmd/node/node/_signalsigterm.go_ new file mode 100755 index 0000000..a15d464 --- /dev/null +++ b/cmd/node/node/_signalsigterm.go_ @@ -0,0 +1,13 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package node + +import ( + "os" + "syscall" +) + +func init() { + + interruptSignals = []os.Signal{os.Interrupt, syscall.SIGTERM} +} diff --git a/cmd/node/noded.go b/cmd/node/noded.go new file mode 100644 index 0000000..e3ef1dc --- /dev/null +++ b/cmd/node/noded.go @@ -0,0 +1,519 @@ +/*Package node is a full-node Parallelcoin implementation written in Go. + +The default options are sane for most users. This means pod will work 'out of the box' for most users. However, there +are also a wide variety of flags that can be used to control it. + +The following section provides a usage overview which enumerates the flags. An interesting point to note is that the +long form of all of these options ( except -C/--configfile and -D --datadir) can be specified in a configuration file +that is automatically parsed when pod starts up. By default, the configuration file is located at ~/.pod/pod. conf on +POSIX-style operating systems and %LOCALAPPDATA%\pod\pod. conf on Windows. The -D (--datadir) flag, can be used to +override this location. + +NAME: + pod node - start parallelcoin full node + +USAGE: + pod node [global options] command [command options] [arguments...] + +VERSION: + v0.0.1 + +COMMANDS: + dropaddrindex drop the address search index + droptxindex drop the address search index + dropcfindex drop the address search index + +GLOBAL OPTIONS: + --help, -h show help +*/ +package node + +import ( + "io" + "net" + "net/http" + "os" + "path/filepath" + "runtime/pprof" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/pkg/apputil" + "github.com/p9c/p9/pkg/chainrpc" + "github.com/p9c/p9/pkg/constant" + "github.com/p9c/p9/pkg/ctrl" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/database/blockdb" + "github.com/p9c/p9/pkg/indexers" + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/pod/state" +) + +// // This enables pprof +// _ "net/http/pprof" + +// winServiceMain is only invoked on Windows. It detects when pod is running as a service and reacts accordingly. +var winServiceMain func() (bool, error) + +// NodeMain is the real main function for pod. +// +// The optional serverChan parameter is mainly used by the service code to be notified with the server once it is setup +// so it can gracefully stop it when requested from the service control manager. +func NodeMain(cx *state.State) (e error) { + T.Ln("starting up node main") + // cx.WaitGroup.Add(1) + cx.WaitAdd() + // enable http profiling server if requested + if cx.Config.Profile.V() != "" { + D.Ln("profiling requested") + go func() { + listenAddr := net.JoinHostPort("", cx.Config.Profile.V()) + I.Ln("profile server listening on", listenAddr) + profileRedirect := http.RedirectHandler("/debug/pprof", http.StatusSeeOther) + http.Handle("/", profileRedirect) + D.Ln("profile server", http.ListenAndServe(listenAddr, nil)) + }() + } + // write cpu profile if requested + if cx.Config.CPUProfile.V() != "" && os.Getenv("POD_TRACE") != "on" { + D.Ln("cpu profiling enabled") + var f *os.File + f, e = os.Create(cx.Config.CPUProfile.V()) + if e != nil { + E.Ln("unable to create cpu profile:", e) + return + } + e = pprof.StartCPUProfile(f) + if e != nil { + D.Ln("failed to start up cpu profiler:", e) + } else { + defer func() { + if e = f.Close(); E.Chk(e) { + } + }() + defer pprof.StopCPUProfile() + interrupt.AddHandler( + func() { + D.Ln("stopping CPU profiler") + e = f.Close() + if e != nil { + } + pprof.StopCPUProfile() + D.Ln("finished cpu profiling", *cx.Config.CPUProfile) + }, + ) + } + } + // perform upgrades to pod as new versions require it + if e = doUpgrades(cx); E.Chk(e) { + return + } + // return now if an interrupt signal was triggered + if interrupt.Requested() { + return nil + } + // load the block database + var db database.DB + db, e = loadBlockDB(cx) + if e != nil { + return + } + closeDb := func() { + // ensure the database is synced and closed on shutdown + T.Ln("gracefully shutting down the database") + func() { + if e = db.Close(); E.Chk(e) { + } + }() + } + defer closeDb() + interrupt.AddHandler(closeDb) + // return now if an interrupt signal was triggered + if interrupt.Requested() { + return nil + } + // drop indexes and exit if requested. + // + // NOTE: The order is important here because dropping the tx index also drops the address index since it relies on + // it + if cx.StateCfg.DropAddrIndex { + W.Ln("dropping address index") + if e = indexers.DropAddrIndex(db, interrupt.ShutdownRequestChan); E.Chk(e) { + return + } + } + if cx.StateCfg.DropTxIndex { + W.Ln("dropping transaction index") + if e = indexers.DropTxIndex(db, interrupt.ShutdownRequestChan); E.Chk(e) { + return + } + } + if cx.StateCfg.DropCfIndex { + W.Ln("dropping cfilter index") + if e = indexers.DropCfIndex(db, interrupt.ShutdownRequestChan); E.Chk(e) { + return + } + } + // return now if an interrupt signal was triggered + if interrupt.Requested() { + return nil + } + mempoolUpdateChan := qu.Ts(1) + mempoolUpdateHook := func() { + mempoolUpdateChan.Signal() + } + // create server and start it + var server *chainrpc.Node + server, e = chainrpc.NewNode( + cx.Config.P2PListeners.S(), + db, + interrupt.ShutdownRequestChan, + state.GetContext(cx), + mempoolUpdateHook, + ) + if e != nil { + E.F("unable to start server on %v: %v", cx.Config.P2PListeners.S(), e) + return e + } + server.Start() + cx.RealNode = server + // if len(server.RPCServers) > 0 && *cx.Config.CAPI { + // D.Ln("starting cAPI.....") + // // chainrpc.RunAPI(server.RPCServers[0], cx.NodeKill) + // // D.Ln("propagating rpc server handle (node has started)") + // } + // I.S(server.RPCServers) + if len(server.RPCServers) > 0 { + cx.RPCServer = server.RPCServers[0] + D.Ln("sending back node") + cx.NodeChan <- cx.RPCServer + } + D.Ln("starting controller") + cx.Controller, e = ctrl.New( + cx.Syncing, + cx.Config, + cx.StateCfg, + cx.RealNode, + cx.RPCServer.Cfg.ConnMgr, + mempoolUpdateChan, + uint64(cx.Config.UUID.V()), + cx.KillAll, + cx.RealNode.StartController, cx.RealNode.StopController, + ) + go cx.Controller.Run() + cx.Controller.Start() + D.Ln("controller started") + once := true + gracefulShutdown := func() { + if !once { + return + } + if once { + once = false + } + D.Ln("gracefully shutting down the server...") + D.Ln("stopping controller") + cx.Controller.Shutdown() + D.Ln("stopping server") + e := server.Stop() + if e != nil { + W.Ln("failed to stop server", e) + } + server.WaitForShutdown() + I.Ln("server shutdown complete") + log.LogChanDisabled.Store(true) + cx.WaitDone() + cx.KillAll.Q() + cx.NodeKill.Q() + } + D.Ln("adding interrupt handler for node") + interrupt.AddHandler(gracefulShutdown) + // Wait until the interrupt signal is received from an OS signal or shutdown is requested through one of the + // subsystems such as the RPC server. + select { + case <-cx.NodeKill.Wait(): + D.Ln("NodeKill") + if !interrupt.Requested() { + interrupt.Request() + } + break + case <-cx.KillAll.Wait(): + D.Ln("KillAll") + if !interrupt.Requested() { + interrupt.Request() + } + break + } + gracefulShutdown() + return nil +} + +// loadBlockDB loads (or creates when needed) the block database taking into account the selected database backend and +// returns a handle to it. It also additional logic such warning the user if there are multiple databases which consume +// space on the file system and ensuring the regression test database is clean when in regression test mode. +func loadBlockDB(cx *state.State) (db database.DB, e error) { + // The memdb backend does not have a file path associated with it, so handle it uniquely. We also don't want to + // worry about the multiple database type warnings when running with the memory database. + if cx.Config.DbType.V() == "memdb" { + I.Ln("creating block database in memory") + if db, e = database.Create(cx.Config.DbType.V()); state.E.Chk(e) { + return nil, e + } + return db, nil + } + warnMultipleDBs(cx) + // The database name is based on the database type. + dbPath := state.BlockDb(cx, cx.Config.DbType.V(), blockdb.NamePrefix) + // The regression test is special in that it needs a clean database for each + // run, so remove it now if it already exists. + e = removeRegressionDB(cx, dbPath) + if e != nil { + D.Ln("failed to remove regression db:", e) + } + I.F("loading block database from '%s'", dbPath) + I.Ln(database.SupportedDrivers()) + if db, e = database.Open(cx.Config.DbType.V(), dbPath, cx.ActiveNet.Net); E.Chk(e) { + T.Ln(e) // return the error if it's not because the database doesn't exist + if dbErr, ok := e.(database.DBError); !ok || dbErr.ErrorCode != + database.ErrDbDoesNotExist { + return nil, e + } + // create the db if it does not exist + e = os.MkdirAll(cx.Config.DataDir.V(), 0700) + if e != nil { + return nil, e + } + db, e = database.Create(cx.Config.DbType.V(), dbPath, cx.ActiveNet.Net) + if e != nil { + return nil, e + } + } + T.Ln("block database loaded") + return db, nil +} + +// removeRegressionDB removes the existing regression test database if running +// in regression test mode and it already exists. +func removeRegressionDB(cx *state.State, dbPath string) (e error) { + // don't do anything if not in regression test mode + if !((cx.Config.Network.V())[0] == 'r') { + return nil + } + // remove the old regression test database if it already exists + fi, e := os.Stat(dbPath) + if e == nil { + I.F("removing regression test database from '%s' %s", dbPath) + if fi.IsDir() { + if e = os.RemoveAll(dbPath); E.Chk(e) { + return e + } + } else { + if e = os.Remove(dbPath); E.Chk(e) { + return e + } + } + } + return nil +} + +// warnMultipleDBs shows a warning if multiple block database types are +// detected. This is not a situation most users want. It is handy for +// development however to support multiple side-by-side databases. +func warnMultipleDBs(cx *state.State) { + // This is intentionally not using the known db types which depend on the + // database types compiled into the binary since we want to detect legacy db + // types as well. + dbTypes := []string{"ffldb", "leveldb", "sqlite"} + duplicateDbPaths := make([]string, 0, len(dbTypes)-1) + for _, dbType := range dbTypes { + if dbType == cx.Config.DbType.V() { + continue + } + // store db path as a duplicate db if it exists + dbPath := state.BlockDb(cx, dbType, blockdb.NamePrefix) + if apputil.FileExists(dbPath) { + duplicateDbPaths = append(duplicateDbPaths, dbPath) + } + } + // warn if there are extra databases + if len(duplicateDbPaths) > 0 { + selectedDbPath := state.BlockDb(cx, cx.Config.DbType.V(), blockdb.NamePrefix) + W.F( + "\nThere are multiple block chain databases using different"+ + " database types.\nYou probably don't want to waste disk"+ + " space by having more than one."+ + "\nYour current database is located at [%v]."+ + "\nThe additional database is located at %v", + selectedDbPath, + duplicateDbPaths, + ) + } +} + +// dirEmpty returns whether or not the specified directory path is empty +func dirEmpty(dirPath string) (bool, error) { + f, e := os.Open(dirPath) + if e != nil { + return false, e + } + defer func() { + if e = f.Close(); E.Chk(e) { + } + }() + // Read the names of a max of one entry from the directory. When the directory is empty, an io.EOF error will be + // returned, so allow it. + names, e := f.Readdirnames(1) + if e != nil && e != io.EOF { + return false, e + } + return len(names) == 0, nil +} + +// doUpgrades performs upgrades to pod as new versions require it +func doUpgrades(cx *state.State) (e error) { + e = upgradeDBPaths(cx) + if e != nil { + return e + } + return upgradeDataPaths() +} + +// oldPodHomeDir returns the OS specific home directory pod used prior to version 0.3.3. This has since been replaced +// with util.AppDataDir but this function is still provided for the automatic upgrade path. +func oldPodHomeDir() string { + // Search for Windows APPDATA first. This won't exist on POSIX OSes + appData := os.Getenv("APPDATA") + if appData != "" { + return filepath.Join(appData, "pod") + } + // Fall back to standard HOME directory that works for most POSIX OSes + home := os.Getenv("HOME") + if home != "" { + return filepath.Join(home, ".pod") + } + // In the worst case, use the current directory + return "." +} + +// upgradeDBPathNet moves the database for a specific network from its location prior to pod version 0.2.0 and uses +// heuristics to ascertain the old database type to rename to the new format. +func upgradeDBPathNet(cx *state.State, oldDbPath, netName string) (e error) { + // Prior to version 0.2.0, the database was named the same thing for both sqlite and leveldb. Use heuristics to + // figure out the type of the database and move it to the new path and name introduced with version 0.2.0 + // accordingly. + fi, e := os.Stat(oldDbPath) + if e == nil { + oldDbType := "sqlite" + if fi.IsDir() { + oldDbType = "leveldb" + } + // The new database name is based on the database type and resides in a directory named after the network type. + newDbRoot := filepath.Join(filepath.Dir(cx.Config.DataDir.V()), netName) + newDbName := blockdb.NamePrefix + "_" + oldDbType + if oldDbType == "sqlite" { + newDbName = newDbName + ".db" + } + newDbPath := filepath.Join(newDbRoot, newDbName) + // Create the new path if needed + // + e = os.MkdirAll(newDbRoot, 0700) + if e != nil { + return e + } + // Move and rename the old database + // + e := os.Rename(oldDbPath, newDbPath) + if e != nil { + return e + } + } + return nil +} + +// upgradeDBPaths moves the databases from their locations prior to pod version 0.2.0 to their new locations +func upgradeDBPaths(cx *state.State) (e error) { + // Prior to version 0.2.0 the databases were in the "db" directory and their names were suffixed by "testnet" and + // "regtest" for their respective networks. Chk for the old database and update it to the new path introduced with + // version 0.2.0 accordingly. + oldDbRoot := filepath.Join(oldPodHomeDir(), "db") + e = upgradeDBPathNet(cx, filepath.Join(oldDbRoot, "pod.db"), "mainnet") + if e != nil { + D.Ln(e) + } + e = upgradeDBPathNet( + cx, filepath.Join(oldDbRoot, "pod_testnet.db"), + "testnet", + ) + if e != nil { + D.Ln(e) + } + e = upgradeDBPathNet( + cx, filepath.Join(oldDbRoot, "pod_regtest.db"), + "regtest", + ) + if e != nil { + D.Ln(e) + } + // Remove the old db directory + return os.RemoveAll(oldDbRoot) +} + +// upgradeDataPaths moves the application data from its location prior to pod version 0.3.3 to its new location. +func upgradeDataPaths() (e error) { + // No need to migrate if the old and new home paths are the same. + oldHomePath := oldPodHomeDir() + newHomePath := constant.DefaultHomeDir + if oldHomePath == newHomePath { + return nil + } + // Only migrate if the old path exists and the new one doesn't + if apputil.FileExists(oldHomePath) && !apputil.FileExists(newHomePath) { + // Create the new path + I.F( + "migrating application home path from '%s' to '%s'", + oldHomePath, newHomePath, + ) + e := os.MkdirAll(newHomePath, 0700) + if e != nil { + return e + } + // Move old pod.conf into new location if needed + oldConfPath := filepath.Join(oldHomePath, constant.DefaultConfigFilename) + newConfPath := filepath.Join(newHomePath, constant.DefaultConfigFilename) + if apputil.FileExists(oldConfPath) && !apputil.FileExists(newConfPath) { + e = os.Rename(oldConfPath, newConfPath) + if e != nil { + return e + } + } + // Move old data directory into new location if needed + oldDataPath := filepath.Join(oldHomePath, constant.DefaultDataDirname) + newDataPath := filepath.Join(newHomePath, constant.DefaultDataDirname) + if apputil.FileExists(oldDataPath) && !apputil.FileExists(newDataPath) { + e = os.Rename(oldDataPath, newDataPath) + if e != nil { + return e + } + } + // Remove the old home if it is empty or show a warning if not + ohpEmpty, e := dirEmpty(oldHomePath) + if e != nil { + return e + } + if ohpEmpty { + e := os.Remove(oldHomePath) + if e != nil { + return e + } + } else { + W.F( + "not removing '%s' since it contains files not created by"+ + " this application you may want to manually move them or"+ + " delete them.", oldHomePath, + ) + } + } + return nil +} diff --git a/cmd/node/parameters/genesisblocks b/cmd/node/parameters/genesisblocks new file mode 100755 index 0000000..5735c31 --- /dev/null +++ b/cmd/node/parameters/genesisblocks @@ -0,0 +1,360 @@ +Parallelcoin mainnet raw genesis block +000009f0fcbad3aac904d3660cfdcf238bf298cfe73adf1d39d14fc5c740ccc7 +020000000000000000000000000000000000000000000000000000000000000000000000b79a9b6f31a9d7d25a1c4b0ec7a671dc56ce7663c380f2d2513a8e65e4ea43c8dcecc953ffff0f1e810201000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a04ffff001d0104324e5954696d657320323031342d30372d3139202d2044656c6c20426567696e7320416363657074696e6720426974636f696effffffff0100e8764817000000434104e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438daeac00000000 +testnet raw genesis block +00000e41ecbaa35ef91b0c2c22ed4d85fa12bbc87da2668fe17572695fb30cdf +020000000000000000000000000000000000000000000000000000000000000000000000b79a9b6f31a9d7d25a1c4b0ec7a671dc56ce7663c380f2d2513a8e65e4ea43c884eac953ffff0f1e18df1a000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a04ffff001d0104324e5954696d657320323031342d30372d3139202d2044656c6c20426567696e7320416363657074696e6720426974636f696effffffff0100e8764817000000434104e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438daeac00000000 +regtestnet raw genesis block +69e9b79e220ea183dc2a52c825667e486bba65e2f64d237b578559ab60379181 +020000000000000000000000000000000000000000000000000000000000000000000000b79a9b6f31a9d7d25a1c4b0ec7a671dc56ce7663c380f2d2513a8e65e4ea43c8d4e5c953ffff7f20010000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a04ffff001d0104324e5954696d657320323031342d30372d3139202d2044656c6c20426567696e7320416363657074696e6720426974636f696effffffff0100e8764817000000434104e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438daeac00000000 +----------------------------------------------------------------------------------- +mainnet genesis block data +BLOCK 0 + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb7, 0x9a, 0x9b, 0x6f, + 0x31, 0xa9, 0xd7, 0xd2, 0x5a, 0x1c, 0x4b, 0x0e, + 0xc7, 0xa6, 0x71, 0xdc, 0x56, 0xce, 0x76, 0x63, + 0xc3, 0x80, 0xf2, 0xd2, 0x51, 0x3a, 0x8e, 0x65, + 0xe4, 0xea, 0x43, 0xc8, 0xdc, 0xec, 0xc9, 0x53, + 0xff, 0xff, 0x0f, 0x1e, 0x81, 0x02, 0x01, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x3a, 0x04, 0xff, 0xff, 0x00, 0x1d, + 0x01, 0x04, 0x32, 0x4e, 0x59, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, + 0x30, 0x37, 0x2d, 0x31, 0x39, 0x20, 0x2d, 0x20, + 0x44, 0x65, 0x6c, 0x6c, 0x20, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x73, 0x20, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x42, 0x69, + 0x74, 0x63, 0x6f, 0x69, 0x6e, 0xff, 0xff, 0xff, + 0xff, 0x01, 0x00, 0xe8, 0x76, 0x48, 0x17, 0x00, + 0x00, 0x00, 0x43, 0x41, 0x04, 0xe0, 0xd2, 0x71, + 0x72, 0x51, 0x0c, 0x68, 0x06, 0x88, 0x97, 0x40, + 0xed, 0xaf, 0xe6, 0xe6, 0x3e, 0xb2, 0x3f, 0xca, + 0x32, 0x78, 0x6f, 0xcc, 0xfd, 0xb2, 0x82, 0xbb, + 0x28, 0x76, 0xa9, 0xf4, 0x3b, 0x22, 0x82, 0x45, + 0xdf, 0x05, 0x76, 0x61, 0xff, 0x94, 0x3f, 0x61, + 0x50, 0x71, 0x6a, 0x20, 0xea, 0x18, 0x51, 0xe8, + 0xa7, 0xe9, 0xf5, 0x4e, 0x62, 0x02, 0x97, 0x66, + 0x46, 0x18, 0x43, 0x8d, 0xae, 0xac, 0x00, 0x00, + 0x00, 0x00, +GENESIS BLOCK HASH +for + 0x00, 0x00, 0x09, 0xf0, 0xfc, 0xba, 0xd3, 0xaa, + 0xc9, 0x04, 0xd3, 0x66, 0x0c, 0xfd, 0xcf, 0x23, + 0x8b, 0xf2, 0x98, 0xcf, 0xe7, 0x3a, 0xdf, 0x1d, + 0x39, 0xd1, 0x4f, 0xc5, 0xc7, 0x40, 0xcc, 0xc7, +rev + 0xc7, 0xcc, 0x40, 0xc7, 0xc5, 0x4f, 0xd1, 0x39, + 0x1d, 0xdf, 0x3a, 0xe7, 0xcf, 0x98, 0xf2, 0x8b, + 0x23, 0xcf, 0xfd, 0x0c, 0x66, 0xd3, 0x04, 0xc9, + 0xaa, 0xd3, 0xba, 0xfc, 0xf0, 0x09, 0x00, 0x00, +Version 2 +rev +0x02000000 +for +0x00000002 +HashPrevBlock 0000000000000000000000000000000000000000000000000000000000000000 +HashMerkleRoot c843eae4658e3a51d2f280c36376ce56dc71a6c70e4b1c5ad2d7a9316f9b9ab7 +rev + 0xc8, 0x43, 0xea, 0xe4, 0x65, 0x8e, 0x3a, 0x51, + 0xd2, 0xf2, 0x80, 0xc3, 0x63, 0x76, 0xce, 0x56, + 0xdc, 0x71, 0xa6, 0xc7, 0x0e, 0x4b, 0x1c, 0x5a, + 0xd2, 0xd7, 0xa9, 0x31, 0x6f, 0x9b, 0x9a, 0xb7, +for + 0xb7, 0x9a, 0x9b, 0x6f, 0x31, 0xa9, 0xd7, 0xd2, + 0x5a, 0x1c, 0x4b, 0x0e, 0xc7, 0xa6, 0x71, 0xdc, + 0x56, 0xce, 0x76, 0x63, 0xc3, 0x80, 0xf2, 0xd2, + 0x51, 0x3a, 0x8e, 0x65, 0xe4, 0xea, 0x43, 0xc8, +Unix timestamp dcecc953 +for 1405742300 0x53c9ecdc +rev 3706505555 0xdcecc953 +Bits ffff0f1e +for 504365055 0x1e0fffff +rev 4294905630 0xffff0f1e +Nonce 66177 +for 66177 0x10281 +rev 2164392192 0x81020100 +Transaction 0 + tx version 1 +for + 0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04, 0x32, + 0x4e, 0x59, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x20, + 0x32, 0x30, 0x31, 0x34, 0x2d, 0x30, 0x37, 0x2d, + 0x31, 0x39, 0x20, 0x2d, 0x20, 0x44, 0x65, 0x6c, + 0x6c, 0x20, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x73, + 0x20, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x69, + 0x6e, 0x67, 0x20, 0x42, 0x69, 0x74, 0x63, 0x6f, + 0x69, 0x6e, +rev + 0x6e, 0x69, 0x6f, 0x63, 0x74, 0x69, 0x42, 0x20, + 0x67, 0x6e, 0x69, 0x74, 0x70, 0x65, 0x63, 0x63, + 0x41, 0x20, 0x73, 0x6e, 0x69, 0x67, 0x65, 0x42, + 0x20, 0x6c, 0x6c, 0x65, 0x44, 0x20, 0x2d, 0x20, + 0x39, 0x31, 0x2d, 0x37, 0x30, 0x2d, 0x34, 0x31, + 0x30, 0x32, 0x20, 0x73, 0x65, 0x6d, 0x69, 0x54, + 0x59, 0x4e, 0x32, 0x04, 0x01, 0x1d, 0x00, 0xff, + 0xff, 0x04, +txout +for + 0x41, 0x04, 0xe0, 0xd2, 0x71, 0x72, 0x51, 0x0c, + 0x68, 0x06, 0x88, 0x97, 0x40, 0xed, 0xaf, 0xe6, + 0xe6, 0x3e, 0xb2, 0x3f, 0xca, 0x32, 0x78, 0x6f, + 0xcc, 0xfd, 0xb2, 0x82, 0xbb, 0x28, 0x76, 0xa9, + 0xf4, 0x3b, 0x22, 0x82, 0x45, 0xdf, 0x05, 0x76, + 0x61, 0xff, 0x94, 0x3f, 0x61, 0x50, 0x71, 0x6a, + 0x20, 0xea, 0x18, 0x51, 0xe8, 0xa7, 0xe9, 0xf5, + 0x4e, 0x62, 0x02, 0x97, 0x66, 0x46, 0x18, 0x43, + 0x8d, 0xae, 0xac, +rev + 0xac, 0xae, 0x8d, 0x43, 0x18, 0x46, 0x66, 0x97, + 0x02, 0x62, 0x4e, 0xf5, 0xe9, 0xa7, 0xe8, 0x51, + 0x18, 0xea, 0x20, 0x6a, 0x71, 0x50, 0x61, 0x3f, + 0x94, 0xff, 0x61, 0x76, 0x05, 0xdf, 0x45, 0x82, + 0x22, 0x3b, 0xf4, 0xa9, 0x76, 0x28, 0xbb, 0x82, + 0xb2, 0xfd, 0xcc, 0x6f, 0x78, 0x32, 0xca, 0x3f, + 0xb2, 0x3e, 0xe6, 0xe6, 0xaf, 0xed, 0x40, 0x97, + 0x88, 0x06, 0x68, 0x0c, 0x51, 0x72, 0x71, 0xd2, + 0xe0, 0x04, 0x41, + [04e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438dae OP_CHECKSIG] +abCFzjNoXHYxVQYP1WDBwpxgPCXzfqoxv7 +100000000000 +----------------------------------------------------------------------------------- +testnet genesis block data +BLOCK 0 + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb7, 0x9a, 0x9b, 0x6f, + 0x31, 0xa9, 0xd7, 0xd2, 0x5a, 0x1c, 0x4b, 0x0e, + 0xc7, 0xa6, 0x71, 0xdc, 0x56, 0xce, 0x76, 0x63, + 0xc3, 0x80, 0xf2, 0xd2, 0x51, 0x3a, 0x8e, 0x65, + 0xe4, 0xea, 0x43, 0xc8, 0x84, 0xea, 0xc9, 0x53, + 0xff, 0xff, 0x0f, 0x1e, 0x18, 0xdf, 0x1a, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x3a, 0x04, 0xff, 0xff, 0x00, 0x1d, + 0x01, 0x04, 0x32, 0x4e, 0x59, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, + 0x30, 0x37, 0x2d, 0x31, 0x39, 0x20, 0x2d, 0x20, + 0x44, 0x65, 0x6c, 0x6c, 0x20, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x73, 0x20, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x42, 0x69, + 0x74, 0x63, 0x6f, 0x69, 0x6e, 0xff, 0xff, 0xff, + 0xff, 0x01, 0x00, 0xe8, 0x76, 0x48, 0x17, 0x00, + 0x00, 0x00, 0x43, 0x41, 0x04, 0xe0, 0xd2, 0x71, + 0x72, 0x51, 0x0c, 0x68, 0x06, 0x88, 0x97, 0x40, + 0xed, 0xaf, 0xe6, 0xe6, 0x3e, 0xb2, 0x3f, 0xca, + 0x32, 0x78, 0x6f, 0xcc, 0xfd, 0xb2, 0x82, 0xbb, + 0x28, 0x76, 0xa9, 0xf4, 0x3b, 0x22, 0x82, 0x45, + 0xdf, 0x05, 0x76, 0x61, 0xff, 0x94, 0x3f, 0x61, + 0x50, 0x71, 0x6a, 0x20, 0xea, 0x18, 0x51, 0xe8, + 0xa7, 0xe9, 0xf5, 0x4e, 0x62, 0x02, 0x97, 0x66, + 0x46, 0x18, 0x43, 0x8d, 0xae, 0xac, 0x00, 0x00, + 0x00, 0x00, +GENESIS BLOCK HASH +for + 0x00, 0x00, 0x0e, 0x41, 0xec, 0xba, 0xa3, 0x5e, + 0xf9, 0x1b, 0x0c, 0x2c, 0x22, 0xed, 0x4d, 0x85, + 0xfa, 0x12, 0xbb, 0xc8, 0x7d, 0xa2, 0x66, 0x8f, + 0xe1, 0x75, 0x72, 0x69, 0x5f, 0xb3, 0x0c, 0xdf, +rev + 0xdf, 0x0c, 0xb3, 0x5f, 0x69, 0x72, 0x75, 0xe1, + 0x8f, 0x66, 0xa2, 0x7d, 0xc8, 0xbb, 0x12, 0xfa, + 0x85, 0x4d, 0xed, 0x22, 0x2c, 0x0c, 0x1b, 0xf9, + 0x5e, 0xa3, 0xba, 0xec, 0x41, 0x0e, 0x00, 0x00, +Version 2 +rev +0x02000000 +for +0x00000002 +HashPrevBlock 0000000000000000000000000000000000000000000000000000000000000000 +HashMerkleRoot c843eae4658e3a51d2f280c36376ce56dc71a6c70e4b1c5ad2d7a9316f9b9ab7 +rev + 0xc8, 0x43, 0xea, 0xe4, 0x65, 0x8e, 0x3a, 0x51, + 0xd2, 0xf2, 0x80, 0xc3, 0x63, 0x76, 0xce, 0x56, + 0xdc, 0x71, 0xa6, 0xc7, 0x0e, 0x4b, 0x1c, 0x5a, + 0xd2, 0xd7, 0xa9, 0x31, 0x6f, 0x9b, 0x9a, 0xb7, +for + 0xb7, 0x9a, 0x9b, 0x6f, 0x31, 0xa9, 0xd7, 0xd2, + 0x5a, 0x1c, 0x4b, 0x0e, 0xc7, 0xa6, 0x71, 0xdc, + 0x56, 0xce, 0x76, 0x63, 0xc3, 0x80, 0xf2, 0xd2, + 0x51, 0x3a, 0x8e, 0x65, 0xe4, 0xea, 0x43, 0xc8, +Unix timestamp 84eac953 +for 1405741700 0x53c9ea84 +rev 2229979475 0x84eac953 +Bits ffff0f1e +for 504365055 0x1e0fffff +rev 4294905630 0xffff0f1e +Nonce 1761048 +for 1761048 0x1adf18 +rev 417274368 0x18df1a00 +Transaction 0 + tx version 1 +for + 0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04, 0x32, + 0x4e, 0x59, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x20, + 0x32, 0x30, 0x31, 0x34, 0x2d, 0x30, 0x37, 0x2d, + 0x31, 0x39, 0x20, 0x2d, 0x20, 0x44, 0x65, 0x6c, + 0x6c, 0x20, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x73, + 0x20, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x69, + 0x6e, 0x67, 0x20, 0x42, 0x69, 0x74, 0x63, 0x6f, + 0x69, 0x6e, +rev + 0x6e, 0x69, 0x6f, 0x63, 0x74, 0x69, 0x42, 0x20, + 0x67, 0x6e, 0x69, 0x74, 0x70, 0x65, 0x63, 0x63, + 0x41, 0x20, 0x73, 0x6e, 0x69, 0x67, 0x65, 0x42, + 0x20, 0x6c, 0x6c, 0x65, 0x44, 0x20, 0x2d, 0x20, + 0x39, 0x31, 0x2d, 0x37, 0x30, 0x2d, 0x34, 0x31, + 0x30, 0x32, 0x20, 0x73, 0x65, 0x6d, 0x69, 0x54, + 0x59, 0x4e, 0x32, 0x04, 0x01, 0x1d, 0x00, 0xff, + 0xff, 0x04, +txout +for + 0x41, 0x04, 0xe0, 0xd2, 0x71, 0x72, 0x51, 0x0c, + 0x68, 0x06, 0x88, 0x97, 0x40, 0xed, 0xaf, 0xe6, + 0xe6, 0x3e, 0xb2, 0x3f, 0xca, 0x32, 0x78, 0x6f, + 0xcc, 0xfd, 0xb2, 0x82, 0xbb, 0x28, 0x76, 0xa9, + 0xf4, 0x3b, 0x22, 0x82, 0x45, 0xdf, 0x05, 0x76, + 0x61, 0xff, 0x94, 0x3f, 0x61, 0x50, 0x71, 0x6a, + 0x20, 0xea, 0x18, 0x51, 0xe8, 0xa7, 0xe9, 0xf5, + 0x4e, 0x62, 0x02, 0x97, 0x66, 0x46, 0x18, 0x43, + 0x8d, 0xae, 0xac, +rev + 0xac, 0xae, 0x8d, 0x43, 0x18, 0x46, 0x66, 0x97, + 0x02, 0x62, 0x4e, 0xf5, 0xe9, 0xa7, 0xe8, 0x51, + 0x18, 0xea, 0x20, 0x6a, 0x71, 0x50, 0x61, 0x3f, + 0x94, 0xff, 0x61, 0x76, 0x05, 0xdf, 0x45, 0x82, + 0x22, 0x3b, 0xf4, 0xa9, 0x76, 0x28, 0xbb, 0x82, + 0xb2, 0xfd, 0xcc, 0x6f, 0x78, 0x32, 0xca, 0x3f, + 0xb2, 0x3e, 0xe6, 0xe6, 0xaf, 0xed, 0x40, 0x97, + 0x88, 0x06, 0x68, 0x0c, 0x51, 0x72, 0x71, 0xd2, + 0xe0, 0x04, 0x41, + [04e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438dae OP_CHECKSIG] +abCFzjNoXHYxVQYP1WDBwpxgPCXzfqoxv7 +100000000000 +----------------------------------------------------------------------------------- +regtestnet genesis block data +BLOCK 0 + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb7, 0x9a, 0x9b, 0x6f, + 0x31, 0xa9, 0xd7, 0xd2, 0x5a, 0x1c, 0x4b, 0x0e, + 0xc7, 0xa6, 0x71, 0xdc, 0x56, 0xce, 0x76, 0x63, + 0xc3, 0x80, 0xf2, 0xd2, 0x51, 0x3a, 0x8e, 0x65, + 0xe4, 0xea, 0x43, 0xc8, 0xd4, 0xe5, 0xc9, 0x53, + 0xff, 0xff, 0x7f, 0x20, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x3a, 0x04, 0xff, 0xff, 0x00, 0x1d, + 0x01, 0x04, 0x32, 0x4e, 0x59, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, + 0x30, 0x37, 0x2d, 0x31, 0x39, 0x20, 0x2d, 0x20, + 0x44, 0x65, 0x6c, 0x6c, 0x20, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x73, 0x20, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x42, 0x69, + 0x74, 0x63, 0x6f, 0x69, 0x6e, 0xff, 0xff, 0xff, + 0xff, 0x01, 0x00, 0xe8, 0x76, 0x48, 0x17, 0x00, + 0x00, 0x00, 0x43, 0x41, 0x04, 0xe0, 0xd2, 0x71, + 0x72, 0x51, 0x0c, 0x68, 0x06, 0x88, 0x97, 0x40, + 0xed, 0xaf, 0xe6, 0xe6, 0x3e, 0xb2, 0x3f, 0xca, + 0x32, 0x78, 0x6f, 0xcc, 0xfd, 0xb2, 0x82, 0xbb, + 0x28, 0x76, 0xa9, 0xf4, 0x3b, 0x22, 0x82, 0x45, + 0xdf, 0x05, 0x76, 0x61, 0xff, 0x94, 0x3f, 0x61, + 0x50, 0x71, 0x6a, 0x20, 0xea, 0x18, 0x51, 0xe8, + 0xa7, 0xe9, 0xf5, 0x4e, 0x62, 0x02, 0x97, 0x66, + 0x46, 0x18, 0x43, 0x8d, 0xae, 0xac, 0x00, 0x00, + 0x00, 0x00, +GENESIS BLOCK HASH +for + 0x69, 0xe9, 0xb7, 0x9e, 0x22, 0x0e, 0xa1, 0x83, + 0xdc, 0x2a, 0x52, 0xc8, 0x25, 0x66, 0x7e, 0x48, + 0x6b, 0xba, 0x65, 0xe2, 0xf6, 0x4d, 0x23, 0x7b, + 0x57, 0x85, 0x59, 0xab, 0x60, 0x37, 0x91, 0x81, +rev + 0x81, 0x91, 0x37, 0x60, 0xab, 0x59, 0x85, 0x57, + 0x7b, 0x23, 0x4d, 0xf6, 0xe2, 0x65, 0xba, 0x6b, + 0x48, 0x7e, 0x66, 0x25, 0xc8, 0x52, 0x2a, 0xdc, + 0x83, 0xa1, 0x0e, 0x22, 0x9e, 0xb7, 0xe9, 0x69, +Version 2 +rev +0x02000000 +for +0x00000002 +HashPrevBlock 0000000000000000000000000000000000000000000000000000000000000000 +HashMerkleRoot c843eae4658e3a51d2f280c36376ce56dc71a6c70e4b1c5ad2d7a9316f9b9ab7 +rev + 0xc8, 0x43, 0xea, 0xe4, 0x65, 0x8e, 0x3a, 0x51, + 0xd2, 0xf2, 0x80, 0xc3, 0x63, 0x76, 0xce, 0x56, + 0xdc, 0x71, 0xa6, 0xc7, 0x0e, 0x4b, 0x1c, 0x5a, + 0xd2, 0xd7, 0xa9, 0x31, 0x6f, 0x9b, 0x9a, 0xb7, +for + 0xb7, 0x9a, 0x9b, 0x6f, 0x31, 0xa9, 0xd7, 0xd2, + 0x5a, 0x1c, 0x4b, 0x0e, 0xc7, 0xa6, 0x71, 0xdc, + 0x56, 0xce, 0x76, 0x63, 0xc3, 0x80, 0xf2, 0xd2, + 0x51, 0x3a, 0x8e, 0x65, 0xe4, 0xea, 0x43, 0xc8, +Unix timestamp d4e5c953 +for 1405740500 0x53c9e5d4 +rev 3571829075 0xd4e5c953 +Bits ffff7f20 +for 545259519 0x207fffff +rev 4294934304 0xffff7f20 +Nonce 1 +for 1 0x0001 +rev 16777216 0x1000000 +Transaction 0 + tx version 1 +for + 0x04, 0xff, 0xff, 0x00, 0x1d, 0x01, 0x04, 0x32, + 0x4e, 0x59, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x20, + 0x32, 0x30, 0x31, 0x34, 0x2d, 0x30, 0x37, 0x2d, + 0x31, 0x39, 0x20, 0x2d, 0x20, 0x44, 0x65, 0x6c, + 0x6c, 0x20, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x73, + 0x20, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x69, + 0x6e, 0x67, 0x20, 0x42, 0x69, 0x74, 0x63, 0x6f, + 0x69, 0x6e, +rev + 0x6e, 0x69, 0x6f, 0x63, 0x74, 0x69, 0x42, 0x20, + 0x67, 0x6e, 0x69, 0x74, 0x70, 0x65, 0x63, 0x63, + 0x41, 0x20, 0x73, 0x6e, 0x69, 0x67, 0x65, 0x42, + 0x20, 0x6c, 0x6c, 0x65, 0x44, 0x20, 0x2d, 0x20, + 0x39, 0x31, 0x2d, 0x37, 0x30, 0x2d, 0x34, 0x31, + 0x30, 0x32, 0x20, 0x73, 0x65, 0x6d, 0x69, 0x54, + 0x59, 0x4e, 0x32, 0x04, 0x01, 0x1d, 0x00, 0xff, + 0xff, 0x04, +txout +for + 0x41, 0x04, 0xe0, 0xd2, 0x71, 0x72, 0x51, 0x0c, + 0x68, 0x06, 0x88, 0x97, 0x40, 0xed, 0xaf, 0xe6, + 0xe6, 0x3e, 0xb2, 0x3f, 0xca, 0x32, 0x78, 0x6f, + 0xcc, 0xfd, 0xb2, 0x82, 0xbb, 0x28, 0x76, 0xa9, + 0xf4, 0x3b, 0x22, 0x82, 0x45, 0xdf, 0x05, 0x76, + 0x61, 0xff, 0x94, 0x3f, 0x61, 0x50, 0x71, 0x6a, + 0x20, 0xea, 0x18, 0x51, 0xe8, 0xa7, 0xe9, 0xf5, + 0x4e, 0x62, 0x02, 0x97, 0x66, 0x46, 0x18, 0x43, + 0x8d, 0xae, 0xac, +rev + 0xac, 0xae, 0x8d, 0x43, 0x18, 0x46, 0x66, 0x97, + 0x02, 0x62, 0x4e, 0xf5, 0xe9, 0xa7, 0xe8, 0x51, + 0x18, 0xea, 0x20, 0x6a, 0x71, 0x50, 0x61, 0x3f, + 0x94, 0xff, 0x61, 0x76, 0x05, 0xdf, 0x45, 0x82, + 0x22, 0x3b, 0xf4, 0xa9, 0x76, 0x28, 0xbb, 0x82, + 0xb2, 0xfd, 0xcc, 0x6f, 0x78, 0x32, 0xca, 0x3f, + 0xb2, 0x3e, 0xe6, 0xe6, 0xaf, 0xed, 0x40, 0x97, + 0x88, 0x06, 0x68, 0x0c, 0x51, 0x72, 0x71, 0xd2, + 0xe0, 0x04, 0x41, + [04e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438dae OP_CHECKSIG] +abCFzjNoXHYxVQYP1WDBwpxgPCXzfqoxv7 +100000000000 \ No newline at end of file diff --git a/cmd/node/parameters/genesistohex.go b/cmd/node/parameters/genesistohex.go new file mode 100644 index 0000000..49b7cd7 --- /dev/null +++ b/cmd/node/parameters/genesistohex.go @@ -0,0 +1,131 @@ +package parameters + +// network genesis info +var ( + MainnetGenesisHash = []byte{ + 0xc7, 0xcc, 0x40, 0xc7, 0xc5, 0x4f, 0xd1, 0x39, + 0x1d, 0xdf, 0x3a, 0xe7, 0xcf, 0x98, 0xf2, 0x8b, + 0x23, 0xcf, 0xfd, 0x0c, 0x66, 0xd3, 0x04, 0xc9, + 0xaa, 0xd3, 0xba, 0xfc, 0xf0, 0x09, 0x00, 0x00, + } + MainnetGenesisBlock = []byte{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb7, 0x9a, 0x9b, 0x6f, + 0x31, 0xa9, 0xd7, 0xd2, 0x5a, 0x1c, 0x4b, 0x0e, + 0xc7, 0xa6, 0x71, 0xdc, 0x56, 0xce, 0x76, 0x63, + 0xc3, 0x80, 0xf2, 0xd2, 0x51, 0x3a, 0x8e, 0x65, + 0xe4, 0xea, 0x43, 0xc8, 0xdc, 0xec, 0xc9, 0x53, + 0xff, 0xff, 0x0f, 0x1e, 0x81, 0x02, 0x01, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x3a, 0x04, 0xff, 0xff, 0x00, 0x1d, + 0x01, 0x04, 0x32, 0x4e, 0x59, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, + 0x30, 0x37, 0x2d, 0x31, 0x39, 0x20, 0x2d, 0x20, + 0x44, 0x65, 0x6c, 0x6c, 0x20, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x73, 0x20, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x42, 0x69, + 0x74, 0x63, 0x6f, 0x69, 0x6e, 0xff, 0xff, 0xff, + 0xff, 0x01, 0x00, 0xe8, 0x76, 0x48, 0x17, 0x00, + 0x00, 0x00, 0x43, 0x41, 0x04, 0xe0, 0xd2, 0x71, + 0x72, 0x51, 0x0c, 0x68, 0x06, 0x88, 0x97, 0x40, + 0xed, 0xaf, 0xe6, 0xe6, 0x3e, 0xb2, 0x3f, 0xca, + 0x32, 0x78, 0x6f, 0xcc, 0xfd, 0xb2, 0x82, 0xbb, + 0x28, 0x76, 0xa9, 0xf4, 0x3b, 0x22, 0x82, 0x45, + 0xdf, 0x05, 0x76, 0x61, 0xff, 0x94, 0x3f, 0x61, + 0x50, 0x71, 0x6a, 0x20, 0xea, 0x18, 0x51, 0xe8, + 0xa7, 0xe9, 0xf5, 0x4e, 0x62, 0x02, 0x97, 0x66, + 0x46, 0x18, 0x43, 0x8d, 0xae, 0xac, 0x00, 0x00, + 0x00, 0x00, + } + TestnetGenesisHash = []byte{ + 0xdf, 0x0c, 0xb3, 0x5f, 0x69, 0x72, 0x75, 0xe1, + 0x8f, 0x66, 0xa2, 0x7d, 0xc8, 0xbb, 0x12, 0xfa, + 0x85, 0x4d, 0xed, 0x22, 0x2c, 0x0c, 0x1b, 0xf9, + 0x5e, 0xa3, 0xba, 0xec, 0x41, 0x0e, 0x00, 0x00, + } + TestnetGenesisBlock = []byte{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb7, 0x9a, 0x9b, 0x6f, + 0x31, 0xa9, 0xd7, 0xd2, 0x5a, 0x1c, 0x4b, 0x0e, + 0xc7, 0xa6, 0x71, 0xdc, 0x56, 0xce, 0x76, 0x63, + 0xc3, 0x80, 0xf2, 0xd2, 0x51, 0x3a, 0x8e, 0x65, + 0xe4, 0xea, 0x43, 0xc8, 0x84, 0xea, 0xc9, 0x53, + 0xff, 0xff, 0x0f, 0x1e, 0x18, 0xdf, 0x1a, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x3a, 0x04, 0xff, 0xff, 0x00, 0x1d, + 0x01, 0x04, 0x32, 0x4e, 0x59, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, + 0x30, 0x37, 0x2d, 0x31, 0x39, 0x20, 0x2d, 0x20, + 0x44, 0x65, 0x6c, 0x6c, 0x20, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x73, 0x20, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x42, 0x69, + 0x74, 0x63, 0x6f, 0x69, 0x6e, 0xff, 0xff, 0xff, + 0xff, 0x01, 0x00, 0xe8, 0x76, 0x48, 0x17, 0x00, + 0x00, 0x00, 0x43, 0x41, 0x04, 0xe0, 0xd2, 0x71, + 0x72, 0x51, 0x0c, 0x68, 0x06, 0x88, 0x97, 0x40, + 0xed, 0xaf, 0xe6, 0xe6, 0x3e, 0xb2, 0x3f, 0xca, + 0x32, 0x78, 0x6f, 0xcc, 0xfd, 0xb2, 0x82, 0xbb, + 0x28, 0x76, 0xa9, 0xf4, 0x3b, 0x22, 0x82, 0x45, + 0xdf, 0x05, 0x76, 0x61, 0xff, 0x94, 0x3f, 0x61, + 0x50, 0x71, 0x6a, 0x20, 0xea, 0x18, 0x51, 0xe8, + 0xa7, 0xe9, 0xf5, 0x4e, 0x62, 0x02, 0x97, 0x66, + 0x46, 0x18, 0x43, 0x8d, 0xae, 0xac, 0x00, 0x00, + 0x00, 0x00, + } + RegtestnetGenesisHash = []byte{ + 0x81, 0x91, 0x37, 0x60, 0xab, 0x59, 0x85, 0x57, + 0x7b, 0x23, 0x4d, 0xf6, 0xe2, 0x65, 0xba, 0x6b, + 0x48, 0x7e, 0x66, 0x25, 0xc8, 0x52, 0x2a, 0xdc, + 0x83, 0xa1, 0x0e, 0x22, 0x9e, 0xb7, 0xe9, 0x69, + } + RegtestnetGenesisBlock = []byte{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xb7, 0x9a, 0x9b, 0x6f, + 0x31, 0xa9, 0xd7, 0xd2, 0x5a, 0x1c, 0x4b, 0x0e, + 0xc7, 0xa6, 0x71, 0xdc, 0x56, 0xce, 0x76, 0x63, + 0xc3, 0x80, 0xf2, 0xd2, 0x51, 0x3a, 0x8e, 0x65, + 0xe4, 0xea, 0x43, 0xc8, 0xd4, 0xe5, 0xc9, 0x53, + 0xff, 0xff, 0x7f, 0x20, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0xff, 0xff, 0x3a, 0x04, 0xff, 0xff, 0x00, 0x1d, + 0x01, 0x04, 0x32, 0x4e, 0x59, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x20, 0x32, 0x30, 0x31, 0x34, 0x2d, + 0x30, 0x37, 0x2d, 0x31, 0x39, 0x20, 0x2d, 0x20, + 0x44, 0x65, 0x6c, 0x6c, 0x20, 0x42, 0x65, 0x67, + 0x69, 0x6e, 0x73, 0x20, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x42, 0x69, + 0x74, 0x63, 0x6f, 0x69, 0x6e, 0xff, 0xff, 0xff, + 0xff, 0x01, 0x00, 0xe8, 0x76, 0x48, 0x17, 0x00, + 0x00, 0x00, 0x43, 0x41, 0x04, 0xe0, 0xd2, 0x71, + 0x72, 0x51, 0x0c, 0x68, 0x06, 0x88, 0x97, 0x40, + 0xed, 0xaf, 0xe6, 0xe6, 0x3e, 0xb2, 0x3f, 0xca, + 0x32, 0x78, 0x6f, 0xcc, 0xfd, 0xb2, 0x82, 0xbb, + 0x28, 0x76, 0xa9, 0xf4, 0x3b, 0x22, 0x82, 0x45, + 0xdf, 0x05, 0x76, 0x61, 0xff, 0x94, 0x3f, 0x61, + 0x50, 0x71, 0x6a, 0x20, 0xea, 0x18, 0x51, 0xe8, + 0xa7, 0xe9, 0xf5, 0x4e, 0x62, 0x02, 0x97, 0x66, + 0x46, 0x18, 0x43, 0x8d, 0xae, 0xac, 0x00, 0x00, + 0x00, 0x00, + } +) diff --git a/cmd/node/parameters/genesistohex_test.go b/cmd/node/parameters/genesistohex_test.go new file mode 100644 index 0000000..22cec42 --- /dev/null +++ b/cmd/node/parameters/genesistohex_test.go @@ -0,0 +1,57 @@ +package parameters + +import ( + "encoding/hex" + "fmt" + "testing" +) + +var ( + mainnetGenesisHash, _ = hex.DecodeString(`000009f0fcbad3aac904d3660cfdcf238bf298cfe73adf1d39d14fc5c740ccc7`) + mainnetGenesisBlock, _ = hex.DecodeString(`020000000000000000000000000000000000000000000000000000000000000000000000b79a9b6f31a9d7d25a1c4b0ec7a671dc56ce7663c380f2d2513a8e65e4ea43c8dcecc953ffff0f1e810201000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a04ffff001d0104324e5954696d657320323031342d30372d3139202d2044656c6c20426567696e7320416363657074696e6720426974636f696effffffff0100e8764817000000434104e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438daeac00000000`) + testnetGenesisHash, _ = hex.DecodeString(`00000e41ecbaa35ef91b0c2c22ed4d85fa12bbc87da2668fe17572695fb30cdf`) + testnetGenesisBlock, _ = hex.DecodeString(`020000000000000000000000000000000000000000000000000000000000000000000000b79a9b6f31a9d7d25a1c4b0ec7a671dc56ce7663c380f2d2513a8e65e4ea43c884eac953ffff0f1e18df1a000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a04ffff001d0104324e5954696d657320323031342d30372d3139202d2044656c6c20426567696e7320416363657074696e6720426974636f696effffffff0100e8764817000000434104e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438daeac00000000`) + regtestnetGenesisHash, _ = hex.DecodeString(`69e9b79e220ea183dc2a52c825667e486bba65e2f64d237b578559ab60379181`) + regtestnetGenesisBlock, _ = hex.DecodeString(`020000000000000000000000000000000000000000000000000000000000000000000000b79a9b6f31a9d7d25a1c4b0ec7a671dc56ce7663c380f2d2513a8e65e4ea43c8d4e5c953ffff7f20010000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff3a04ffff001d0104324e5954696d657320323031342d30372d3139202d2044656c6c20426567696e7320416363657074696e6720426974636f696effffffff0100e8764817000000434104e0d27172510c6806889740edafe6e63eb23fca32786fccfdb282bb2876a9f43b228245df057661ff943f6150716a20ea1851e8a7e9f54e620297664618438daeac00000000`) +) + +func TestGenesisToHex(t *testing.T) { + printByteAssignments("mainnetGenesisHash", *rev(mainnetGenesisHash)) + printByteAssignments("mainnetGenesisBlock", mainnetGenesisBlock) + printByteAssignments("testnetGenesisHash", *rev(testnetGenesisHash)) + printByteAssignments("testnetGenesisBlock", testnetGenesisBlock) + printByteAssignments("regtestnetGenesisHash", *rev(regtestnetGenesisHash)) + printByteAssignments("regtestnetGenesisBlock", regtestnetGenesisBlock) +} +func printByteAssignments(name string, in []byte) { + fmt.Print(name, "=[]byte{\n") + printGoHexes(in) + fmt.Print("}\n") +} +func printGoHexes(in []byte) { + fmt.Print("\t") + for i := range in { + if i%8 == 0 && i != 0 { + fmt.Print("\n\t") + } + fmt.Printf("0x%02x, ", in[i]) + } + fmt.Println() +} +func rev(in []byte) (out *[]byte) { + o := make([]byte, len(in)) + out = &o + for i := range in { + (*out)[len(in)-i-1] = in[i] + } + return +} + +// func hx(// in []byte) string { +// return hex.EncodeToString(in) +// } +// func split(// in []byte, pos int) (out []byte, piece []byte) { +// out = in[pos:] +// piece = in[:pos] +// return +// } diff --git a/cmd/wallet/CHANGES b/cmd/wallet/CHANGES new file mode 100644 index 0000000..734f711 --- /dev/null +++ b/cmd/wallet/CHANGES @@ -0,0 +1,527 @@ +============================================================================ +User visible changes for btcwallet + A wallet daemon for pod, written in Go +============================================================================ + +Changes in 0.7.0 (Mon Nov 23 2015) + - New features: + - Wallet will now detect network inactivity and reconnect to the pod + RPC server if the connection was lost (#320) + + - Bug fixes: + - Removed data races in the RPC server (#292) and waddrmgr package + (#293) + - Corrected handling of btcutil.AddressPubKey addresses when querying + for a ManagedAddress from the address manager (#313) + - Fixed signmessage and verifymessage algorithm to match the equivalent + algorithms used by Core (#324) + + - Notable developer-related changes: + - Added support for AppVeyor continuous integration (#299) + - Take advantage of optimized zeroing from the Go 1.5 release (#286) + - Added IsError function to waddrmgr to check that an error is a + ManagerError and contains a matching error code (#289). Simplified + error handling in the wallet package and RPC server with this function + (#290). + - Switched to using a more space efficient data structure for the + wtxmgr CreditRecord type (#295) + - Incorporated latest updates to the votingpool package (#315) + + - Miscellaneous: + - Updated websocket notification handlers to latest API required by + pod (#294) + - Enabled the logging subsystem of the rpcclient package (#328) + + - Contributors (alphabetical order): + - Alex Yocom-Piatt + - cjepson + - Dave Collins + - John C. Vernaleo + - Josh Rickmar + - Rune T. Aune + +Changes in 0.6.0 (Wed May 27 2015) + - New features: + - Add initial account support (#155): + - Add account names for each account number + - Create initial account with the "default" name + - Create new accounts using the createnewaccount RPC + - All accounts (with the exception of the imported account) may be + renamed using the renameaccount RPC + - RPC requests with an unspecified account that default to the unnamed + account in Bitcoin Core Wallet default to "default", the name of the + initial account + - Several RPCs with account parameters do not work with btcwallet + accounts due to concerns over expectations of API compatibility with + Bitcoin Core Wallet. A new RPC API is being planned to rectify this + (#220). + - Store transactions, transaction history, and spend tracking in the + database (#217, #234) + - A full rescan is required when updating from previous wallet + versions to rebuild the transaction history + - Add utility (cmd/dropwtxmgr) to drop transaction history and force a + rescan (#234) + - Implement the help RPC to return single line usages of all wallet and + pod server requests as well as detailed usage for a single request + + - Bug fixes: + - Handle chain reorgs by unconfirming transactions from removed blocks + (#248) + - Rollback all transaction history when none of the saved recently seen + block hashes are known to pod (#234, #281) + - Prevent the situation where the default account was renamed but cannot + be renamed back to "" or "default" by removing the special case naming + policy for the default account (#253) + - Create the initial account address if needed when calling the + getaccountaddress RPC (#238) + - Prevent listsinceblock RPC from including all listtransactions result + objects for all transactions since the genesis block (fix included in + #227) + - Add missing fields to listtransactions and gettransaction RPC results + (#265) + - Remove target confirmations limit on listsinceblock results (#266) + - Add JSON array to report errors creating input signature for + signrawtransaction RPC (#267) + - Use negative fees with listtransactions result types (#272) + - Prevent duplicate wallet lock attempt after timeout if explicitly + locked (#275) + - Use correct RPC server JSON-RPC error code for incorrect passphrases + with a walletpassphrase request (#284) + + - Regressions: + - Inserting transactions and marking outputs as controlled by wallet in + the new transaction database is extremely slow compared to the previous + in-memory implementation. Later versions may improve this performance + regression by using write-ahead logging (WAL) and performing more + updates at a time under a single database transaction. + + - Notable developer-related changes: + - Relicense all code to the btcsuite developers (#258) + - Replace txstore package with wtxmgr, the walletdb-based transaction + store (#217, #234) + - Add Cursor API to walletdb for forwards and backwards iteration over + a bucket (included in #234) + - Factor out much of main's wallet.go into a wallet package (#213, + #276, #255) + - Convert RPC server and client to btcjson v2 API (#233, #227) + - Help text and single line usages for the help RPC are pregenerated + from descriptions in the internal/rpchelp package and saved as + globals in main. Help text must be regenerated (using `go generate`) + each time the btcjson struct tags change or the help definitions are + modified. + - Add additional features to the votingpool package: + - Implement StartWithdrawal API to begin an Open Transactions + withdrawal (#178) + - Add internal APIs to store withdrawal transactions in the wallet's + transaction database (#221) + - Addresses marked as used after appearing publicly on the blockchain or + in mempool; required for future single-use address support (#207) + - Modified waddrmgr APIs to use ForEach functions to iterate over + address strings and managed addresses to improve scability (#216) + - Move legacy directory under internal directory to prevent importing + of unmaintained packages (enforced since Go 1.5) (#285) + - Improve test coverage in the waddrmgr and wtxmgr packages (#239, #217) + + - Contributors (alphabetical order): + - Dave Collins + - Guilherme Salgado + - Javed Khan + - Josh Rickmar + - Manan Patel + +Changes in 0.5.1 (Fri Mar 06 2015) + - New features: + - Add flag (--createtemp) to create a temporary simnet wallet + + - Bug fixes: + - Mark newly received transactions confirmed when the wallet is initially + created or opened with no addresses + + - Notable developer-related changes: + - Refactor the address manager database upgrade paths for easier future + upgrades + - Private key zeroing functions consolidated into the internal zero package + and optimized + +Changes in 0.5.0 (Tue Mar 03 2015) + - New features: + - Add a new address manager package (waddrmgr) to replace the previous + wallet/keystore package: + - BIP0032 hierarchical deterministic keys + - BIP0043/BIP0044 multi-account hierarchy + - Strong focus on security: + - Wallet master encryption keys protected by scrypt PBKDF + - NaCl-based secretbox cryptography (XSalsa20 and Poly1305) + - Mandatory encryption of private keys and P2SH redeeming scripts + - Optional encryption of public data, including extended public keys + and addresses + - Different crypto keys for redeeming scripts to mitigate cryptanalysis + - Hardened against memory scraping through the use of actively clearing + private material from memory when locked + - Different crypto keys used for public, private, and script data + - Ability for different passphrases for public and private data + - Multi-tier scalable key design to allow instant password changes + regardless of the number of addresses stored + - Import WIF keys + - Import pay-to-script-hash scripts for things such as multi-signature + transactions + - Ability to export a watching-only version which does not contain any + private key material + - Programmatically detectable errors, including encapsulation of errors + from packages it relies on + - Address synchronization capabilities + - Add a new namespaced database package (walletdb): + - Key/value store + - Namespace support + - Allows multiple packages to have their own area in the database without + worrying about conflicts + - Read-only and read-write transactions with both manual and managed modes + - Nested buckets + - Supports registration of backend databases + - Comprehensive test coverage + - Replace the createencryptedwallet RPC with a wizard-style prompt + (--create) to create a new walletdb-backed wallet file and import keys + from the old Armory wallet file (if any) + - Transaction creation changes: + - Drop default transaction fee to 0.00001 DUO per kB + - Use standard script flags provided by the txscript package for + transaction creation and sanity checking + - Randomize change output index + - Includes amounts (total spendable, total needed, and fee) in all + insufficient funds errors + - Add support for simnet, the private simulation test network + - Implement the following Bitcoin Core RPCs: + - listreceivedbyaddress (#53) + - lockunspent, listlockunspent (#50, #55) + - getreceivedbyaddress + - listreceivedbyaccount + - Reimplement pod RPCs which return the best block to use the block most + recently processed by wallet to avoid confirmation races: + - getbestblockhash + - getblockcount + - Perform clean shutdown on interrupt or when a stop RPC is received (#69) + - Throttle the number of connected HTTP POST and websocket client + connections (tunable using the rpcmaxclients and rpcmaxwebsockets config + options) + - Provide the ability to disable TLS when connecting to a localhost pod or + serving localhost clients + + - Rescan improvements: + - Add a rescan notification for when the rescan has completed and no more + rescan notifications are expected (#99) + - Use the most recent partial sync height from a rescan progress + notification when a rescan is restarted after the pod connection is lost + - Force a rescan if the transaction store cannot be opened (due to a + missing file or if the deserialization failed) + + - RPC compatibility improvements: + - Allow the use of the `*` account name to refer to all accounts + - Make the account parameter optional for the getbalance and + listalltransactions requests + - Add iswatchonly field to the validateaddress response result + - Check address equivalence in verifymessage by comparing pubkeys and pubkey + hashes rather than requiring the address being verified to be one + controlled by the wallet and using its private key for verification + + - Bug fixes: + - Prevent an out-of-bounds panic when handling a gettransaction RPC. + - Prevent a panic on client disconnect (#110). + - Prevent double spending coins when creating multiple transactions at once + by serializing access to the transaction creation logic (#120) + - Mark unconfirmed transaction credits as spent when another unconfirmed + transaction spends one (#91) + - Exclude immature coinbase outputs from listunspent results (#103) + - Fix several data and logic races during sync with pod (#101) + - Avoid a memory issue from incorrect slice usage which caused both + duplicate and missing blocks in the transaction store when middle + inserting transactions from a new block + - Only spend P2PKH outputs when creating sendfrom/sendmany/sendtoaddress + transactions (#89) + - Return the correct UTXO set when fetching all wallet UTXOs by fixing an + incorrect slice append + - Remove a deadlock caused by filling the pod notification channel (#100) + - Avoid a confirmation race by using the most recently processed block in + RPC handlers, rather than using the most recently notified block by pod + - Marshal empty JSON arrays as `[]` instead of the JSON `null` by using + empty, non-nil Go slices + - Flush logs and run all deferred functions before main returns and the + process exits + - Sync temporary transaction store flat file before closing and renaming + - Accept hex strings with an odd number of characters + + - Notable developer-related changes: + - Switch from the go.net websocket package to gorilla websockets + - Refactor the RPC server: + - Move several global variables to the RPCServer struct + - Dynamically look up appropriate handlers for the current pod connection + status and wallet sync state + - Begin creating websocket notifications by sending to one of many + notification channels in the RPCServer struct, which are in turn + marshalled and broadcast to each websocket client + - Separate the RPC client code into the chain package: + - Uses rpcclient for a pod websocket RPC client + - Converts all notification callbacks to typed messages sent over channels + - Uses an unbounded queue for waiting notifications + - Import a new voting pool package (votingpool): + - Create and fetch voting pools and series from a walletdb namespace + - Generate deposit addresses utilizing m-of-n multisig P2SH scripts + - Improve transaction creation readability by splitting a monolithic + function into several smaller ones + - Check and handle all errors in some way, or explicitly comment why a + particular error was left unchecked + - Simplify RPC error handling by wrapping specific errors in unique types to + create an appropriate btcjson error before the response is marshalled + - Add a map of unspent outputs (keyed by outpoint) to the transaction store + for quick lookup of any UTXO and access to the full wallet UTXO set + without iterating over many transactions looking for unspent credits + - Modify several data structures and function signatures have been modified + to reduce the number of needed allocations and be more cache friendly + + - Miscellaneous: + - Rewrite paths relative to the data directory when an alternate data + directory is provided on the command line + - Switch the websocket endpoint to `ws` to match pod + - Remove the getaddressbalance extension RPC to discourage address reuse and + encourage watching for expected payments by using listunspent + - Increase transaction creation performance by moving the sorting of + transaction outputs by their amount out of an inner loop + - Add additional logging to the transaction store: + - Log each transaction added to the store + - Log each previously unconfirmed transaction that is mined + - [debug] Log which previous outputs are marked spent by a newly inserted + debiting transaction + - [debug] Log each transaction that is removed in a rollback + - Only log rollbacks if transactions are reorged out of the old chain + - Save logs to network-specific directories + (e.g. ~/.btcwallet/logs/testnet3) to match pod behavior (#114) + +Changes in 0.4.0 (Sun May 25 2014) + - Implement the following standard bitcoin server RPC requests: + - signmessage (https://github.com/conformal/btcwallet/issues/58) + - verifymessage (https://github.com/conformal/btcwallet/issues/61) + - listunspent (https://github.com/conformal/btcwallet/issues/54) + - validateaddress (https://github.com/conformal/btcwallet/issues/60) + - addressmultisig (https://github.com/conformal/btcwallet/issues/37) + - createmultisig (https://github.com/conformal/btcwallet/issues/37) + - signrawtransaction (https://github.com/conformal/btcwallet/issues/59) + + - Add authenticate extension RPC request to authenticate a websocket + session without requiring the use of the HTTP Authorization header + + - Add podusername and podpassword options to allow separate + authentication credentials from wallet clients when authenticating to a + pod websocket RPC server + + - Fix RPC response passthrough: JSON unmarshaling and marshaling is now + delayed until necessary and JSON result objects from pod are sent to + clients directly without an extra decode+encode that may change the + representation of large integer values + + - Fix several websocket client connection issues: + - Disconnect clients are cleanly removed without hanging on any final + sends + - Set deadline for websocket client sends to prevent hanging on + misbehaving clients or clients with a bad connection + + - Fix return result for dumprivkey by always padding the private key bytes + to a length of 32 + + - Fix rescan for transaction history for imported addresses + (https://github.com/conformal/btcwallet/issues/74) + + - Fix listsinceblock request handler to consider the minimum confirmation + parameter (https://github.com/conformal/btcwallet/issues/80) + + - Fix several RPC handlers which require an unlocked wallet to check + for an unlocked wallet before continuing + (https://github.com/conformal/btcwallet/issues/65) + + - Fix handling for block rewards (coinbase transactions): + - Update listtransactions results to use "generate" category for + coinbase outputs + - Prevent inclusion of immature coinbase outputs for newly created + transactions + + - Rewrite the transaction store to handle several issues regarding + transation malleability and performance issues + - The new transaction store is written to disk in a different format + then before, and upgrades will require a rescan to rebuild the + transaction history + + - Improve rescan: + - Begin rescan with known UTXO set at start height + - Serialize executation of all rescan requests + - Merge waiting rescan jobs so all jobs can be handled with a single + rescan + - Support parially synced addresses in the keystore and incrementally + mark rescan progress. If a rescan is unable to continue (wallet + closes, pod disconnects, etc.) a new rescan can start at the last + synced chain height + + - Notify (with an unsolicited notification) websocket clients of pod + connection state + + - Improve logging: + - Log reason for disconnecting a websocket client + + - Updates for pod websocket API changes + + - Stability fixes, internal API changes, general code cleanup, and comment + corrections + +Changes in 0.3.0 (Mon Feb 10 2014) + - Use correct hash algorithm for chained addresses (fixes a bug where + address chaining was still deterministic, but forked from Armory and + previous btcwallet implementations) + + - Change websocket endpoint to connect to pod 0.6.0-alpha + + - Redo server implementation to serialize handling of client requests + + - Redo account locking to greatly reduce btcwallet lockups caused by + incorrect mutex usage + + - Open all accounts, rather than just the default account, at startup + + - Generate new addresses using pubkey chaining if keypool is depleted and + wallet is locked + + - Make maximum keypool size a configuration option (keypoolsize) + + - Add disallowfree configuration option (default false) to force adding + the minimum fee to all outbound transactions + + - Implement the following standard bitcoin server RPC requests: + - getinfo (https://github.com/conformal/btcwallet/issues/63) + - getrawchangeaddress (https://github.com/conformal/btcwallet/issues/41) + - getreceivedbyaccount (https://github.com/conformal/btcwallet/issues/42) + - gettransaction (https://github.com/conformal/btcwallet/issues/44) + - keypoolrefill (https://github.com/conformal/btcwallet/issues/48) + - listsinceblock (https://github.com/conformal/btcwallet/issues/52) + - sendtoaddress (https://github.com/conformal/btcwallet/issues/56) + + - Add empty (unimplemented) handlers for the following RPC requests so + requests are not passed down to pod: + - getblocktemplate + - getwork + - stop + + - Add RPC extension request, exportwatchingwallet, to export an account + with a watching-only wallet from an account with a hot wallet that + may be used by a separate btcwallet instance + + - Require all account wallets to share the same passphrase + + - Change walletlock and walletpassphrase RPC requests to lock or unlock + all account wallets + + - Allow opening accounts with watching-only wallets + + - Return txid for sendfrom RPC requests + (https://github.com/conformal/btcwallet/issues/64) + + - Rescan imported private keys in background + (https://github.com/conformal/btcwallet/issues/34) + + - Do not import duplicate private keys + (https://github.com/conformal/btcwallet/issues/35) + + - Write all three account files for a new account, rather than just + the wallet (https://github.com/conformal/btcwallet/issues/30) + + - Create any missing directories before writing autogenerated certificate + pair + + - Fix rescanning of a new account's root address + + - Fix error in the wallet file serialization causing duplicate address + encryption attempts + + - Fix issue calculating eligible transaction inputs caused by a bad + confirmation check + + - Fix file locking issue on Windows caused by not closing files before + renaming + + - Fix typos in README file + +Changes in 0.2.1 (Thu Jan 10 2014) + - Fix a mutex issue which caused btcwallet to lockup on all + RPC requests needing to read or write an account + +Changes in 0.2.0 (Thu Jan 09 2014) + - Enable mainnet support (disabled by default, use --mainnet to enable) + + - Don't hardcode localhost pod connections. Instead, add a --connect + option to specify the hostname or address and port of a local or + remote pod instance + (https://github.com/conformal/btcwallet/issues/1) + + - Remove --serverport port and replace with --listen. This option works + just like pod's --rpclisten and allows to specify the interfaces to + listen for RPC connections + + - Require TLS and Basic HTTP authentication before wallet can be + controlled over RPC + + - Refill keypool if wallet is unlocked and keypool is emptied + + - Detect and rollback saved tx/utxo info after pod performs blockchain + reorganizations while btcwallet was disconnected + + - Add support for the following standard bitcoin JSON-RPC calls: + - dumpprivkey (https://github.com/conformal/btcwallet/issues/9) + - getaccount + - getaccountaddress + - importprivkey (https://github.com/conformal/btcwallet/issues/2) + - listtransactions (https://github.com/conformal/btcwallet/issues/12) + + - Add several extension RPC calls for websocket connections: + - getaddressbalance: get the balance associated with a single address + - getunconfirmedbalance: get total balance for unconfirmed transactions + - listaddresstransactions: list transactions for a single address + (https://github.com/conformal/btcwallet/issues/27) + - listalltransactions: lists all transactions without specifying a range + + - Make RPC extensions available only to websocket connections, with the + exception of createencryptedwallet + + - Add dummy handlers for unimplemented wallet RPC calls + (https://github.com/conformal/btcwallet/issues/29) + + - Add socks5/tor proxy support + + - Calculate and add minimum transaction fee to created transactions + + - Use OS-specific rename calls to provide atomic file renames which + can replace a currently-existing file + (https://github.com/conformal/btcwallet/issues/20) + + - Move account files to a single directory per bitcoin network to + prevent a future scaling issue + (https://github.com/conformal/btcwallet/issues/16) + + - Fix several data races and mutex mishandling + + - Fix a bug where the RPC server hung on requests requiring pod + when a pod connection was never established + + - Fix a bug where creating account files did not create all necessary + directories (https://github.com/conformal/btcwallet/issues/15) + + - Fix a bug where '~' did not expand to a home or user directory + (https://github.com/conformal/btcwallet/issues/17) + + - Fix a bug where returning account names as strings did not remove + trailing ending 0s + + - Fix a bug where help usage was displayed twice using the -h or --help + flag + + - Fix sample listening address in sample configuration file + + - Update sample configuration file with all available options with + descriptions and defaults for each + +Initial Release 0.1.0 (Wed Nov 13 2013) + - Initial release diff --git a/cmd/wallet/LICENSE b/cmd/wallet/LICENSE new file mode 100644 index 0000000..d7da5c6 --- /dev/null +++ b/cmd/wallet/LICENSE @@ -0,0 +1,17 @@ +ISC License + +Copyright (c) 2018- The Parallelcoin Team +Copyright (c) 2013-2017 The btcsuite developers +Copyright (c) 2015-2016 The Decred developers + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/cmd/wallet/README.md b/cmd/wallet/README.md new file mode 100755 index 0000000..bd36a67 --- /dev/null +++ b/cmd/wallet/README.md @@ -0,0 +1,221 @@ +pod wallet +========= + +[![Build Status](https://travis-ci.org/btcsuite/btcwallet.png?branch=master)](https://travis-ci.org/btcsuite/btcwallet) +[![Build status](https://ci.appveyor.com/api/projects/status/88nxvckdj8upqr36/branch/master?svg=true)](https://ci.appveyor.com/project/jrick/btcwallet/branch/master) + +btcwallet is a daemon handling bitcoin wallet functionality for a single user. +It acts as both an RPC client to pod and an RPC server for wallet clients and +legacy RPC applications. + +Public and private keys are derived using the hierarchical deterministic format +described by +[BIP0032](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki). +Unencrypted private keys are not supported and are never written to disk. +btcwallet uses the +`m/44'/'/'//
` +HD path for all derived addresses, as described by +[BIP0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki). + +Due to the sensitive nature of public data in a BIP0032 wallet, btcwallet +provides the option of encrypting not just private keys, but public data as +well. This is intended to thwart privacy risks where a wallet file is +compromised without exposing all current and future addresses (public keys) +managed by the wallet. While access to this information would not allow an +attacker to spend or steal coins, it does mean they could track all transactions +involving your addresses and therefore know your exact balance. In a future +release, public data encryption will extend to transactions as well. + +btcwallet is not an SPV client and requires connecting to a local or remote pod +instance for asynchronous blockchain queries and notifications over websockets. +Full pod installation instructions can be +found [here](https://github.com/parallelcointeam/parallelcoin). An alternative +SPV mode that is compatible with pod and Bitcoin Core is planned for a future +release. + +Wallet clients can use one of two RPC servers: + +1. A legacy JSON-RPC server mostly compatible with Bitcoin Core + + The JSON-RPC server exists to ease the migration of wallet applications from + Core, but complete compatibility is not guaranteed. Some portions of the + API (and especially accounts) have to work differently due to other design + decisions (mostly due to BIP0044). However, if you find a compatibility issue + and feel that it could be reasonably supported, please report an issue. This + server is enabled by default. + +2. An experimental gRPC server + + The gRPC server uses a new API built for btcwallet, but the API is not + stabilized and the server is feature gated behind a config option + (`--experimentalrpclisten`). If you don't mind applications breaking due to + API changes, don't want to deal with issues of the legacy API, or need + notifications for changes to the wallet, this is the RPC server to use. The + gRPC server is documented [here](./rpc/documentation/README.md). + +## Installation and updating + +### Windows - MSIs Available + +Install the latest MSIs available here: + +https://github.com/p9c/p9/releases + +https://github.com/p9c/p9/walletmain/releases + +### Windows/Linux/BSD/POSIX - Build from source + +Building or updating from source requires the following build dependencies: + +- **Go 1.5 or 1.6** + + Installation instructions can be found here: http://golang.org/doc/install. It + is recommended to add `$GOPATH/bin` to your `PATH` at this point. + + **Note:** If you are using Go 1.5, you must manually enable the vendor + experiment by setting the `GO15VENDOREXPERIMENT` environment variable to + `1`. This step is not required for Go 1.6. + +- **Glide** + + Glide is used to manage project dependencies and provide reproducible builds. + To install: + + `go get -u github.com/Masterminds/glide` + +Unfortunately, the use of `glide` prevents a handy tool such as `go get` from +automatically downloading, building, and installing the source in a single +command. Instead, the latest project and dependency sources must be first +obtained manually with `git` and `glide`, and then `go` is used to build and +install the project. + +**Getting the source**: + +For a first time installation, the project and dependency sources can be +obtained manually with `git` and `glide` ( +create directories as needed): + +``` +git clone https://github.com/p9c/p9/walletmain $GOPATH/src/github.com/p9c/p9/walletmain +cd $GOPATH/src/github.com/p9c/p9/walletmain +glide install +``` + +To update an existing source tree, pull the latest changes and install the +matching dependencies: + +``` +cd $GOPATH/src/github.com/p9c/p9/walletmain +git pull +glide install +``` + +**Building/Installing**: + +The `go` tool is used to build or install (to `GOPATH`) the project. Some +example build instructions are provided below (all must run from the `btcwallet` +project directory). + +To build and install `btcwallet` and all helper commands (in the `cmd` +directory) to `$GOPATH/bin/`, as well as installing all compiled packages to +`$GOPATH/pkg/` (**use this if you are unsure which command to run**): + +``` +go install . ./cmd/... +``` + +To build a `btcwallet` executable and install it to `$GOPATH/bin/`: + +``` +go install +``` + +To build a `btcwallet` executable and place it in the current directory: + +``` +go build +``` + +## Getting Started + +The following instructions detail how to get started with btcwallet connecting +to a localhost pod. Commands should be run in `cmd.exe` or PowerShell on +Windows, or any terminal emulator on *nix. + +- Run the following command to start pod: + +``` +pod -u rpcuser -P rpcpass +``` + +- Run the following command to create a wallet: + +``` +btcwallet -u rpcuser -P rpcpass --create +``` + +- Run the following command to start btcwallet: + +``` +btcwallet -u rpcuser -P rpcpass +``` + +If everything appears to be working, it is recommended at this point to copy the +sample pod and btcwallet configurations and update with your RPC username and +password. + +PowerShell (Installed from MSI): + +``` +PS> cp "$env:ProgramFiles\Pod Suite\Pod\sample-pod.conf" $env:LOCALAPPDATA\Pod\pod.conf +PS> cp "$env:ProgramFiles\Pod Suite\Btcwallet\sample-btcwallet.conf" $env:LOCALAPPDATA\Btcwallet\btcwallet.conf +PS> $editor $env:LOCALAPPDATA\Pod\pod.conf +PS> $editor $env:LOCALAPPDATA\Btcwallet\btcwallet.conf +``` + +PowerShell (Installed from source): + +``` +PS> cp $env:GOPATH\src\github.com\btcsuite\pod\sample-pod.conf $env:LOCALAPPDATA\Pod\pod.conf +PS> cp $env:GOPATH\src\github.com\btcsuite\btcwallet\sample-btcwallet.conf $env:LOCALAPPDATA\Btcwallet\btcwallet.conf +PS> $editor $env:LOCALAPPDATA\Pod\pod.conf +PS> $editor $env:LOCALAPPDATA\Btcwallet\btcwallet.conf +``` + +Linux/BSD/POSIX (Installed from source): + +```bash +$ cp $GOPATH/src/github.com/p9c/p9/sample-pod.conf ~/.pod/pod.conf +$ cp $GOPATH/src/github.com/p9c/p9/walletmain/sample-btcwallet.conf ~/.btcwallet/btcwallet.conf +$ $EDITOR ~/.pod/pod.conf +$ $EDITOR ~/.btcwallet/btcwallet.conf +``` + +## Issue Tracker + +The [integrated github issue tracker](https://github.com/p9c/p9/walletmain/issues) +is used for this project. + +## GPG Verification Key + +All official release tags are signed by Conformal so users can ensure the code +has not been tampered with and is coming from the btcsuite developers. To verify +the signature perform the following: + +- Download the public key from the Conformal website at + https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt + +- Import the public key into your GPG keyring: + ```bash + gpg --import GIT-GPG-KEY-conformal.txt + ``` + +- Verify the release tag with the following command where `TAG_NAME` is a + placeholder for the specific tag: + ```bash + git tag -v TAG_NAME + ``` + +## License + +btcwallet is licensed under the liberal ISC License. diff --git a/cmd/wallet/_config.go_ b/cmd/wallet/_config.go_ new file mode 100644 index 0000000..177f21c --- /dev/null +++ b/cmd/wallet/_config.go_ @@ -0,0 +1,735 @@ +package wallet + +import ( + "time" + + "github.com/urfave/cli" +) + +// Config is the main configuration for wallet +type Config struct { + // General application behavior + ConfigFile *string `short:"C" long:"configfile" description:"Path to configuration file"` + ShowVersion *bool `short:"True" long:"version" description:"Display version information and exit"` + LogLevel *string + Create *bool `long:"create" description:"Create the wallet if it does not exist"` + CreateTemp *bool `long:"createtemp" description:"Create a temporary simulation wallet (pass=password) in the data directory indicated; must call with --datadir"` + AppDataDir *string `short:"A" long:"appdata" description:"Application data directory for wallet config, databases and logs"` + TestNet3 *bool `long:"testnet" description:"Use the test Bitcoin network (version 3) (default mainnet)"` + SimNet *bool `long:"simnet" description:"Use the simulation test network (default mainnet)"` + NoInitialLoad *bool `long:"noinitialload" description:"Defer wallet creation/opening on startup and enable loading wallets over RPC"` + LogDir *string `long:"logdir" description:"Directory to log output."` + Profile *string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` + // GUI *bool `long:"__OLDgui" description:"Launch GUI"` + // Wallet options + WalletPass *string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` + // RPC client options + RPCConnect *string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of pod RPC server to connect to (default localhost:11048, testnet: localhost:21048, simnet: localhost:41048)"` + CAFile *string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with pod"` + EnableClientTLS *bool `long:"clienttls" description:"Enable TLS for the RPC client"` + PodUsername *string `long:"podusername" description:"Username for pod authentication"` + PodPassword *string `long:"podpassword" default-mask:"-" description:"Password for pod authentication"` + Proxy *string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` + ProxyUser *string `long:"proxyuser" description:"Username for proxy server"` + ProxyPass *string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` + // SPV client options + UseSPV *bool `long:"usespv" description:"Enables the experimental use of SPV rather than RPC for chain synchronization"` + AddPeers *cli.StringSlice `short:"a" long:"addpeer" description:"Add a peer to connect with at startup"` + ConnectPeers *cli.StringSlice `long:"connect" description:"Connect only to the specified peers at startup"` + MaxPeers *int `long:"maxpeers" description:"Max number of inbound and outbound peers"` + BanDuration *time.Duration `long:"banduration" description:"How long to ban misbehaving peers. Valid time units are {s, m, h}. Minimum 1 second"` + BanThreshold *int `long:"banthreshold" description:"Maximum allowed ban score before disconnecting and banning misbehaving peers."` + // RPC server options + // + // The legacy server is still enabled by default (and eventually will be replaced with the experimental server) so + // prepare for that change by renaming the struct fields (but not the configuration options). + // + // Usernames can also be used for the consensus RPC client, so they aren't considered legacy. + RPCCert *string `long:"rpccert" description:"File containing the certificate file"` + RPCKey *string `long:"rpckey" description:"File containing the certificate key"` + OneTimeTLSKey *bool `long:"onetimetlskey" description:"Generate a new TLS certpair at startup, but only write the certificate to disk"` + EnableServerTLS *bool `long:"servertls" description:"Enable TLS for the RPC server"` + LegacyRPCListeners *cli.StringSlice `long:"rpclisten" description:"Listen for legacy RPC connections on this interface/port (default port: 11046, testnet: 21046, simnet: 41046)"` + LegacyRPCMaxClients *int `long:"rpcmaxclients" description:"Max number of legacy RPC clients for standard connections"` + LegacyRPCMaxWebsockets *int `long:"rpcmaxwebsockets" description:"Max number of legacy RPC websocket connections"` + Username *string `short:"u" long:"username" description:"Username for legacy RPC and pod authentication (if podusername is unset)"` + Password *string `short:"P" long:"password" default-mask:"-" description:"Password for legacy RPC and pod authentication (if podpassword is unset)"` + // EXPERIMENTAL RPC server options + // + // These options will change (and require changes to config files, etc.) when the new gRPC server is enabled. + ExperimentalRPCListeners *cli.StringSlice `long:"experimentalrpclisten" description:"Listen for RPC connections on this interface/port"` + // Deprecated options + DataDir *string `short:"b" long:"datadir" default-mask:"-" description:"DEPRECATED -- use appdata instead"` +} + +// A bunch of constants +const () + +/* +// cleanAndExpandPath expands environement variables and leading ~ in the +// passed path, cleans the result, and returns it. +func cleanAndExpandPath(path string) string { +// NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style + // %VARIABLE%, but they variables can still be expanded via POSIX-style + // $VARIABLE. + path = os.ExpandEnv(path) + if !strings.HasPrefix(path, "~") { +return filepath.Clean(path) + } + // Expand initial ~ to the current user's home directory, or ~otheruser to otheruser's home directory. On Windows, both forward and backward slashes can be used. + path = path[1:] + var pathSeparators string + if runtime.GOOS == "windows" { +pathSeparators = string(os.PathSeparator) + "/" + } else { +pathSeparators = string(os.PathSeparator) + } + userName := "" + if i := strings.IndexAny(path, pathSeparators); i != -1 { +userName = path[:i] + path = path[i:] + } + homeDir := "" + var u *user.User + var e error + if userName == "" { +u, e = user.Current() + } else { +u, e = user.Lookup(userName) + } + if e == nil { +homeDir = u.HomeDir + } + // Fallback to CWD if user lookup fails or user has no home directory. + if homeDir == "" { +homeDir = "." + } + return filepath.Join(homeDir, path) +} +// createDefaultConfig creates a basic config file at the given destination path. +// For this it tries to read the config file for the RPC server (either pod or +// sac), and extract the RPC user and password from it. +func createDefaultConfigFile(destinationPath, serverConfigPath, + serverDataDir, walletDataDir string) (e error) { +// fmt.Println("server config path", serverConfigPath) + // Read the RPC server config + serverConfigFile, e := os.Open(serverConfigPath) + if e != nil { +return e + } + defer serverConfigFile.Close() + content, e := ioutil.ReadAll(serverConfigFile) + if e != nil { +return e + } + // content := []byte(samplePodCtlConf) + // Extract the rpcuser + rpcUserRegexp, e := regexp.Compile(`(?m)^\s*rpcuser=([^\s]+)`) + if e != nil { +return e + } + userSubmatches := rpcUserRegexp.FindSubmatch(content) + if userSubmatches == nil { +// No user found, nothing to do + return nil + } + // Extract the rpcpass + rpcPassRegexp, e := regexp.Compile(`(?m)^\s*rpcpass=([^\s]+)`) + if e != nil { +return e + } + passSubmatches := rpcPassRegexp.FindSubmatch(content) + if passSubmatches == nil { +// No password found, nothing to do + return nil + } + // Extract the TLS + TLSRegexp, e := regexp.Compile(`(?m)^\s*tls=(0|1)(?:\s|$)`) + if e != nil { +return e + } + TLSSubmatches := TLSRegexp.FindSubmatch(content) + // Create the destination directory if it does not exists + e = os.MkdirAll(filepath.Dir(destinationPath), 0700) + if e != nil { +return e + } + // fmt.Println("config path", destinationPath) + // Create the destination file and write the rpcuser and rpcpass to it + dest, e := os.OpenFile(destinationPath, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if e != nil { + return e + } + defer dest.Close() + destString := fmt.Sprintf("username=%s\npassword=%s\n", + string(userSubmatches[1]), string(passSubmatches[1])) + if TLSSubmatches != nil { + fmt.Println("TLS is enabled but more than likely the certificates will +fail verification because of the CA. +Currently there is no adequate tool for this, but will be soon.") + destString += fmt.Sprintf("clienttls=%s\n", TLSSubmatches[1]) + } + output := ";;; Defaults created from local pod/sac configuration:\n" + destString + "\n" + string(sampleModConf) + dest.WriteString(output) + return nil +} +func copy(src, dst string) (int64, error) { +// fmt.Println(src, dst) + sourceFileStat, e := os.Stat(src) + if e != nil { +return 0, e + } + if !sourceFileStat.Mode().IsRegular() { +return 0, fmt.Errorf("%s is not a regular file", src) + } + source, e := os.Open(src) + if e != nil { +return 0, e + } + defer source.Close() + destination, e := os.Create(dst) + if e != nil { +return 0, e + } + defer destination.Close() + nBytes, e := io.Copy(destination, source) + return nBytes, e +} +// supportedSubsystems returns a sorted slice of the supported subsystems for +// logging purposes. +func supportedSubsystems() []string { +// Convert the subsystemLoggers map keys to a slice. + subsystems := make([]string, 0, len(subsystemLoggers)) + for subsysID := range subsystemLoggers { +subsystems = append(subsystems, subsysID) + } + // Sort the subsytems for stable display. + txsort.Strings(subsystems) + return subsystems +} +// parseAndSetDebugLevels attempts to parse the specified debug level and set +// the levels accordingly. An appropriate error is returned if anything is +// invalid. +func parseAndSetDebugLevels(debugLevel string) (e error) { +// When the specified string doesn't have any delimters, treat it as + // the log level for all subsystems. + if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") { +// Validate debug log level. + if !validLogLevel(debugLevel) { +str := "The specified debug level [%v] is invalid" + return fmt.Errorf(str, debugLevel) + } + // Change the logging level for all subsystems. + setLogLevels(debugLevel) + return nil +} +// Split the specified string into subsystem/level pairs while detecting +// issues and update the log levels accordingly. +for _, logLevelPair := range strings.Split(debugLevel, ",") { +if !strings.Contains(logLevelPair, "=") { +str := "The specified debug level contains an invalid " + + "subsystem/level pair [%v]" + return fmt.Errorf(str, logLevelPair) + } + // Extract the specified subsystem and log level. + fields := strings.Split(logLevelPair, "=") + subsysID, logLevel := fields[0], fields[1] + // Validate subsystem. + if _, exists := subsystemLoggers[subsysID]; !exists { +str := "The specified subsystem [%v] is invalid -- " + + "supported subsytems %v" + return fmt.Errorf(str, subsysID, supportedSubsystems()) + } + // Validate log level. + if !validLogLevel(logLevel) { +str := "The specified debug level [%v] is invalid" + return fmt.Errorf(str, logLevel) + } + setLogLevel(subsysID, logLevel) + } + return nil +} +// loadConfig initializes and parses the config using a config file and command +// line options. +// +// The configuration proceeds as follows: +// 1) Start with a default config with sane settings +// 2) Pre-parse the command line to check for an alternative config file +// 3) Load configuration file overwriting defaults with any specified options +// 4) Parse CLI options and overwrite/add any specified options +// +// The above results in btcwallet functioning properly without any config +// settings while still allowing the user to override settings with config files +// and command line options. Command line options always take precedence. +func loadConfig( cfg *Config) (*Config, []string, error) { +cfg = Config{ + ConfigFile: DefaultConfigFile, + AppDataDir: DefaultAppDataDir, + LogDir: DefaultLogDir, + WalletPass: wallet.InsecurePubPassphrase, + CAFile: "", + RPCKey: DefaultRPCKeyFile, + RPCCert: DefaultRPCCertFile, + WalletRPCMaxClients: DefaultRPCMaxClients, + WalletRPCMaxWebsockets: DefaultRPCMaxWebsockets, + DataDir: DefaultAppDataDir, + // AddPeers: []string{}, + // ConnectPeers: []string{}, + } + // Pre-parse the command line options to see if an alternative config + // file or the version flag was specified. + preCfg := cfg + preParser := flags.NewParser(&preCfg, flags.Default) + _, e := preParser.Parse() + if e != nil { +if e, ok := e.(*flags.Error); !ok || e.Type != flags.ErrHelp { +preParser.WriteHelp(os.Stderr) + } + return nil, nil, e + } + // Show the version and exit if the version flag was specified. + funcName := "loadConfig" + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) + if preCfg.ShowVersion { +fmt.Println(appName, "version", version()) + os.Exit(0) + } + // Load additional config from file. + var configFileError error + parser := flags.NewParser(&cfg, flags.Default) + configFilePath := preCfg.ConfigFile.value + if preCfg.ConfigFile.ExplicitlySet() { +configFilePath = cleanAndExpandPath(configFilePath) + } else { +appDataDir := preCfg.AppDataDir.value + if !preCfg.AppDataDir.ExplicitlySet() && preCfg.DataDir.ExplicitlySet() { +appDataDir = cleanAndExpandPath(preCfg.DataDir.value) + } + if appDataDir != DefaultAppDataDir { +configFilePath = filepath.Join(appDataDir, DefaultConfigFilename) + } + } + e = flags.NewIniParser(parser).ParseFile(configFilePath) + if e != nil { +if _, ok := e.(*os.PathError); !ok { +fmt.Fprintln(os.Stderr, e) + parser.WriteHelp(os.Stderr) + return nil, nil, e + } + configFileError = e + } + // Parse command line options again to ensure they take precedence. + remainingArgs, e := parser.Parse() + if e != nil { +if e, ok := e.(*flags.Error); !ok || e.Type != flags.ErrHelp { +parser.WriteHelp(os.Stderr) + } + return nil, nil, e + } + // Chk deprecated aliases. The new options receive priority when both + // are changed from the default. + if cfg.DataDir.ExplicitlySet() { +fmt.Fprintln(os.Stderr, "datadir opt has been replaced by "+ + "appdata -- please update your config") + if !cfg.AppDataDir.ExplicitlySet() { +cfg.AppDataDir.value = cfg.DataDir.value + } + } + // If an alternate data directory was specified, and paths with defaults + // relative to the data dir are unchanged, modify each path to be + // relative to the new data dir. + if cfg.AppDataDir.ExplicitlySet() { +cfg.AppDataDir.value = cleanAndExpandPath(cfg.AppDataDir.value) + if !cfg.RPCKey.ExplicitlySet() { +cfg.RPCKey.value = filepath.Join(cfg.AppDataDir.value, "rpc.key") + } + if !cfg.RPCCert.ExplicitlySet() { +cfg.RPCCert.value = filepath.Join(cfg.AppDataDir.value, "rpc.cert") + } + } + if _, e = os.Stat(cfg.DataDir.value); os.IsNotExist(e) { +// Create the destination directory if it does not exists + e = os.MkdirAll(cfg.DataDir.value, 0700) + if e != nil { +fmt.Println("ERROR", e) + return nil, nil, e + } + } + var generatedRPCPass, generatedRPCUser string + if _, e = os.Stat(cfg.ConfigFile.value); os.IsNotExist(e) { +// If we can find a pod.conf in the standard location, copy + // copy the rpcuser and rpcpassword and TLS setting + c := cleanAndExpandPath("~/.pod/pod.conf") + // fmt.Println("server config path:", c) + // _, e = os.Stat(c) + // fmt.Println(e) + // fmt.Println(os.IsNotExist(err)) + if _, e = os.Stat(c); e == nil { +fmt.Println("Creating config from pod config") + createDefaultConfigFile(cfg.ConfigFile.value, c, cleanAndExpandPath("~/.pod"), + cfg.AppDataDir.value) + } else { +var bb bytes.Buffer + bb.Write(sampleModConf) + fmt.Println("Writing config file:", cfg.ConfigFile.value) + dest, e := os.OpenFile(cfg.ConfigFile.value, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if e != nil { +fmt.Println("ERROR", e) + return nil, nil, e + } + defer dest.Close() + // We generate a random user and password + randomBytes := make([]byte, 20) + _, e = rand.Read(randomBytes) + if e != nil { +return nil, nil, e + } + generatedRPCUser = base64.StdEncoding.EncodeToString(randomBytes) + _, e = rand.Read(randomBytes) + if e != nil { +return nil, nil, e + } + generatedRPCPass = base64.StdEncoding.EncodeToString(randomBytes) + // We copy every line from the sample config file to the destination, + // only replacing the two lines for rpcuser and rpcpass + // + var line string + reader := bufio.NewReader(&bb) + for e != io.EOF { +line, e = reader.ReadString('\n') + if e != nil && e != io.EOF { +return nil, nil, e + } + if !strings.Contains(line, "podusername=") && !strings.Contains(line, "podpassword=") { +if strings.Contains(line, "username=") { +line = "username=" + generatedRPCUser + "\n" + } else if strings.Contains(line, "password=") { +line = "password=" + generatedRPCPass + "\n" + } + } + _, _ = generatedRPCPass, generatedRPCUser + if _, e = dest.WriteString(line); E.Chk(e) { +return nil, nil, e + } + } + } + } + // Choose the active network netparams based on the selected network. + // Multiple networks can't be selected simultaneously. + numNets := 0 + if cfg.TestNet3 { +activeNet = &chaincfg.TestNet3Params + numNets++ + } + if cfg.SimNet { +activeNet = &chaincfg.SimNetParams + numNets++ + } + if numNets > 1 { +str := "%s: The testnet and simnet netparams can't be used " + + "together -- choose one" + e := fmt.Errorf(str, "loadConfig") + fmt.Fprintln(os.Stderr, e) + parser.WriteHelp(os.Stderr) + return nil, nil, e + } + // Append the network type to the log directory so it is "namespaced" + // per network. + cfg.LogDir = cleanAndExpandPath(cfg.LogDir) + cfg.LogDir = filepath.Join(cfg.LogDir, activeNet.Params.Name) + // Special show command to list supported subsystems and exit. + if cfg.DebugLevel == "show" { +fmt.Println("Supported subsystems", supportedSubsystems()) + os.Exit(0) + } + // Initialize log rotation. After log rotation has been initialized, the + // logger variables may be used. + initLogRotator(filepath.Join(cfg.LogDir, DefaultLogFilename)) + // Parse, validate, and set debug log level(s). + if e := parseAndSetDebugLevels(cfg.DebugLevel); E.Chk(e) { +e := fmt.Errorf("%s: %v", "loadConfig", e.Error()) + fmt.Fprintln(os.Stderr, e) + parser.WriteHelp(os.Stderr) + return nil, nil, e + } + // Exit if you try to use a simulation wallet with a standard + // data directory. + if !(cfg.AppDataDir.ExplicitlySet() || cfg.DataDir.ExplicitlySet()) && cfg.CreateTemp { +fmt.Fprintln(os.Stderr, "Tried to create a temporary simulation "+ + "wallet, but failed to specify data directory!") + os.Exit(0) + } + // Exit if you try to use a simulation wallet on anything other than + // simnet or testnet3. + if !cfg.SimNet && cfg.CreateTemp { +fmt.Fprintln(os.Stderr, "Tried to create a temporary simulation "+ + "wallet for network other than simnet!") + os.Exit(0) + } + // Ensure the wallet exists or create it when the create flag is set. + netDir := NetworkDir(cfg.AppDataDir, ActiveNet.Params) + dbPath := filepath.Join(netDir, WalletDbName) + if cfg.CreateTemp && cfg.Create { +e := fmt.Errorf("The flags --create and --createtemp can not " + + "be specified together. Use --help for more information.") + fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + dbFileExists, e := cfgutil.FileExists(dbPath) + if e != nil { +log <- cl.Error{err} + return nil, nil, e + } + if cfg.CreateTemp { +tempWalletExists := false + if dbFileExists { +str := fmt.Sprintf("The wallet already exists. Loading this " + + "wallet instead.") + fmt.Fprintln(os.Out, str) + tempWalletExists = true + } + // Ensure the data directory for the network exists. + if e := checkCreateDir(netDir); E.Chk(e) { +fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + if !tempWalletExists { +// Perform the initial wallet creation wizard. + if e := createSimulationWallet(&cfg); E.Chk(e) { +fmt.Fprintln(os.Stderr, "Unable to create wallet:", e) + return nil, nil, e + } + } + } else if cfg.Create { +// Error if the create flag is set and the wallet already + // exists. + if dbFileExists { +e := fmt.Errorf("The wallet database file `%v` "+ + "already exists.", dbPath) + fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + // Ensure the data directory for the network exists. + if e := checkCreateDir(netDir); E.Chk(e) { +fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + // Perform the initial wallet creation wizard. + if e := createWallet(&cfg); E.Chk(e) { +fmt.Fprintln(os.Stderr, "Unable to create wallet:", e) + return nil, nil, e + } + // Created successfully, so exit now with success. + os.Exit(0) + } else if !dbFileExists && !cfg.NoInitialLoad { +keystorePath := filepath.Join(netDir, keystore.Filename) + keystoreExists, e := cfgutil.FileExists(keystorePath) + if e != nil { +fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + if !keystoreExists { +// e = fmt.Errorf("The wallet does not exist. Run with the " + + // "--create opt to initialize and create it...") + // Ensure the data directory for the network exists. + fmt.Println("Existing wallet not found in", cfg.ConfigFile.value) + if e := checkCreateDir(netDir); E.Chk(e) { +fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + // Perform the initial wallet creation wizard. + if e := createWallet(&cfg); E.Chk(e) { +fmt.Fprintln(os.Stderr, "Unable to create wallet:", e) + return nil, nil, e + } + // Created successfully, so exit now with success. + os.Exit(0) + } else { +e = fmt.Errorf("The wallet is in legacy format. Run with the " + + "--create opt to import it.") + } + fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + // localhostListeners := map[string]struct{}{ + // "localhost": {}, + // "127.0.0.1": {}, + // "::1": {}, + // } + // if cfg.UseSPV { +// sac.MaxPeers = cfg.MaxPeers + // sac.BanDuration = cfg.BanDuration + // sac.BanThreshold = cfg.BanThreshold + // } else { +if cfg.RPCConnect == "" { +cfg.RPCConnect = net.JoinHostPort("localhost", activeNet.RPCClientPort) + } + // Add default port to connect flag if missing. + cfg.RPCConnect, e = cfgutil.NormalizeAddress(cfg.RPCConnect, + activeNet.RPCClientPort) + if e != nil { +fmt.Fprintf(os.Stderr, + "Invalid rpcconnect network address: %v\n", e) + return nil, nil, e + } + // RPCHost, _, e = net.SplitHostPort(cfg.RPCConnect) + // if e != nil { +// return nil, nil, e + // } + if cfg.EnableClientTLS { +// if _, ok := localhostListeners[RPCHost]; !ok { +// str := "%s: the --noclienttls opt may not be used " + + // "when connecting RPC to non localhost " + + // "addresses: %s" + // e := fmt.Errorf(str, funcName, cfg.RPCConnect) + // fmt.Fprintln(os.Stderr, e) + // fmt.Fprintln(os.Stderr, usageMessage) + // return nil, nil, e + // } + // } else { +// If CAFile is unset, choose either the copy or local pod cert. + if !cfg.CAFile.ExplicitlySet() { +cfg.CAFile.value = filepath.Join(cfg.AppDataDir.value, DefaultCAFilename) + // If the CA copy does not exist, check if we're connecting to + // a local pod and switch to its RPC cert if it exists. + certExists, e := cfgutil.FileExists(cfg.CAFile.value) + if e != nil { +fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + if !certExists { +// if _, ok := localhostListeners[RPCHost]; ok { +podCertExists, e := cfgutil.FileExists( + DefaultCAFile) + if e != nil { +fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + if podCertExists { +cfg.CAFile.value = DefaultCAFile + } + // } + } + } + } + // } + // Only set default RPC listeners when there are no listeners set for + // the experimental RPC server. This is required to prevent the old RPC + // server from sharing listen addresses, since it is impossible to + // remove defaults from go-flags slice options without assigning + // specific behavior to a particular string. + if len(cfg.ExperimentalRPCListeners) == 0 && len(cfg.WalletRPCListeners) == 0 { +addrs, e := net.LookupHost("localhost") + if e != nil { +return nil, nil, e + } + cfg.WalletRPCListeners = make([]string, 0, len(addrs)) + for _, addr := range addrs { +addr = net.JoinHostPort(addr, activeNet.WalletRPCServerPort) + cfg.WalletRPCListeners = append(cfg.WalletRPCListeners, addr) + } + } + // Add default port to all rpc listener addresses if needed and remove + // duplicate addresses. + cfg.WalletRPCListeners, e = cfgutil.NormalizeAddresses( + cfg.WalletRPCListeners, activeNet.WalletRPCServerPort) + if e != nil { +fmt.Fprintf(os.Stderr, + "Invalid network address in legacy RPC listeners: %v\n", e) + return nil, nil, e + } + cfg.ExperimentalRPCListeners, e = cfgutil.NormalizeAddresses( + cfg.ExperimentalRPCListeners, activeNet.WalletRPCServerPort) + if e != nil { +fmt.Fprintf(os.Stderr, + "Invalid network address in RPC listeners: %v\n", e) + return nil, nil, e + } + // Both RPC servers may not listen on the same interface/port. + if len(cfg.WalletRPCListeners) > 0 && len(cfg.ExperimentalRPCListeners) > 0 { +seenAddresses := make(map[string]struct{}, len(cfg.WalletRPCListeners)) + for _, addr := range cfg.WalletRPCListeners { +seenAddresses[addr] = struct{}{} + } + for _, addr := range cfg.ExperimentalRPCListeners { +_, seen := seenAddresses[addr] + if seen { +e := fmt.Errorf("Address `%s` may not be "+ + "used as a listener address for both "+ + "RPC servers", addr) + fmt.Fprintln(os.Stderr, e) + return nil, nil, e + } + } + } + // Only allow server TLS to be disabled if the RPC server is bound to + // localhost addresses. + if !cfg.EnableServerTLS { +allListeners := append(cfg.WalletRPCListeners, + cfg.ExperimentalRPCListeners...) + for _, addr := range allListeners { +if e != nil { +str := "%s: RPC listen interface '%s' is " + + "invalid: %v" + e := fmt.Errorf(str, funcName, addr, e) + fmt.Fprintln(os.Stderr, e) + fmt.Fprintln(os.Stderr, usageMessage) + return nil, nil, e + } + // host, _, e = net.SplitHostPort(addr) + // if _, ok := localhostListeners[host]; !ok { +// str := "%s: the --noservertls opt may not be used " + + // "when binding RPC to non localhost " + + // "addresses: %s" + // e := fmt.Errorf(str, funcName, addr) + // fmt.Fprintln(os.Stderr, e) + // fmt.Fprintln(os.Stderr, usageMessage) + // return nil, nil, e + // } + } + } + // Expand environment variable and leading ~ for filepaths. + cfg.CAFile.value = cleanAndExpandPath(cfg.CAFile.value) + cfg.RPCCert.value = cleanAndExpandPath(cfg.RPCCert.value) + cfg.RPCKey.value = cleanAndExpandPath(cfg.RPCKey.value) + // If the pod username or password are unset, use the same auth as for + // the client. The two settings were previously shared for pod and + // client auth, so this avoids breaking backwards compatibility while + // allowing users to use different auth settings for pod and wallet. + if cfg.PodUsername == "" { +cfg.PodUsername = cfg.Username + } + if cfg.PodPassword == "" { +cfg.PodPassword = cfg.Password + } + // Warn about missing config file after the final command line parse + // succeeds. This prevents the warning on help messages and invalid + // options. + if configFileError != nil { +Log.Warnf.Print("%v", configFileError) + } + return cfg, nil, nil + } + // validLogLevel returns whether or not logLevel is a valid debug log level. + func validLogLevel( logLevel string) bool { +switch logLevel { +case "trace": + fallthrough + case "debug": + fallthrough + case "info": + fallthrough + case "warn": + fallthrough + case "error": + fallthrough + case "critical": + return true + } + return false + } +*/ diff --git a/cmd/wallet/_signal.go_ b/cmd/wallet/_signal.go_ new file mode 100755 index 0000000..064875d --- /dev/null +++ b/cmd/wallet/_signal.go_ @@ -0,0 +1,109 @@ +package wallet + +import ( + "os" + "os/signal" + + cl "github.com/p9c/p9/pkg/util/cl" +) + + +// interruptChannel is used to receive SIGINT (Ctrl+C) signals. +var interruptChannel chan os.Signal + + +// addHandlerChannel is used to add an interrupt handler to the list of handlers + +// to be invoked on SIGINT (Ctrl+C) signals. +var addHandlerChannel = make(chan func()) + + +// interruptHandlersDone is closed after all interrupt handlers run the first + +// time an interrupt is signaled. +var interruptHandlersDone = make(qu.C) + +var simulateInterruptChannel = make(qu.C, 1) + + +// signals defines the signals that are handled to do a clean shutdown. + +// Conditional compilation is used to also include SIGTERM on Unix. +var signals = []os.Signal{os.Interrupt} + + +// simulateInterrupt requests invoking the clean termination process by an + +// internal component instead of a SIGINT. +func simulateInterrupt( ) { + + select { + case simulateInterruptChannel <- struct{}{}: + default: + } +} + + +// mainInterruptHandler listens for SIGINT (Ctrl+C) signals on the + +// interruptChannel and invokes the registered interruptCallbacks accordingly. + +// It also listens for callback registration. It must be run as a goroutine. +func mainInterruptHandler( ) { + + + // interruptCallbacks is a list of callbacks to invoke when a + + // SIGINT (Ctrl+C) is received. + var interruptCallbacks []func() + invokeCallbacks := func() { + + + // run handlers in LIFO order. + for i := range interruptCallbacks { + idx := len(interruptCallbacks) - 1 - i + interruptCallbacks[idx]() + } + close(interruptHandlersDone) + } + + for { + select { + case sig := <-interruptChannel: + log <- cl.Infof{ + "received signal (%s) - shutting down...", sig, + } + _ = sig + invokeCallbacks() + return + case <-simulateInterruptChannel: + log <- cl.Inf( + "received shutdown request - shutting down...", + ) + invokeCallbacks() + return + + case handler := <-addHandlerChannel: + interruptCallbacks = append(interruptCallbacks, handler) + } + } +} + + +// addInterruptHandler adds a handler to call when a SIGINT (Ctrl+C) is + +// received. +func addInterruptHandler( handler func()) { + + + // Create the channel and start the main interrupt handler which invokes + + // all other callbacks and exits if not already done. + if interruptChannel == nil { + interruptChannel = make(chan os.Signal, 1) + signal.Notify(interruptChannel, signals...) + go mainInterruptHandler() + } + + addHandlerChannel <- handler +} diff --git a/cmd/wallet/_signalsigterm.go_ b/cmd/wallet/_signalsigterm.go_ new file mode 100755 index 0000000..a9a15c1 --- /dev/null +++ b/cmd/wallet/_signalsigterm.go_ @@ -0,0 +1,12 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd solaris +package wallet + +import ( + "os" + "syscall" +) + +func init( ) { + + signals = []os.Signal{os.Interrupt, syscall.SIGTERM} +} diff --git a/cmd/wallet/chainntfns.go b/cmd/wallet/chainntfns.go new file mode 100644 index 0000000..8c072de --- /dev/null +++ b/cmd/wallet/chainntfns.go @@ -0,0 +1,314 @@ +package wallet + +import ( + "bytes" + "github.com/p9c/p9/pkg/btcaddr" + "strings" + + "github.com/p9c/p9/pkg/chainclient" + "github.com/p9c/p9/pkg/txscript" + wm "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + tm "github.com/p9c/p9/pkg/wtxmgr" +) + +func (w *Wallet) handleChainNotifications() { + defer w.wg.Done() + if w == nil { + panic("w should not be nil") + } + chainClient, e := w.requireChainClient() + if e != nil { + E.Ln("handleChainNotifications called without RPC client", e) + return + } + sync := func(w *Wallet) { + if w.db != nil { + // At the moment there is no recourse if the rescan fails for some reason, however, the wallet will not be + // marked synced and many methods will error early since the wallet is known to be out of date. + e := w.syncWithChain() + if e != nil && !w.ShuttingDown() { + W.Ln("unable to synchronize wallet to chain:", e) + } + } + } + catchUpHashes := func( + w *Wallet, client chainclient.Interface, + height int32, + ) (e error) { + // TODO(aakselrod): There's a race condition here, which happens when a reorg occurs between the rescanProgress + // notification and the last GetBlockHash call. The solution when using pod is to make pod send blockconnected + // notifications with each block the way Neutrino does, and get rid of the loop. The other alternative is to + // check the final hash and, if it doesn't match the original hash returned by the notification, to roll back + // and restart the rescan. + I.F( + "handleChainNotifications: catching up block hashes to height %d, this might take a while", height, + ) + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + startBlock := w.Manager.SyncedTo() + for i := startBlock.Height + 1; i <= height; i++ { + hash, e := client.GetBlockHash(int64(i)) + if e != nil { + return e + } + header, e := chainClient.GetBlockHeader(hash) + if e != nil { + return e + } + bs := wm.BlockStamp{ + Height: i, + Hash: *hash, + Timestamp: header.Timestamp, + } + e = w.Manager.SetSyncedTo(ns, &bs) + if e != nil { + return e + } + } + return nil + }, + ) + if e != nil { + E.F( + "failed to update address manager sync state for height %d: %v", + height, e, + ) + } + I.Ln("done catching up block hashes") + return e + } + for { + select { + case n, ok := <-chainClient.Notifications(): + if !ok { + return + } + var notificationName string + var e error + switch n := n.(type) { + case chainclient.ClientConnected: + if w != nil { + go sync(w) + } + case chainclient.BlockConnected: + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + return w.connectBlock(tx, tm.BlockMeta(n)) + }, + ) + notificationName = "blockconnected" + case chainclient.BlockDisconnected: + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + return w.disconnectBlock(tx, tm.BlockMeta(n)) + }, + ) + notificationName = "blockdisconnected" + case chainclient.RelevantTx: + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + return w.addRelevantTx(tx, n.TxRecord, n.Block) + }, + ) + notificationName = "recvtx/redeemingtx" + case chainclient.FilteredBlockConnected: + // Atomically update for the whole block. + if len(n.RelevantTxs) > 0 { + e = walletdb.Update( + w.db, func( + tx walletdb.ReadWriteTx, + ) (e error) { + for _, rec := range n.RelevantTxs { + e = w.addRelevantTx( + tx, rec, + n.Block, + ) + if e != nil { + return e + } + } + return nil + }, + ) + } + notificationName = "filteredblockconnected" + // The following require some database maintenance, but also need to be reported to the wallet's rescan + // goroutine. + case *chainclient.RescanProgress: + e = catchUpHashes(w, chainClient, n.Height) + notificationName = "rescanprogress" + select { + case w.rescanNotifications <- n: + case <-w.quitChan().Wait(): + return + } + case *chainclient.RescanFinished: + e = catchUpHashes(w, chainClient, n.Height) + notificationName = "rescanprogress" + w.SetChainSynced(true) + select { + case w.rescanNotifications <- n: + case <-w.quitChan().Wait(): + return + } + } + if e != nil { + // On out-of-sync blockconnected notifications, only send a debug message. + errStr := "failed to process consensus server " + + "notification (name: `%s`, detail: `%v`)" + if notificationName == "blockconnected" && + strings.Contains( + e.Error(), + "couldn't get hash from database", + ) { + D.F(errStr, notificationName, e) + } else { + E.F(errStr, notificationName, e) + } + } + case <-w.quit.Wait(): + return + } + } +} + +// connectBlock handles a chain server notification by marking a wallet that's currently in-sync with the chain server +// as being synced up to the passed block. +func (w *Wallet) connectBlock(dbtx walletdb.ReadWriteTx, b tm.BlockMeta) (e error) { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + bs := wm.BlockStamp{ + Height: b.Height, + Hash: b.Hash, + Timestamp: b.Time, + } + e = w.Manager.SetSyncedTo(addrmgrNs, &bs) + if e != nil { + return e + } + // Notify interested clients of the connected block. + // + // TODO: move all notifications outside of the database transaction. + w.NtfnServer.notifyAttachedBlock(dbtx, &b) + return nil +} + +// disconnectBlock handles a chain server reorganize by rolling back all block history from the reorged block for a +// wallet in-sync with the chain server. +func (w *Wallet) disconnectBlock(dbtx walletdb.ReadWriteTx, b tm.BlockMeta) (e error) { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + if !w.ChainSynced() { + return nil + } + // Disconnect the removed block and all blocks after it if we know about the disconnected block. Otherwise, the + // block is in the future. + if b.Height <= w.Manager.SyncedTo().Height { + hash, e := w.Manager.BlockHash(addrmgrNs, b.Height) + if e != nil { + return e + } + if bytes.Equal(hash[:], b.Hash[:]) { + bs := wm.BlockStamp{ + Height: b.Height - 1, + } + hash, e = w.Manager.BlockHash(addrmgrNs, bs.Height) + if e != nil { + return e + } + b.Hash = *hash + client := w.ChainClient() + header, e := client.GetBlockHeader(hash) + if e != nil { + return e + } + bs.Timestamp = header.Timestamp + e = w.Manager.SetSyncedTo(addrmgrNs, &bs) + if e != nil { + return e + } + e = w.TxStore.Rollback(txmgrNs, b.Height) + if e != nil { + return e + } + } + } + // Notify interested clients of the disconnected block. + w.NtfnServer.notifyDetachedBlock(&b.Hash) + return nil +} +func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *tm.TxRecord, block *tm.BlockMeta) (e error) { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + // At the moment all notified transactions are assumed to actually be relevant. This assumption will not hold true + // when SPV support is added, but until then, simply insert the transaction because there should either be one or + // more relevant inputs or outputs. + e = w.TxStore.InsertTx(txmgrNs, rec, block) + if e != nil { + return e + } + // Chk every output to determine whether it is controlled by a wallet key. If so, mark the output as a credit. + for i, output := range rec.MsgTx.TxOut { + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, + w.chainParams, + ) + if e != nil { + // Non-standard outputs are skipped. + continue + } + for _, addr := range addrs { + ma, e := w.Manager.Address(addrmgrNs, addr) + if e == nil { + // TODO: Credits should be added with the account they belong to, so tm is able to track per-account + // balances. + e = w.TxStore.AddCredit( + txmgrNs, rec, block, uint32(i), + ma.Internal(), + ) + if e != nil { + return e + } + e = w.Manager.MarkUsed(addrmgrNs, addr) + if e != nil { + return e + } + T.Ln("marked address used:", addr) + continue + } + // Missing addresses are skipped. Other errors should be propagated. + if !wm.IsError(e, wm.ErrAddressNotFound) { + return e + } + } + } + // Send notification of mined or unmined transaction to any interested clients. + // + // TODO: Avoid the extra db hits. + if block == nil { + details, e := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, nil) + if e != nil { + E.Ln("cannot query transaction details for notification:", e) + } + // It's possible that the transaction was not found within the wallet's set of unconfirmed transactions due to + // it already being confirmed, so we'll avoid notifying it. + // + // TODO(wilmer): ideally we should find the culprit to why we're receiving an additional unconfirmed + // chain.RelevantTx notification from the chain backend. + if details != nil { + w.NtfnServer.notifyUnminedTransaction(dbtx, details) + } + } else { + details, e := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, &block.Block) + if e != nil { + E.Ln("cannot query transaction details for notification:", e) + } + // We'll only notify the transaction if it was found within the wallet's set of confirmed transactions. + if details != nil { + w.NtfnServer.notifyMinedTransaction(dbtx, details, block) + } + } + return nil +} diff --git a/cmd/wallet/common.go b/cmd/wallet/common.go new file mode 100644 index 0000000..bc60f49 --- /dev/null +++ b/cmd/wallet/common.go @@ -0,0 +1,75 @@ +package wallet + +import ( + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// Note: The following common types should never reference the Wallet type. Long term goal is to move these to their own +// package so that the database access APIs can create them directly for the wallet to return. + +// BlockIdentity identifies a block, or the lack of one (used to describe an unmined transaction). +type BlockIdentity struct { + Hash chainhash.Hash + Height int32 +} + +// None returns whether there is no block described by the instance. When associated with a transaction, this indicates +// the transaction is unmined. +func (b *BlockIdentity) None() bool { + // BUG: Because dcrwallet uses both 0 and -1 in various places to refer to an unmined transaction this must check + // against both and may not ever be usable to represent the genesis block. + return *b == BlockIdentity{Height: -1} || *b == BlockIdentity{} +} + +// OutputKind describes a kind of transaction output. This is used to differentiate between coinbase, stakebase, and +// normal outputs. +type OutputKind byte + +// Defined OutputKind constants +const ( + OutputKindNormal OutputKind = iota + OutputKindCoinbase +) + +// TransactionOutput describes an output that was or is at least partially controlled by the wallet. Depending on +// context, this could refer to an unspent output, or a spent one. +type TransactionOutput struct { + OutPoint wire.OutPoint + Output wire.TxOut + OutputKind OutputKind + // These should be added later when the DB can return them more efficiently: + // + // TxLockTime uint32 + // TxExpiry uint32 + ContainingBlock BlockIdentity + ReceiveTime time.Time +} + +// OutputRedeemer identifies the transaction input which redeems an output. +type OutputRedeemer struct { + TxHash chainhash.Hash + InputIndex uint32 +} + +// P2SHMultiSigOutput describes a transaction output with a pay-to-script-hash output script and an imported redemption +// script. Along with common details of the output, this structure also includes the P2SH address the script was created +// from and the number of signatures required to redeem it. +// +// TODO: Could be useful to return how many of the required signatures can be created by this wallet. +type P2SHMultiSigOutput struct { + // TODO: Add a TransactionOutput member to this struct and remove these fields which are duplicated by it. This + // improves consistency. Only not done now because wtxmgr APIs don't support an efficient way of fetching other + // Transactionoutput data together with the rest of the multisig info. + OutPoint wire.OutPoint + OutputAmount amt.Amount + ContainingBlock BlockIdentity + P2SHAddress *btcaddr.ScriptHash + RedeemScript []byte + M, N uint8 // M of N signatures required to redeem + Redeemer *OutputRedeemer // nil unless spent +} diff --git a/cmd/wallet/config.go b/cmd/wallet/config.go new file mode 100644 index 0000000..3937473 --- /dev/null +++ b/cmd/wallet/config.go @@ -0,0 +1,9 @@ +package wallet + +// Options contains the required options for running the legacy RPC server. +type Options struct { + Username string + Password string + MaxPOSTClients int64 + MaxWebsocketClients int64 +} diff --git a/cmd/wallet/createtx.go b/cmd/wallet/createtx.go new file mode 100644 index 0000000..7715aa6 --- /dev/null +++ b/cmd/wallet/createtx.go @@ -0,0 +1,242 @@ +// Package wallet Copyright (c) 2015-2016 The btcsuite developers +package wallet + +import ( + "fmt" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chainclient" + "sort" + + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/txauthor" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wire" + "github.com/p9c/p9/pkg/wtxmgr" +) + +// byAmount defines the methods needed to satisify sort.Interface to txsort credits by their output amount. +type byAmount []wtxmgr.Credit + +func (s byAmount) Len() int { return len(s) } +func (s byAmount) Less(i, j int) bool { return s[i].Amount < s[j].Amount } +func (s byAmount) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { + // Pick largest outputs first. This is only done for compatibility with previous + // tx creation code, not because it's a good idea. + sort.Sort(sort.Reverse(byAmount(eligible))) + // Current inputs and their total value. These are closed over by the returned + // input source and reused across multiple calls. + currentTotal := amt.Amount(0) + currentInputs := make([]*wire.TxIn, 0, len(eligible)) + currentScripts := make([][]byte, 0, len(eligible)) + currentInputValues := make([]amt.Amount, 0, len(eligible)) + return func(target amt.Amount) ( + amt.Amount, []*wire.TxIn, + []amt.Amount, [][]byte, error, + ) { + for currentTotal < target && len(eligible) != 0 { + nextCredit := &eligible[0] + eligible = eligible[1:] + nextInput := wire.NewTxIn(&nextCredit.OutPoint, nil, nil) + currentTotal += nextCredit.Amount + currentInputs = append(currentInputs, nextInput) + currentScripts = append(currentScripts, nextCredit.PkScript) + currentInputValues = append(currentInputValues, nextCredit.Amount) + } + return currentTotal, currentInputs, currentInputValues, currentScripts, nil + } +} + +// secretSource is an implementation of txauthor.SecretSource for the wallet's address manager. +type secretSource struct { + *waddrmgr.Manager + addrmgrNs walletdb.ReadBucket +} + +// GetKey gets the private key for an address if it is available +func (s secretSource) GetKey(addr btcaddr.Address) (privKey *ec.PrivateKey, cmpr bool, e error) { + var ma waddrmgr.ManagedAddress + ma, e = s.Address(s.addrmgrNs, addr) + if e != nil { + return nil, false, e + } + var ok bool + var mpka waddrmgr.ManagedPubKeyAddress + mpka, ok = ma.(waddrmgr.ManagedPubKeyAddress) + if !ok { + e = fmt.Errorf( + "managed address type for %v is `%T` but "+ + "want waddrmgr.ManagedPubKeyAddress", addr, ma, + ) + return nil, false, e + } + if privKey, e = mpka.PrivKey(); E.Chk(e) { + return nil, false, e + } + return privKey, ma.Compressed(), nil +} + +// GetScript returns pay to script transaction +func (s secretSource) GetScript(addr btcaddr.Address) ([]byte, error) { + ma, e := s.Address(s.addrmgrNs, addr) + if e != nil { + return nil, e + } + msa, ok := ma.(waddrmgr.ManagedScriptAddress) + if !ok { + e := fmt.Errorf( + "managed address type for %v is `%T` but "+ + "want waddrmgr.ManagedScriptAddress", addr, ma, + ) + return nil, e + } + return msa.Script() +} + +// txToOutputs creates a signed transaction which includes each output from +// outputs. Previous outputs to reedeem are chosen from the passed account's +// UTXO set and minconf policy. An additional output may be added to return +// change to the wallet. An appropriate fee is included based on the wallet's +// current relay fee. The wallet must be unlocked to create the transaction. +func (w *Wallet) txToOutputs( + outputs []*wire.TxOut, account uint32, + minconf int32, feeSatPerKb amt.Amount, +) (tx *txauthor.AuthoredTx, e error) { + var chainClient chainclient.Interface + if chainClient, e = w.requireChainClient(); E.Chk(e) { + return nil, e + } + e = walletdb.Update( + w.db, func(dbtx walletdb.ReadWriteTx) (e error) { + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + // Get current block's height and hash. + var bs *waddrmgr.BlockStamp + if bs, e = chainClient.BlockStamp(); E.Chk(e) { + return + } + var eligible []wtxmgr.Credit + if eligible, e = w.findEligibleOutputs(dbtx, account, minconf, bs); E.Chk(e) { + return + } + inputSource := makeInputSource(eligible) + changeSource := func() (b []byte, e error) { + // Derive the change output script. As a hack to allow spending from the + // imported account, change addresses are created from account 0. + var changeAddr btcaddr.Address + if account == waddrmgr.ImportedAddrAccount { + changeAddr, e = w.newChangeAddress(addrmgrNs, 0) + } else { + changeAddr, e = w.newChangeAddress(addrmgrNs, account) + } + if E.Chk(e) { + return + } + return txscript.PayToAddrScript(changeAddr) + } + if tx, e = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, inputSource, changeSource); E.Chk(e) { + return + } + // Randomize change position, if change exists, before signing. This doesn't + // affect the serialize size, so the change amount will still be valid. + if tx.ChangeIndex >= 0 { + tx.RandomizeChangePosition() + } + return tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) + }, + ) + if E.Chk(e) { + return + } + if e = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues); E.Chk(e) { + return + } + if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount { + changeAmount := amt.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value) + W.F( + "spend from imported account produced change: moving %v from imported account into default account.", + changeAmount, + ) + } + return +} +func (w *Wallet) findEligibleOutputs( + dbtx walletdb.ReadTx, + account uint32, + minconf int32, + bs *waddrmgr.BlockStamp, +) ([]wtxmgr.Credit, error) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + unspent, e := w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return nil, e + } + // TODO: Eventually all of these filters (except perhaps output locking) should + // be handled by the call to UnspentOutputs (or similar). Because one of these + // filters requires matching the output script to the desired + // account, this change depends on making wtxmgr a waddrmgr dependancy and + // requesting unspent outputs for a single account. + eligible := make([]wtxmgr.Credit, 0, len(unspent)) + for i := range unspent { + output := &unspent[i] + // Only include this output if it meets the required number of confirmations. + // Coinbase transactions must have have reached maturity before their outputs + // may be spent. + if !confirmed(minconf, output.Height, bs.Height) { + continue + } + if output.FromCoinBase { + target := int32(w.chainParams.CoinbaseMaturity) + if !confirmed(target, output.Height, bs.Height) { + continue + } + } + // Locked unspent outputs are skipped. + if w.LockedOutpoint(output.OutPoint) { + continue + } + // Only include the output if it is associated with the passed account. + // + // TODO: Handle multisig outputs by determining if enough of the addresses are + // controlled. + var addrs []btcaddr.Address + if _, addrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams, + ); E.Chk(e) || len(addrs) != 1 { + continue + } + var addrAcct uint32 + if _, addrAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]); E.Chk(e) || + addrAcct != account { + continue + } + eligible = append(eligible, *output) + } + return eligible, nil +} + +// validateMsgTx verifies transaction input scripts for tx. All previous output +// scripts from outputs redeemed by the transaction, in the same order they are +// spent, must be passed in the prevScripts slice. +func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte, inputValues []amt.Amount) (e error) { + hashCache := txscript.NewTxSigHashes(tx) + for i, prevScript := range prevScripts { + var vm *txscript.Engine + vm, e = txscript.NewEngine( + prevScript, tx, i, + txscript.StandardVerifyFlags, nil, hashCache, int64(inputValues[i]), + ) + if E.Chk(e) { + return fmt.Errorf("cannot create script engine: %s", e) + } + e = vm.Execute() + if E.Chk(e) { + return fmt.Errorf("cannot validate transaction: %s", e) + } + } + return nil +} diff --git a/cmd/wallet/disksync.go b/cmd/wallet/disksync.go new file mode 100644 index 0000000..e89e383 --- /dev/null +++ b/cmd/wallet/disksync.go @@ -0,0 +1,26 @@ +package wallet + +import ( + "fmt" + "os" +) + +// checkCreateDir checks that the path exists and is a directory. If path does not exist, it is created. +func checkCreateDir(path string) (e error) { + var fi os.FileInfo + if fi, e = os.Stat(path); E.Chk(e) { + if os.IsNotExist(e) { + // Attempt data directory creation + if e = os.MkdirAll(path, 0700); E.Chk(e) { + return fmt.Errorf("cannot create directory: %s", e) + } + } else { + return fmt.Errorf("error checking directory: %s", e) + } + } else { + if !fi.IsDir() { + return fmt.Errorf("path '%s' is not a directory", path) + } + } + return nil +} diff --git a/cmd/wallet/doc.go b/cmd/wallet/doc.go new file mode 100644 index 0000000..01c7624 --- /dev/null +++ b/cmd/wallet/doc.go @@ -0,0 +1,7 @@ +/*Package wallet provides ... +TODO: Flesh out this section + +Overview + +*/ +package wallet diff --git a/cmd/wallet/doc/README.md b/cmd/wallet/doc/README.md new file mode 100644 index 0000000..96d1598 --- /dev/null +++ b/cmd/wallet/doc/README.md @@ -0,0 +1,16 @@ +# RPC Documentation + +This project provides a [gRPC](http://www.grpc.io/) server for Remote Procedure +Call (RPC) access from other processes. This is intended to be the primary means +by which users, through other client programs, interact with the wallet. + +These documents cover the documentation for both consumers of the server and +developers who must make changes or additions to the API and server +implementation: + +- [API specification](api.md) +- [Client usage](clientusage.md) +- [Making API changes](serverchanges.md) + +A legacy RPC server based on the JSON-RPC API of Bitcoin Core's wallet is also +available, but documenting its usage is out of scope for these documents. diff --git a/cmd/wallet/doc/api.md b/cmd/wallet/doc/api.md new file mode 100644 index 0000000..bfe170b --- /dev/null +++ b/cmd/wallet/doc/api.md @@ -0,0 +1,987 @@ +# RPC API Specification + +Version: 2.0.1 +======= + +**Note:** This document assumes the reader is familiar with gRPC concepts. Refer +to +the [gRPC Concepts documentation](http://www.grpc.io/docs/guides/concepts.html) +for any unfamiliar terms. + +**Note:** The naming style used for autogenerated identifiers may differ +depending on the language being used. This document follows the naming style +used by Google in their Protocol Buffers and gRPC documentation as well as this +project's `.proto` files. That is, CamelCase is used for services, methods, and +messages, lower_snake_case for message fields, and SCREAMING_SNAKE_CASE for +enums. + +**Note:** The entierty of the RPC API is currently considered unstable and may +change anytime. Stability will be gradually added based on correctness, +perceived usefulness and ease-of-use over alternatives, and user feedback. + +This document is the authoritative source on the RPC API's definitions and +semantics. Any divergence from this document is an implementation error. API +fixes and additions require a version increase according to the rules of +[Semantic Versioning 2.0.0](http://semver.org/). + +Only optional proto3 message fields are used (the `required` keyword is never +used in the `.proto` file). If a message field must be set to something other +than the default value, or any other values are invalid, the error must occur in +the application's message handling. This prevents accidentally introducing +parsing errors if a previously optional field is missing or a new required field +is added. + +Functionality is grouped into gRPC services. Depending on what functions are +currently callable, different services will be running. As an example, the +server may be running without a loaded wallet, in which case the Wallet service +is not running and the Loader service must be used to create a new or load an +existing wallet. + +- [`VersionService`](#versionservice) +- [`LoaderService`](#loaderservice) +- [`WalletService`](#walletservice) + +## `VersionService` + +The `VersionService` service provides the caller with versioning information +regarding the RPC server. It has no dependencies and is always running. + +**Methods:** + +- [`Version`](#version) + +### Methods + +#### `Version` + +The `Version` method returns the RPC server version. Versioning follows the +rules of Semantic Versioning (SemVer) 2.0.0. + +**Request:** `VersionRequest` + +**Response:** `VersionResponse` + +- `string version_string`: The version encoded as a string. + +- `uint32 major`: The SemVer major version number. + +- `uint32 minor`: The SemVer minor version number. + +- `uint32 patch`: The SemVer patch version number. + +- `string prerelease`: The SemVer pre-release version identifier, if any. + +- `string build_metadata`: Extra SemVer build metadata, if any. + +**Expected errors:** None + +**Stability:** Stable + +## `LoaderService` + +The `LoaderService` service provides the caller with functions related to the +management of the wallet and its connection to the Bitcoin network. It has no +dependencies and is always running. + +**Methods:** + +- [`WalletExists`](#walletexists) +- [`CreateWallet`](#createwallet) +- [`OpenWallet`](#openwallet) +- [`CloseWallet`](#closewallet) +- [`StartConsensusRPC`](#StartConsensusRPC) + +**Shared messages:** + +- [`BlockDetails`](#blockdetails) +- [`TransactionDetails`](#transactiondetails) + +### Methods + +#### `WalletExists` + +The `WalletExists` method returns whether a file at the wallet database's file +path exists. Clients that must load wallets with this service are expected to +call this RPC to query whether `OpenWallet` should be used to open an existing +wallet, or `CreateWallet` to create a new wallet. + +**Request:** `WalletExistsRequest` + +**Response:** `WalletExistsResponse` + +- `bool exists`: Whether the wallet file exists. + +**Expected errors:** None + +**Stability:** Unstable + +___ + +#### `CreateWallet` + +The `CreateWallet` method is used to create a wallet that is protected by two +levels of encryption: the public passphrase (for data that is made public on the +blockchain) and the private passphrase (for private keys). Since the seed is not +saved in the wallet database and clients should make their users backup the +seed, it needs to be passed as part of the request. + +After creating a wallet, the `WalletService` service begins running. + +**Request:** `CreateWalletRequest` + +- `bytes public_passphrase`: The passphrase used for the outer wallet + encryption. This passphrase protects data that is made public on the + blockchain. If this passphrase has zero length, an insecure default is used + instead. + +- `bytes private_passphrase`: The passphrase used for the inner wallet + encryption. This is the passphrase used for data that must always remain + private, such as private keys. The length of this field must not be zero. + +- `bytes seed`: The BIP0032 seed used to derive all wallet keys. The length of + this field must be between 16 and 64 bytes, inclusive. + +**Response:** `CreateWalletReponse` + +**Expected errors:** + +- `FailedPrecondition`: The wallet is currently open. + +- `AlreadyExists`: A file already exists at the wallet database file path. + +- `InvalidArgument`: A private passphrase was not included in the request, or + the seed is of incorrect length. + +**Stability:** Unstable: There needs to be a way to recover all keys and +transactions of a wallet being recovered by its seed. It is unclear whether it +should be part of this method or a `WalletService` method. + +___ + +#### `OpenWallet` + +The `OpenWallet` method is used to open an existing wallet database. If the +wallet is protected by a public passphrase, it can not be successfully opened if +the public passphrase parameter is missing or incorrect. + +After opening a wallet, the `WalletService` service begins running. + +**Request:** `OpenWalletRequest` + +- `bytes public_passphrase`: The passphrase used for the outer wallet + encryption. This passphrase protects data that is made public on the + blockchain. If this passphrase has zero length, an insecure default is used + instead. + +**Response:** `OpenWalletResponse` + +**Expected errors:** + +- `FailedPrecondition`: The wallet is currently open. + +- `NotFound`: The wallet database file does not exist. + +- `InvalidArgument`: The public encryption passphrase was missing or incorrect. + +**Stability:** Unstable + +___ + +#### `CloseWallet` + +The `CloseWallet` method is used to cleanly stop all wallet operations on a +loaded wallet and close the database. After closing, the `WalletService` +service will remain running but any operations that require the database will be +unusable. + +**Request:** `CloseWalletRequest` + +**Response:** `CloseWalletResponse` + +**Expected errors:** + +- `FailedPrecondition`: The wallet is not currently open. + +**Stability:** Unstable: It would be preferable to stop the `WalletService` +after closing, but there does not appear to be any way to do so currently. It +may also be a good idea to limit under what conditions a wallet can be closed, +such as only closing wallets loaded by `LoaderService` and/or using a secret to +authenticate the operation. + +___ + +#### `StartConsensusRPC` + +The `StartConsensusRPC` method is used to provide clients the ability to +dynamically start the pod RPC client. This RPC client is used for wallet syncing +and publishing transactions to the Bitcoin network. + +**Request:** `StartConsensusRPCRequest` + +- `string network_address`: The host/IP and optional port of the RPC server to + connect to. IP addresses may be IPv4 or IPv6. If the port is missing, a + default port is chosen corresponding to the default pod RPC port of the active + Bitcoin network. + +- `string username`: The RPC username required to authenticate to the RPC + server. + +- `bytes password`: The RPC password required to authenticate to the RPC server. + +- `bytes certificate`: The consensus RPC server's TLS certificate. If this field + has zero length and the network address describes a loopback connection + (`localhost`, `127.0.0.1`, or `::1`) TLS will be disabled. + +**Response:** `StartConsensusRPCResponse` + +**Expected errors:** + +- `FailedPrecondition`: A consensus RPC client is already active. + +- `InvalidArgument`: The network address is ill-formatted or does not contain a + valid IP address. + +- `NotFound`: The consensus RPC server is unreachable. This condition may not + return `Unavailable` as that refers to `LoaderService` itself being + unavailable. + +- `InvalidArgument`: The username, password, or certificate are invalid. This + condition may not be return `Unauthenticated` as that refers to the client not + having the credentials to call this method. + +**Stability:** Unstable: It is unknown if the consensus RPC client will remain +used after the project gains SPV support. + +## `WalletService` + +The WalletService service provides RPCs for the wallet itself. The service +depends on a loaded wallet and does not run when the wallet has not been created +or opened yet. + +The service provides the following methods: + +- [`Ping`](#ping) +- [`Network`](#network) +- [`AccountNumber`](#accountnumber) +- [`Accounts`](#accounts) +- [`Balance`](#balance) +- [`GetTransactions`](#gettransactions) +- [`ChangePassphrase`](#changepassphrase) +- [`RenameAccount`](#renameaccount) +- [`NextAccount`](#nextaccount) +- [`NextAddress`](#nextaddress) +- [`ImportPrivateKey`](#importprivatekey) +- [`FundTransaction`](#fundtransaction) +- [`SignTransaction`](#signtransaction) +- [`PublishTransaction`](#publishtransaction) +- [`TransactionNotifications`](#transactionnotifications) +- [`SpentnessNotifications`](#spentnessnotifications) +- [`AccountNotifications`](#accountnotifications) + +#### `Ping` + +The `Ping` method checks whether the service is active. + +**Request:** `PingRequest` + +**Response:** `PingResponse` + +**Expected errors:** None + +**Stability:** Unstable: This may be moved to another service as it does not +depend on the wallet. + +___ + +#### `Network` + +The `Network` method returns the network identifier constant describing the +server's active network. + +**Request:** `NetworkRequest` + +**Response:** `NetworkResponse` + +- `uint32 active_network`: The network identifier. + +**Expected errors:** None + +**Stability:** Unstable: This may be moved to another service as it does not +depend on the wallet. + +___ + +#### `AccountNumber` + +The `AccountNumber` method looks up a BIP0044 account number by an account's +unique name. + +**Request:** `AccountNumberRequest` + +- `string account_name`: The name of the account being queried. + +**Response:** `AccountNumberResponse` + +- `uint32 account_number`: The BIP0044 account number. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `NotFound`: No accounts exist by the name in the request. + +**Stability:** Unstable + +___ + +#### `Accounts` + +The `Accounts` method returns the current properties of all accounts managed in +the wallet. + +**Request:** `AccountsRequest` + +**Response:** `AccountsResponse` + +- `repeated Account accounts`: Account properties grouped into `Account` nested + message types, one per account, ordered by increasing account numbers. + + **Nested message:** `Account` + + - `uint32 account_number`: The BIP0044 account number. + + - `string account_name`: The name of the account. + + - `int64 total_balance`: The total (zero-conf and immature) balance, counted + in Satoshis. + + - `uint32 external_key_count`: The number of derived keys in the external + key chain. + + - `uint32 internal_key_count`: The number of derived keys in the internal + key chain. + + - `uint32 imported_key_count`: The number of imported keys. + +- `bytes current_block_hash`: The hash of the block wallet is considered to be + synced with. + +- `int32 current_block_height`: The height of the block wallet is considered to + be synced with. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `Balance` + +The `Balance` method queries the wallet for an account's balance. Balances are +returned as combination of total, spendable (by consensus and request policy), +and unspendable immature coinbase balances. + +**Request:** `BalanceRequest` + +- `uint32 account_number`: The account number to query. + +- `int32 required_confirmations`: The number of confirmations required before an + unspent transaction output's value is included in the spendable balance. This + may not be negative. + +**Response:** `BalanceResponse` + +- `int64 total`: The total (zero-conf and immature) balance, counted in + Satoshis. + +- `int64 spendable`: The spendable balance, given some number of required + confirmations, counted in Satoshis. This equals the total balance when the + required number of confirmations is zero and there are no immature coinbase + outputs. + +- `int64 immature_reward`: The total value of all immature coinbase outputs, + counted in Satoshis. + +**Expected errors:** + +- `InvalidArgument`: The required number of confirmations is negative. + +- `Aborted`: The wallet database is closed. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable: It may prove useful to modify this RPC to query +multiple accounts together. + +___ + +#### `GetTransactions` + +The `GetTransactions` method queries the wallet for relevant transactions. The +query set may be specified using a block range, inclusive, with the heights or +hashes of the minimum and maximum block. Transaction results are grouped grouped +by the block they are mined in, or grouped together with other unmined +transactions. + +**Request:** `GetTransactionsRequest` + +- `bytes starting_block_hash`: The block hash of the block to begin including + transactions from. If this field is set to the default, the + `starting_block_height` field is used instead. If changed, the byte array must + have length 32 and `starting_block_height` must be zero. + +- `sint32 starting_block_height`: The block height to begin including + transactions from. If this field is non-zero, `starting_block_hash` must be + set to its default value to avoid ambiguity. If positive, the field is + interpreted as a block height. If negative, the height is subtracted from the + block wallet considers itself in sync with. + +- `bytes ending_block_hash`: The block hash of the last block to include + transactions from. If this default is set to the default, the + `ending_block_height` field is used instead. If changed, the byte array must + have length 32 and `ending_block_height` must be zero. + +- `int32 ending_block_height`: The block height of the last block to include + transactions from. If non-zero, the `ending_block_hash` field must be set to + its default value to avoid ambiguity. If both this field and + `ending_block_hash` are set to their default values, no upper block limit is + used and transactions through the best block and all unmined transactions are + included. + +**Response:** `GetTransactionsResponse` + +- `repeated BlockDetails mined_transactions`: All mined transactions, organized + by blocks in the order they appear in the blockchain. + + The `BlockDetails` message is used by other methods and is documented + [here](#blockdetails). + +- `repeated TransactionDetails unmined_transactions`: All unmined transactions. + The ordering is unspecified. + + The `TransactionDetails` message is used by other methods and is documented + [here](#transactiondetails). + +**Expected errors:** + +- `InvalidArgument`: A non-default block hash field did not have the correct + length. + +- `Aborted`: The wallet database is closed. + +- `NotFound`: A block, specified by its height or hash, is unknown to the + wallet. + +**Stability:** Unstable + +- There is currently no way to get only unmined transactions due to the way the + block range is specified. + +- It would be useful to ignore the block range and return some minimum number of + the most recent transaction, but it is unclear if that should be added to this + method's request object, or to make a new method. + +- A specified ordering (such as dependency order) for all returned unmined + transactions would be useful. + +___ + +#### `ChangePassphrase` + +The `ChangePassphrase` method requests a change to either the public (outer) or +private (inner) encryption passphrases. + +**Request:** `ChangePassphraseRequest` + +- `Key key`: The key being changed. + + **Nested enum:** `Key` + + - `PRIVATE`: The request specifies to change the private (inner) encryption + passphrase. + + - `PUBLIC`: The request specifies to change the public (outer) encryption + passphrase. + +- `bytes old_passphrase`: The current passphrase for the encryption key. This is + the value being modified. If the public passphrase is being modified and this + value is the default value, an insecure default is used instead. + +- `bytes new_passphrase`: The replacement passphrase. This field may only have + zero length if the public passphrase is being changed, in which case an + insecure default will be used instead. + +**Response:** `ChangePassphraseResponse` + +**Expected errors:** + +- `InvalidArgument`: A zero length passphrase was specified when changing the + private passphrase, or the old passphrase was incorrect. + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `RenameAccount` + +The `RenameAccount` method requests a change to an account's name property. + +**Request:** `RenameAccountRequest` + +- `uint32 account_number`: The number of the account being modified. + +- `string new_name`: The new name for the account. + +**Response:** `RenameAccountResponse` + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The new account name is a reserved name. + +- `NotFound`: The account does not exist. + +- `AlreadyExists`: An account by the same name already exists. + +**Stability:** Unstable: There should be a way to specify a starting block or +time to begin the rescan at. Additionally, since the client is expected to be +able to do asynchronous RPC, it may be useful for the response to block on the +rescan finishing before returning. + +___ + +#### `NextAccount` + +The `NextAccount` method generates the next BIP0044 account for the wallet. + +**Request:** `NextAccountRequest` + +- `bytes passphrase`: The private passphrase required to derive the next + account's key. + +- `string account_name`: The name to give the new account. + +**Response:** `NextAccountResponse` + +- `uint32 account_number`: The number of the newly-created account. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The private passphrase is incorrect. + +- `InvalidArgument`: The new account name is a reserved name. + +- `AlreadyExists`: An account by the same name already exists. + +**Stability:** Unstable + +___ + +#### `NextAddress` + +The `NextAddress` method generates the next deterministic address for the +wallet. + +**Request:** `NextAddressRequest` + +- `uint32 account`: The number of the account to derive the next address for. + +- `Kind kind`: The type of address to generate. + + **Nested enum:** `Kind` + + - `BIP0044_EXTERNAL`: The request specifies to generate the next address for + the account's BIP0044 external key chain. + + - `BIP0044_INTERNAL`: The request specifies to generate the next address for + the account's BIP0044 internal key chain. + +**Response:** `NextAddressResponse` + +- `string address`: The payment address string. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable + +___ + +#### `ImportPrivateKey` + +The `ImportPrivateKey` method imports a private key in Wallet Import Format +(WIF) encoding to a wallet account. A rescan may optionally be started to search +for transactions involving the private key's associated payment address. + +**Request:** `ImportPrivateKeyRequest` + +- `bytes passphrase`: The wallet's private passphrase. + +- `uint32 account`: The account number to associate the imported key with. + +- `string private_key_wif`: The private key, encoded using WIF. + +- `bool rescan`: Whether or not to perform a blockchain rescan for the imported + key. + +**Response:** `ImportPrivateKeyResponse` + +**Expected errors:** + +- `InvalidArgument`: The private key WIF string is not a valid WIF encoding. + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The private passphrase is incorrect. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable + +___ + +#### `FundTransaction` + +The `FundTransaction` method queries the wallet for unspent transaction outputs +controlled by some account. Results may be refined by setting a target output +amount and limiting the required confirmations. The selection algorithm is +unspecified. + +Output results are always created even if a minimum target output amount could +not be reached. This allows this method to behave similar to the `Balance` +method while also including the outputs that make up that balance. + +Change outputs can optionally be returned by this method as well. This can +provide the caller with everything necessary to construct an unsigned +transaction paying to already known addresses or scripts. + +**Request:** `FundTransactionRequest` + +- `uint32 account`: Account number containing the keys controlling the output + set to query. + +- `int64 target_amount`: If positive, the service may limit output results to + those that sum to at least this amount (counted in Satoshis). If zero, all + outputs not excluded by other arguments are returned. This may not be + negative. + +- `int32 required_confirmations`: The minimum number of block confirmations + needed to consider including an output in the return set. This may not be + negative. + +- `bool include_immature_coinbases`: If true, immature coinbase outputs will + also be included. + +- `bool include_change_script`: If true, a change script is included in the + response object. + +**Response:** `FundTransactionResponse` + +- `repeated PreviousOutput selected_outputs`: The output set returned as a list + of `PreviousOutput` nested message objects. + + **Nested message:** `PreviousOutput` + + - `bytes transaction_hash`: The hash of the transaction this output + originates from. + + - `uint32 output_index`: The output index of the transaction this output + originates from. + + - `int64 amount`: The output value (counted in Satoshis) of the unspent + transaction output. + + - `bytes pk_script`: The output script of the unspent transaction output. + + - `int64 receive_time`: The earliest Unix time the wallet became aware of + the transaction containing this output. + + - `bool from_coinbase`: Whether the output is a coinbase output. + +- `int64 total_amount`: The sum of all returned output amounts. This may be less + than a positive target amount if there were not enough eligible outputs + available. + +- `bytes change_pk_script`: A transaction output script used to pay the + remaining amount to a newly-generated change address for the account. This is + null if `include_change_script` was false or the target amount was not + exceeded. + +**Expected errors:** + +- `InvalidArgument`: The target amount is negative. + +- `InvalidArgument`: The required confirmations is negative. + +- `Aborted`: The wallet database is closed. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable + +___ + +#### `SignTransaction` + +The `SignTransaction` method adds transaction input signatures to a serialized +transaction using a wallet private keys. + +**Request:** `SignTransactionRequest` + +- `bytes passphrase`: The wallet's private passphrase. + +- `bytes serialized_transaction`: The transaction to add input signatures to. + +- `repeated uint32 input_indexes`: The input indexes that signature scripts must + be created for. If there are no indexes, input scripts are created for every + input that is missing an input script. + +**Response:** `SignTransactionResponse` + +- `bytes transaction`: The serialized transaction with added input scripts. + +- `repeated uint32 unsigned_input_indexes`: The indexes of every input that an + input script could not be created for. + +**Expected errors:** + +- `InvalidArgument`: The serialized transaction can not be decoded. + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The private passphrase is incorrect. + +**Stability:** Unstable: It is unclear if the request should include an account, +and only secrets of that account are used when creating input scripts. It's also +missing options similar to Core's signrawtransaction, such as the sighash flags +and additional keys. + +___ + +#### `PublishTransaction` + +The `PublishTransaction` method publishes a signed, serialized transaction to +the Bitcoin network. If the transaction spends any of the wallet's unspent +outputs or creates a new output controlled by the wallet, it is saved by the +wallet and republished later if it or a double spend are not mined. + +**Request:** `PublishTransactionRequest` + +- `bytes signed_transaction`: The signed transaction to publish. + +**Response:** `PublishTransactionResponse` + +**Expected errors:** + +- `InvalidArgument`: The serialized transaction can not be decoded or is missing + input scripts. + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `TransactionNotifications` + +The `TransactionNotifications` method returns a stream of notifications +regarding changes to the blockchain and transactions relevant to the wallet. + +**Request:** `TransactionNotificationsRequest` + +**Response:** `stream TransactionNotificationsResponse` + +- `repeated BlockDetails attached_blocks`: A list of blocks attached to the main + chain, sorted by increasing height. All newly mined transactions are included + in these messages, in the message corresponding to the block that contains + them. If this field has zero length, the notification is due to an unmined + transaction being added to the wallet. + + The `BlockDetails` message is used by other methods and is documented + [here](#blockdetails). + +- `repeated bytes detached_blocks`: The hashes of every block that was + reorganized out of the main chain. These are sorted by heights in decreasing + order (newest blocks first). + +- `repeated TransactionDetails unmined_transactions`: All newly added unmined + transactions. When relevant transactions are reorganized out and not included + in (or double-spent by) the new chain, they are included here. + + The `TransactionDetails` message is used by other methods and is documented + [here](#transactiondetails). + +- `repeated bytes unmined_transaction_hashes`: The hashes of every + currently-unmined transaction. This differs from the `unmined_transactions` + field by including every unmined transaction, rather than those newly added to + the unmined set. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable: This method could use a better name. + +___ + +#### `SpentnessNotifications` + +The `SpentnessNotifications` method returns a stream of notifications regarding +the spending of unspent outputs and/or the discovery of new unspent outputs for +an account. + +**Request:** `SpentnessNotificationsRequest` + +- `uint32 account`: The account to create notifications for. + +- `bool no_notify_unspent`: If true, do not send any notifications for + newly-discovered unspent outputs controlled by the account. + +- `bool no_notify_spent`: If true, do not send any notifications for newly-spent + transactions controlled by the account. + +**Response:** `stream SpentnessNotificationsResponse` + +- `bytes transaction_hash`: The hash of the serialized transaction containing + the output being reported. + +- `uint32 output_index`: The output index of the output being reported. + +- `Spender spender`: If null, the output is a newly-discovered unspent output. + If not null, the message records the transaction input that spends the + previously-unspent output. + + **Nested message:** `Spender` + + - `bytes transaction_hash`: The hash of the serialized transaction that + spends the reported output. + + - `uint32 input_index`: The index of the input that spends the reported + output. + +**Expected errors:** + +- `InvalidArgument`: The `no_notify_unspent` and `no_notify_spent` request + fields are both true. + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `AccountNotifications` + +The `AccountNotifications` method returns a stream of notifications for account +property changes, such as name and key counts. + +**Request:** `AccountNotificationsRequest` + +**Response:** `stream AccountNotificationsResponse` + +- `uint32 account_number`: The BIP0044 account being reported. + +- `string account_name`: The current account name. + +- `uint32 external_key_count`: The current number of BIP0032 external keys + derived for the account. + +- `uint32 internal_key_count`: The current number of BIP0032 internal keys + derived for the account. + +- `uint32 imported_key_count`: The current number of private keys imported into + the account. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable: This should probably share a message with the +`Accounts` method. + +___ + +### Shared messages + +The following messages are used by multiple methods. To avoid unnecessary +duplication, they are documented once here. + +#### `BlockDetails` + +The `BlockDetails` message is included in responses to report a block and the +wallet's relevant transactions contained therein. + +- `bytes hash`: The hash of the block being reported. + +- `int32 height`: The height of the block being reported. + +- `int64 timestamp`: The Unix time included in the block header. + +- `repeated TransactionDetails transactions`: All transactions relevant to the + wallet that are mined in this block. Transactions are sorted by their block + index in increasing order. + + The `TransactionDetails` message is used by other methods and is documented + [here](#transactiondetails). + +**Stability**: Unstable: This should probably include the block version. + +___ + +#### `TransactionDetails` + +The `TransactionDetails` message is included in responses to report transactions +relevant to the wallet. The message includes details such as which previous +wallet inputs are spent by this transaction, whether each output is controlled +by the wallet or not, the total fee (if calculable), and the earlist time the +transaction was seen. + +- `bytes hash`: The hash of the serialized transaction. + +- `bytes transaction`: The serialized transaction. + +- `repeated Input debits`: Properties for every previously-unspent wallet output + spent by this transaction. + + **Nested message:** `Input` + + - `uint32 index`: The transaction input index of the input being reported. + + - `uint32 previous_account`: The account that controlled the now-spent + output. + + - `int64 previous_amount`: The previous output value. + +- `repeated Output credits`: Properties for every output controlled by the + wallet. + + **Nested message:** `Output` + + - `uint32 index`: The transaction output index of the output being reported. + + - `uint32 account`: The account number of the controlled output. + + - `bool internal`: Whether the output pays to an address derived from the + account's internal key series. This often means the output is a change + output. + +- `int64 fee`: The transaction fee, if calculable. The fee is only calculable + when every previous output spent by this transaction is also recorded by + wallet. Otherwise, this field is zero. + +- `int64 timestamp`: The Unix time of the earliest time this transaction was + seen. + +**Stability**: Unstable: Since the caller is expected to decode the serialized +transaction, and would have access to every output script, the output properties +could be changed to only include outputs controlled by the wallet. diff --git a/cmd/wallet/doc/clientusage.md b/cmd/wallet/doc/clientusage.md new file mode 100755 index 0000000..64b231b --- /dev/null +++ b/cmd/wallet/doc/clientusage.md @@ -0,0 +1,480 @@ +# Client usage + +Clients use RPC to interact with the wallet. A client may be implemented in any +language directly supported by [gRPC](http://www.grpc.io/), languages capable of +performing [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface) with +these, and languages that share a common runtime (e.g. Scala, Kotlin, and Ceylon +for the JVM, F# for the CLR, etc.). Exact instructions differ slightly depending +on the language being used, but the general process is the same for each. In +short summary, to call RPC server methods, a client must: + +1. Generate client bindings specific for the [wallet RPC server API](api.md) +2. Import or include the gRPC dependency +3. (Optional) Wrap the client bindings with application-specific types +4. Open a gRPC channel using the wallet server's self-signed TLS certificate + +The only exception to these steps is if the client is being written in Go. In +that case, the first step may be omitted by importing the bindings from +btcwallet itself. + +The rest of this document provides short examples of how to quickly get started +by implementing a basic client that fetches the balance of the default account +(account 0) from a testnet3 wallet listening on `localhost:18332` in several +different languages: + +- [Go](#go) +- [C++](#cpp) +- [C#](#csharp) +- [Node.js](#nodejs) +- [Python](#python) + +Unless otherwise stated under the language example, it is assumed that gRPC is +already already installed. The gRPC installation procedure can vary greatly +depending on the operating system being used and whether a gRPC source install +is required. Follow +the [gRPC install instructions](https://github.com/grpc/grpc/blob/master/INSTALL) +if gRPC is not already installed. A full gRPC install also includes +[Protocol Buffers](https://github.com/google/protobuf) (compiled with support +for the proto3 language version), which contains the protoc tool and language +plugins used to compile this project's `.proto` +files to language-specific bindings. + +## Go + +The native gRPC library (gRPC Core) is not required for Go clients (a pure Go +implementation is used instead) and no additional setup is required to generate +Go bindings. + +```Go +package main + +import ( + "fmt" + "path/filepath" + + log "github.com/p9c/p9/pkg/logi" + + pb "git.parallelcoin.io/mod/rpc/walletrpc" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/btcsuite/btcutil" +) + +var certificateFile = filepath.Join(btcutil.AppDataDir("mod", false), + "rpc.cert" +) + +func main() { + + creds, e := credentials.NewClientTLSFromFile(certificateFile, "localhost") + if e != nil { + L. + return + } + conn, e := grpc.Dial("localhost:18332", + grpc.WithTransportCredentials(creds) + ) + if e != nil { + L. + return + } + defer conn.Close() + c := pb.NewWalletServiceClient(conn) + + balanceRequest := &pb.BalanceRequest{ + AccountNumber: 0, + RequiredConfirmations: 1, + } + balanceResponse, e := c.Balance(context.Background(), balanceRequest) + if e != nil { + L. + return + } + + log.Println("Spendable balance: ", btcutil.Amount(balanceResponse + .Spendable)) +} +``` + + +## C++ + +**Note:** Protocol Buffers and gRPC require at least C++11. The example client +is written using C++14. + +**Note:** The following instructions assume the client is being written on a +Unix-like platform (with instructions using the `sh` shell and Unix-isms in the +example source code) with a source gRPC install in `/usr/local`. + +First, generate the C++ language bindings by compiling the `.proto`: + +```bash +$ protoc -I/path/to/btcwallet/rpc --cpp_out=. --grpc_out=. \ + --plugin=protoc-genopts-grpc=$(which grpc_cpp_plugin) \ + /path/to/btcwallet/rpc/api.proto +``` + +Once the `.proto` file has been compiled, the example client can be completed. +Note that the following code uses synchronous calls which will block the main +thread on all gRPC IO. + +```C++ +// example.cc +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "api.grpc.pb.h" + +using namespace std::string_literals; + +struct NoHomeDirectoryException : std::exception { + + char const* what() const noexcept override { + + return "Failed to lookup home directory"; + } +}; + +auto read_file(std::string const& file_path) -> std::string { + + std::ifstream in{file_path}; + std::stringstream ss{}; + ss << in.rdbuf(); + return ss.str(); +} + +auto main() -> int { + + // Before the gRPC native library (gRPC Core) is lazily loaded and + // initialized, an environment variable must be set so BoringSSL is + // configured to use ECDSA TLS certificates (required by btcwallet). + setenv("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA", 1); + + // Note: This path is operating system-dependent. This can be created + // portably using boost::filesystem or the experimental filesystem class + // expected to ship in C++17. + auto wallet_tls_cert_file = []{ + auto pw = getpwuid(getuid()); + if (pw == nullptr || pw->pw_dir == nullptr) { + + + throw NoHomeDirectoryException{}; + } + return pw->pw_dir + "/.btcwallet/rpc.cert"s; + }(); + + grpc::SslCredentialsOptions cred_options{ + .pem_root_certs = read_file(wallet_tls_cert_file), + }; + auto creds = grpc::SslCredentials(cred_options); + auto channel = grpc::CreateChannel("localhost:18332", creds); + auto stub = walletrpc::WalletService::NewStub(channel); + + grpc::ClientContext context{}; + + walletrpc::BalanceRequest request{}; + request.set_account_number(0); + request.set_required_confirmations(1); + + walletrpc::BalanceResponse response{}; + auto status = stub->Balance(&context, request, &response); + if (!status.ok()) { + + + std::cout << status.error_message() << std::endl; + } else { + + std::cout << "Spendable balance: " << response.spendable() << " Satoshis" << std::endl; + } +} +``` + +The example can then be built with the following commands: + +```bash +$ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.pb.o api.pb.cc +$ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.grpc.pb.o api.grpc.pb.cc +$ c++ -std=c++14 -I/usr/local/include -pthread -c -o example.o example.cc +$ c++ *.o -L/usr/local/lib -lgrpc++ -lgrpc -lgpr -lprotobuf -lpthread -ldl -o example +``` + + +## C# + +The quickest way of generating client bindings in a Windows .NET environment is +by using the protoc binary included in the gRPC NuGet package. From the NuGet +package manager PowerShell console, this can be performed with: + +``` +PM> Install-Package Grpc +``` + +The protoc and C# plugin binaries can then be found in the packages directory. +For example, `.\packages\Google.Protobuf.x.x.x\tools\protoc.exe` and +`.\packages\Grpc.Tools.x.x.x\tools\grpc_csharp_plugin.exe`. + +When writing a client on other platforms (e.g. Mono on OS X), or when doing a +full gRPC source install on Windows, protoc and the C# plugin must be installed +by other means. Consult +the [official documentation](https://github.com/grpc/grpc/blob/master/src/csharp/README.md) +for these steps. + +Once protoc and the C# plugin have been obtained, client bindings can be +generated. The following command generates the files `Api.cs` and `ApiGrpc.cs` +in the `Example` project directory using the `Walletrpc` namespace: + +```PowerShell +PS> & protoc.exe -I \Path\To\btcwallet\rpc --csharp_out=Example --grpc_out=Example ` + --plugin=protoc-gen-grpc=\Path\To\grpc_csharp_plugin.exe ` + \Path\To\btcwallet\rpc\api.proto +``` + +Once references have been added to the project for the `Google.Protobuf` and +`Grpc.Core` assemblies, the example client can be implemented. + +```C# +using Grpc.Core; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Walletrpc; + +namespace Example +{ + static class Program + { + + static void Main(string[] args) + { + + ExampleAsync().Wait(); + } + + static async Task ExampleAsync() + { + + // Before the gRPC native library (gRPC Core) is lazily loaded and initialized, + // an environment variable must be set so BoringSSL is configured to use ECDSA TLS + // certificates (required by btcwallet). + Environment.SetEnvironmentVariable("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA"); + + var walletAppData = Portability.LocalAppData(Environment.OSVersion.Platform, "mod"); + var walletTlsCertFile = Path.Combine(walletAppData, "rpc.cert"); + var cert = await FileUtils.ReadFileAsync(walletTlsCertFile); + var channel = new Channel("localhost:18332", new SslCredentials(cert)); + try + { + + var c = WalletService.NewClient(channel); + var balanceRequest = new BalanceRequest + { + + AccountNumber = 0, + RequiredConfirmations = 1, + }; + var balanceResponse = await c.BalanceAsync(balanceRequest); + Console.WriteLine($"Spendable balance: {balanceResponse.Spendable} Satoshis"); + } + finally + { + + await channel.ShutdownAsync(); + } + } + } + + static class FileUtils + { + + public static async Task ReadFileAsync(string filePath) + { + + using (var r = new StreamReader(filePath, Encoding.UTF8)) + { + + return await r.ReadToEndAsync(); + } + } + } + + static class Portability + { + + public static string LocalAppData(PlatformID platform, string processName) + { + + if (processName == null) + throw new ArgumentNullException(nameof(processName)); + if (processName.Length == 0) + throw new ArgumentException(nameof(processName) + " may not have zero length"); + + switch (platform) + { + + case PlatformID.Win32NT: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + ToUpper(processName)); + case PlatformID.MacOSX: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + "Library", "Application Support", ToUpper(processName)); + case PlatformID.Unix: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ToDotLower(processName)); + default: + throw new PlatformNotSupportedException($"PlatformID={platform}"); + } + } + + private static string ToUpper(string value) + { + + var firstChar = value[0]; + if (char.IsUpper(firstChar)) + return value; + else + return char.ToUpper(firstChar) + value.Substring(1); + } + + private static string ToDotLower(string value) + { + + var firstChar = value[0]; + return "." + char.ToLower(firstChar) + value.Substring(1); + } + } +} +``` + +## Node.js + +First, install gRPC (either by building the latest source release, or by +installing a gRPC binary development package through your operating system's +package manager). This is required to install the npm module as it wraps the +native C library (gRPC Core) with C++ bindings. Installing +the [grpc module](https://www.npmjs.com/package/grpc) to your project can then +be done by executing: + +``` +npm install grpc +``` + +A Node.js client does not require generating JavaScript stub files for the +wallet's API from the `.proto`. Instead, a call to `grpc.load` +with the `.proto` file path dynamically loads the Protobuf descriptor and +generates bindings for each service. Either copy the `.proto` to the client +project directory, or reference the file from the +`btcwallet` project directory. + +```JavaScript +// Before the gRPC native library (gRPC Core) is lazily loaded and +// initialized, an environment variable must be set so BoringSSL is +// configured to use ECDSA TLS certificates (required by btcwallet). +process.env['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'; + +var fs = require('fs'); +var path = require('path'); +var os = require('os'); +var grpc = require('grpc'); +var protoDescriptor = grpc.load('./api.proto'); +var walletrpc = protoDescriptor.walletrpc; + +var certPath = path.join(process.env.HOME, '.btcwallet', 'rpc.cert'); +if (os.platform == 'win32') { + + + certPath = path.join(process.env.LOCALAPPDATA, 'Btcwallet', 'rpc.cert'); +} else if (os.platform == 'darwin') { + + + certPath = path.join(process.env.HOME, 'Library', 'Application Support', + 'Btcwallet', 'rpc.cert'); +} + +var cert = fs.readFileSync(certPath); +var creds = grpc.credentials.createSsl(cert); +var client = new walletrpc.WalletService('localhost:18332', creds); + +var request = { + + account_number: 0, + required_confirmations: 1 +}; +client.balance(request, function (err, response) { + + + if (err) { + + + console.err.Ln(; + } else { + + console.log('Spendable balance:', response.spendable, 'Satoshis'); + } +}); +``` + +## Python + +**Note:** gRPC requires Python 2.7. + +After installing gRPC Core and Python development headers, `pip` +should be used to install the `grpc` module and its dependencies. Full +instructions for this procedure can be found +[here](https://github.com/grpc/grpc/blob/master/src/python/README.md). + +Generate Python stubs from the `.proto`: + +```bash +$ protoc -I /path/to/btcsuite/btcwallet/rpc --python_out=. --grpc_out=. \ + --plugin=protoc-genopts-grpc=$(which grpc_python_plugin) \ + /path/to/btcwallet/rpc/api.proto +``` + +Implement the client: + +```Python +import os +import platform +from grpc.beta import implementations + +import api_pb2 as walletrpc + +timeout = 1 # seconds + +def main(): + # Before the gRPC native library (gRPC Core) is lazily loaded and + # initialized, an environment variable must be set so BoringSSL is + # configured to use ECDSA TLS certificates (required by btcwallet). + os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' + + cert_file_path = os.path.join(os.environ['HOME'], '.btcwallet', 'rpc.cert') + if platform.system() == 'Windows': + cert_file_path = os.path.join(os.environ['LOCALAPPDATA'], "mod", "rpc.cert") + elif platform.system() == 'Darwin': + cert_file_path = os.path.join(os.environ['HOME'], 'Library', 'Application Support', + 'Btcwallet', 'rpc.cert') + + with open(cert_file_path, 'r') as f: + cert = f.read() + creds = implementations.ssl_client_credentials(cert, None, None) + channel = implementations.secure_channel('localhost', 18332, creds) + stub = walletrpc.beta_create_WalletService_stub(channel) + + request = walletrpc.BalanceRequest(account_number = 0, required_confirmations = 1) + response = stub.Balance(request, timeout) + print 'Spendable balance: %d Satoshis' % response.spendable + +if __name__ == '__main__': + main() +``` diff --git a/cmd/wallet/doc/serverchanges.md b/cmd/wallet/doc/serverchanges.md new file mode 100644 index 0000000..65a42c7 --- /dev/null +++ b/cmd/wallet/doc/serverchanges.md @@ -0,0 +1,95 @@ +# Making API Changes + +This document describes the process of how btcwallet developers must make +changes to the RPC API and server. Due to the use of gRPC and Protocol Buffers +for the RPC implementation, changes to this API require extra dependencies and +steps before changes to the server can be implemented. + +## Requirements + +- The Protocol Buffer compiler `protoc` installed with support for the `proto3` + language + + The `protoc` tool is part of the Protocol Buffers project. This can be + installed [from source](https://github.com/google/protobuf/blob/master/INSTALL.txt) + , from + an [official binary release](https://github.com/google/protobuf/releases), or + through an operating system's package manager. + +- The gRPC `protoc` plugin for Go + + This plugin is written in Go and can be installed using `go get`: + + ``` + go get github.com/golang/protobuf/protoc-gen-go + ``` + +- Knowledge of Protocol Buffers version 3 (proto3) + +Note that a full installation of gRPC Core is not required, and only the +`protoc` compiler and Go plugins are necessary. This is due to the project using +a pure Go gRPC implementation instead of wrapping the C library from gRPC Core. + +## Step 1: Modify the `.proto` + +Once the developer dependencies have been met, changes can be made to the API by +modifying the Protocol Buffers descriptor file [`api.proto`](../api.proto_). + +The API is versioned according to the rules +of [Semantic Versioning 2.0](http://semver.org/). After any changes, bump the +API version in the [API specification](api.md) and add the changes to the spec. + +Unless backwards compatibility is broken (and the version is bumped to represent +this change), message fields must never be removed or changed, and new fields +must always be appended. + +It is forbidden to use the `required` attribute on a message field as this can +cause errors during parsing when the new API is used by an older client. +Instead, the (implicit) optional attribute is used, and the server +implementation must return an appropriate error if the new request field is not +set to a valid value. + +## Step 2: Compile the `.proto` + +Once changes to the descriptor file and API specification have been made, the +`protoc` compiler must be used to compile the descriptor into a Go package. This +code contains interfaces (stubs) for each service (to be implemented by the +wallet) and message types used for each RPC. This same code can also be imported +by a Go client that then calls same interface methods to perform RPC with the +wallet. + +By committing the autogenerated package to the project repo, the `proto3` +compiler and plugin are not needed by users installing the project by source or +by other developers not making changes to the RPC API. + +A `sh` shell script is included to compile the Protocol Buffers descriptor. It +must be run from the `rpc` directory. + +```bash +$ sh regen.sh +``` + +If a `sh` shell is unavailable, the command can be run manually instead (again +from the `rpc` directory). + +``` +protoc -I. api.proto --go_out=plugins=grpc:walletrpc +``` + +TODO(jrick): This step could be simplified and be more portable by putting the +commands in a Go source file and executing them with `go generate`. It should, +however, only be run when API changes are performed (not +with `go generate ./...` in the project root) since not all developers are +expected to have +`protoc` installed. + +## Step 3: Implement the API change in the RPC server + +After the Go code for the API has been regenated, the necessary changes can be +implemented in the [`rpcserver`](../rpcserver/) package. + +## Additional Resources + +- [Protocol Buffers Language Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3) +- [Protocol Buffers Basics: Go](https://developers.google.com/protocol-buffers/docs/gotutorial) +- [gRPC Basics: Go](http://www.grpc.io/docs/tutorials/basic/go.html) diff --git a/cmd/wallet/docs/README.md b/cmd/wallet/docs/README.md new file mode 100755 index 0000000..734976b --- /dev/null +++ b/cmd/wallet/docs/README.md @@ -0,0 +1,3 @@ +### Guides + +[Rebuilding all transaction history with forced rescans](https://github.com/p9c/p9/walletmain/tree/master/docs/force_rescans.md) diff --git a/cmd/wallet/dropwallethistory.go b/cmd/wallet/dropwallethistory.go new file mode 100644 index 0000000..6003c37 --- /dev/null +++ b/cmd/wallet/dropwallethistory.go @@ -0,0 +1,87 @@ +package wallet + +import ( + "encoding/binary" + "path/filepath" + + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wtxmgr" + "github.com/p9c/p9/pod/config" +) + +func DropWalletHistory(w *Wallet, cfg *config.Config) (e error) { + var ( + // Namespace keys. + syncBucketName = []byte("sync") + waddrmgrNamespace = []byte("waddrmgr") + wtxmgrNamespace = []byte("wtxmgr") + // Sync related key names (sync bucket). + syncedToName = []byte("syncedto") + startBlockName = []byte("startblock") + recentBlocksName = []byte("recentblocks") + ) + dbPath := filepath.Join( + cfg.DataDir.V(), + cfg.Network.V(), "wallet.db", + ) + // I.Ln("dbPath", dbPath) + var db walletdb.DB + db, e = walletdb.Open("bdb", dbPath) + if E.Chk(e) { + // DBError("failed to open database:", err) + return e + } + defer db.Close() + D.Ln("dropping wtxmgr namespace") + e = walletdb.Update( + db, func(tx walletdb.ReadWriteTx) (e error) { + D.Ln("deleting top level bucket") + if e = tx.DeleteTopLevelBucket(wtxmgrNamespace); E.Chk(e) { + } + if e != nil && e != walletdb.ErrBucketNotFound { + return e + } + var ns walletdb.ReadWriteBucket + D.Ln("creating new top level bucket") + if ns, e = tx.CreateTopLevelBucket(wtxmgrNamespace); E.Chk(e) { + return e + } + if e = wtxmgr.Create(ns); E.Chk(e) { + return e + } + ns = tx.ReadWriteBucket(waddrmgrNamespace).NestedReadWriteBucket(syncBucketName) + startBlock := ns.Get(startBlockName) + D.Ln("putting start block", startBlock) + if e = ns.Put(syncedToName, startBlock); E.Chk(e) { + return e + } + recentBlocks := make([]byte, 40) + copy(recentBlocks[0:4], startBlock[0:4]) + copy(recentBlocks[8:], startBlock[4:]) + binary.LittleEndian.PutUint32(recentBlocks[4:8], uint32(1)) + defer D.Ln("put recent blocks") + return ns.Put(recentBlocksName, recentBlocks) + }, + ) + if E.Chk(e) { + return e + } + D.Ln("updated wallet") + // if w != nil { + // // Rescan chain to ensure balance is correctly regenerated + // job := &wallet.RescanJob{ + // InitialSync: true, + // } + // // Submit rescan job and log when the import has completed. + // // Do not block on finishing the rescan. The rescan success + // // or failure is logged elsewhere, and the channel is not + // // required to be read, so discard the return value. + // errC := w.SubmitRescan(job) + // select { + // case e := <-errC: + // DB // // case <-time.After(time.Second * 5): + // // break + // } + // } + return e +} diff --git a/cmd/wallet/errors.go b/cmd/wallet/errors.go new file mode 100644 index 0000000..c6b0b1e --- /dev/null +++ b/cmd/wallet/errors.go @@ -0,0 +1,69 @@ +package wallet + +import ( + "errors" + + "github.com/p9c/p9/pkg/btcjson" +) + +// TODO(jrick): There are several error paths which 'replace' various errors +// with a more appropiate error from the json package. Create a map of +// these replacements so they can be handled once after an RPC handler has +// returned and before the error is marshaled. +// +// BTCJSONError types to simplify the reporting of specific categories of +// errors, and their *json.RPCError creation. +type ( + // DeserializationError describes a failed deserializaion due to bad user input. It corresponds to + // json.ErrRPCDeserialization. + DeserializationError struct { + error + } + // InvalidParameterError describes an invalid parameter passed by the user. It corresponds to + // json.ErrRPCInvalidParameter. + InvalidParameterError struct { + error + } + // ParseError describes a failed parse due to bad user input. It corresponds to json.ErrRPCParse. + ParseError struct { + error + } +) + +// Errors variables that are defined once here to avoid duplication below. +var ( + ErrNeedPositiveAmount = InvalidParameterError{ + errors.New("amount must be positive"), + } + ErrNeedPositiveMinconf = InvalidParameterError{ + errors.New("minconf must be positive"), + } + ErrAddressNotInWallet = btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "address not found in wallet", + } + ErrAccountNameNotFound = btcjson.RPCError{ + Code: btcjson.ErrRPCWalletInvalidAccountName, + Message: "account name not found", + } + ErrUnloadedWallet = btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "Request requires a wallet but wallet has not loaded yet", + } + ErrWalletUnlockNeeded = btcjson.RPCError{ + Code: btcjson.ErrRPCWalletUnlockNeeded, + Message: "Enter the wallet passphrase with walletpassphrase first", + } + ErrNotImportedAccount = btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "imported addresses must belong to the imported account", + } + ErrNoTransactionInfo = btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "No information for transaction", + } + ErrReservedAccountName = btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Account name is reserved by RPC server", + } +) diff --git a/cmd/wallet/genapi/genapi.go b/cmd/wallet/genapi/genapi.go new file mode 100644 index 0000000..dbf7716 --- /dev/null +++ b/cmd/wallet/genapi/genapi.go @@ -0,0 +1,517 @@ +package main + +import ( + "os" + "sort" + "text/template" + + "github.com/p9c/p9/pkg/log" +) + +type handler struct { + Method, Handler, HandlerWithChain, Cmd, ResType string +} + +type handlersT []handler + +func (h handlersT) Len() int { + return len(h) +} + +func (h handlersT) Less(i, j int) bool { + return h[i].Method < h[j].Method +} + +func (h handlersT) Swap(i, j int) { + h[i], h[j] = h[j], h[i] +} + +func main() { + log.SetLogLevel("trace") + if fd, e := os.Create("rpchandlers.go"); E.Chk(e) { + } else { + defer fd.Close() + t := template.Must(template.New("noderpc").Parse(NodeRPCHandlerTpl)) + sort.Sort(handlers) + if e = t.Execute(fd, handlers); E.Chk(e) { + } + } +} + +const ( + RPCMapName = "RPCHandlers" + Worker = "CAPI" +) + +var NodeRPCHandlerTpl = `// generated by go run ./genapi/.; DO NOT EDIT +// +`+`//go:generate go run ./genapi/. + +package wallet + +import ( + "io" + "net/rpc" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainclient" +) + +// API stores the channel, parameters and result values from calls via the channel +type API struct { + Ch interface{} + Params interface{} + Result interface{} +} + +// CAPI is the central structure for configuration and access to a net/rpc API access endpoint for this RPC API +type CAPI struct { + Timeout time.Duration + quit qu.C +} + +// NewCAPI returns a new CAPI +func NewCAPI(quit qu.C, timeout ...time.Duration) (c *CAPI) { + c = &CAPI{quit: quit} + if len(timeout)>0 { + c.Timeout = timeout[0] + } else { + c.Timeout = time.Second * 5 + } + return +} + +// CAPIClient is a wrapper around RPC calls +type CAPIClient struct { + *rpc.Client +} + +// NewCAPIClient creates a new client for a kopach_worker. Note that any kind of connection can be used here, +// other than the StdConn +func NewCAPIClient(conn io.ReadWriteCloser) *CAPIClient { + return &CAPIClient{rpc.NewClient(conn)} +} + +type ( + // None means no parameters it is not checked so it can be nil + None struct{} {{range .}} + // {{.Handler}}Res is the result from a call to {{.Handler}} + {{.Handler}}Res struct { Res *{{.ResType}}; e error }{{end}} +) + +// RequestHandler is a handler function to handle an unmarshaled and parsed request into a marshalable response. If the +// error is a *json.RPCError or any of the above special error classes, the server will respond with the JSON-RPC +// appropriate error code. All other errors use the wallet catch-all error code, json.ErrRPCWallet. +type RequestHandler func(interface{}, *Wallet, + ...*chainclient.RPCClient) (interface{}, error) + +// ` + RPCMapName + ` is all of the RPC calls available +// +// - Handler is the handler function +// +// - Call is a channel carrying a struct containing parameters and error that is listened to in RunAPI to dispatch the +// calls +// +// - Result is a bundle of command parameters and a channel that the result will be sent back on +// +// Get and save the Result function's return, and you can then call the call functions check, result and wait functions +// for asynchronous and synchronous calls to RPC functions +var ` + RPCMapName + ` = map[string]struct { + Handler RequestHandler + // Function variables cannot be compared against anything but nil, so use a boolean to record whether help + // generation is necessary. This is used by the tests to ensure that help can be generated for every implemented + // method. + // + // A single map and this bool is here is used rather than several maps for the unimplemented handlers so every + // method has exactly one handler function. + // + // The Return field returns a new channel of the type returned by this function. This makes it possible to use this + // for callers to receive a response in the cpc library which implements the functions as channel pipes + NoHelp bool + Call chan API + Params interface{} + Result func() API +}{ +{{range .}} "{{.Method}}":{ + Handler: {{.Handler}}, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan {{.Handler}}Res)} }}, +{{end}} +} + +// API functions +// +// The functions here provide access to the RPC through a convenient set of functions generated for each call in the RPC +// API to request, check for, access the results and wait on results + +{{range .}} +// {{.Handler}} calls the method with the given parameters +func (a API) {{.Handler}}(cmd {{.Cmd}}) (e error) { + ` + RPCMapName + `["{{.Method}}"].Call <- API{a.Ch, cmd, nil} + return +} + +// {{.Handler}}Check checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) {{.Handler}}Check() (isNew bool) { + select { + case o := <- a.Ch.(chan {{.Handler}}Res): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// {{.Handler}}GetRes returns a pointer to the value in the Result field +func (a API) {{.Handler}}GetRes() (out *{{.ResType}}, e error) { + out, _ = a.Result.(*{{.ResType}}) + e, _ = a.Result.(error) + return +} + +// {{.Handler}}Wait calls the method and blocks until it returns or 5 seconds passes +func (a API) {{.Handler}}Wait(cmd {{.Cmd}}) (out *{{.ResType}}, e error) { + ` + RPCMapName + `["{{.Method}}"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan {{.Handler}}Res): + out, e = o.Res, o.e + } + return +} +{{end}} + +// RunAPI starts up the api handler server that receives rpc.API messages and runs the handler and returns the result +// Note that the parameters are type asserted to prevent the consumer of the API from sending wrong message types not +// because it's necessary since they are interfaces end to end +func RunAPI(chainRPC *chainclient.RPCClient, wallet *Wallet, + quit qu.C) { + nrh := ` + RPCMapName + ` + go func() { + D.Ln("starting up wallet cAPI") + var e error + var res interface{} + for { + select { {{range .}} + case msg := <-nrh["{{.Method}}"].Call: + if res, e = nrh["{{.Method}}"]. + Handler(msg.Params.({{.Cmd}}), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.({{.ResType}}); ok { + msg.Ch.(chan {{.Handler}}Res) <- {{.Handler}}Res{&r, e} } {{end}} + case <-quit.Wait(): + D.Ln("stopping wallet cAPI") + return + } + } + }() +} + +// RPC API functions to use with net/rpc +{{range .}} +func (c *CAPI) {{.Handler}}(req {{.Cmd}}, resp {{.ResType}}) (e error) { + nrh := ` + RPCMapName + ` + res := nrh["{{.Method}}"].Result() + res.Params = req + nrh["{{.Method}}"].Call <- res + select { + case resp = <-res.Ch.(chan {{.ResType}}): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} +{{end}} +// Client call wrappers for a CAPI client with a given Conn +{{range .}} +func (r *CAPIClient) {{.Handler}}(cmd ...{{.Cmd}}) (res {{.ResType}}, e error) { + var c {{.Cmd}} + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("` + Worker + `.{{.Handler}}", c, &res); E.Chk(e) { + } + return +} +{{end}} +` + +var handlers = handlersT{ + { + Method: "addmultisigaddress", + Handler: "AddMultiSigAddress", + Cmd: "*btcjson.AddMultisigAddressCmd", + ResType: "string", + }, + { + Method: "createmultisig", + Handler: "CreateMultiSig", + Cmd: "*btcjson.CreateMultisigCmd", + ResType: "btcjson.CreateMultiSigResult", + }, + { + Method: "dumpprivkey", + Handler: "DumpPrivKey", + Cmd: "*btcjson.DumpPrivKeyCmd", + ResType: "string", + }, + { + Method: "getaccount", + Handler: "GetAccount", + Cmd: "*btcjson.GetAccountCmd", + ResType: "string", + }, + { + Method: "getaccountaddress", + Handler: "GetAccountAddress", + Cmd: "*btcjson.GetAccountAddressCmd", + ResType: "string", + }, + { + Method: "getaddressesbyaccount", + Handler: "GetAddressesByAccount", + Cmd: "*btcjson.GetAddressesByAccountCmd", + ResType: "[]string", + }, + { + Method: "getbalance", + Handler: "GetBalance", + Cmd: "*btcjson.GetBalanceCmd", + ResType: "float64", + }, + { + Method: "getbestblockhash", + Handler: "GetBestBlockHash", + Cmd: "*None", + ResType: "string", + }, + { + Method: "getblockcount", + Handler: "GetBlockCount", + Cmd: "*None", + ResType: "int32", + }, + { + Method: "getinfo", + Handler: "GetInfo", + Cmd: "*None", + ResType: "btcjson.InfoWalletResult", + }, + { + Method: "getnewaddress", + Handler: "GetNewAddress", + Cmd: "*btcjson.GetNewAddressCmd", + ResType: "string", + }, + { + Method: "getrawchangeaddress", + Handler: "GetRawChangeAddress", + Cmd: "*btcjson.GetRawChangeAddressCmd", + ResType: "string", + }, + { + Method: "getreceivedbyaccount", + Handler: "GetReceivedByAccount", + Cmd: "*btcjson.GetReceivedByAccountCmd", + ResType: "float64", + }, + { + Method: "getreceivedbyaddress", + Handler: "GetReceivedByAddress", + Cmd: "*btcjson.GetReceivedByAddressCmd", + ResType: "float64", + }, + { + Method: "gettransaction", + Handler: "GetTransaction", + Cmd: "*btcjson.GetTransactionCmd", + ResType: "btcjson.GetTransactionResult", + }, + { + Method: "help", + Handler: "HelpNoChainRPC", + HandlerWithChain: "HelpWithChainRPC", + Cmd: "btcjson.HelpCmd", + ResType: "string", + }, + { + Method: "importprivkey", + Handler: "ImportPrivKey", + Cmd: "*btcjson.ImportPrivKeyCmd", + ResType: "None", + }, + { + Method: "keypoolrefill", + Handler: "KeypoolRefill", + Cmd: "*None", + ResType: "None", + }, + { + Method: "listaccounts", + Handler: "ListAccounts", + Cmd: "*btcjson.ListAccountsCmd", + ResType: "map[string]float64", + }, + { + Method: "listlockunspent", + Handler: "ListLockUnspent", + Cmd: "*None", + ResType: "[]btcjson.TransactionInput", + }, + { + Method: "listreceivedbyaccount", + Handler: "ListReceivedByAccount", + Cmd: "*btcjson.ListReceivedByAccountCmd", + ResType: "[]btcjson.ListReceivedByAccountResult", + }, + { + Method: "listreceivedbyaddress", + Handler: "ListReceivedByAddress", + Cmd: "*btcjson.ListReceivedByAddressCmd", + ResType: "btcjson.ListReceivedByAddressResult", + }, + { + Method: "listsinceblock", + Handler: "ListSinceBlock", + HandlerWithChain: "ListSinceBlock", + Cmd: "btcjson.ListSinceBlockCmd", + ResType: "btcjson.ListSinceBlockResult", + }, + { + Method: "listtransactions", + Handler: "ListTransactions", + Cmd: "*btcjson.ListTransactionsCmd", + ResType: "[]btcjson.ListTransactionsResult", + }, + { + Method: "listunspent", + Handler: "ListUnspent", + Cmd: "*btcjson.ListUnspentCmd", + ResType: "[]btcjson.ListUnspentResult", + }, + { + Method: "sendfrom", + Handler: "LockUnspent", + HandlerWithChain: "LockUnspent", + Cmd: "btcjson.LockUnspentCmd", + ResType: "bool", + }, + { + Method: "sendmany", + Handler: "SendMany", + Cmd: "*btcjson.SendManyCmd", + ResType: "string", + }, + { + Method: "sendtoaddress", + Handler: "SendToAddress", + Cmd: "*btcjson.SendToAddressCmd", + ResType: "string", + }, + { + Method: "settxfee", + Handler: "SetTxFee", + Cmd: "*btcjson.SetTxFeeCmd", + ResType: "bool", + }, + { + Method: "signmessage", + Handler: "SignMessage", + Cmd: "*btcjson.SignMessageCmd", + ResType: "string", + }, + { + Method: "signrawtransaction", + Handler: "SignRawTransaction", + HandlerWithChain: "SignRawTransaction", + Cmd: "btcjson.SignRawTransactionCmd", + ResType: "btcjson.SignRawTransactionResult", + }, + { + Method: "validateaddress", + Handler: "ValidateAddress", + Cmd: "*btcjson.ValidateAddressCmd", + ResType: "btcjson.ValidateAddressWalletResult", + }, + { + Method: "verifymessage", + Handler: "VerifyMessage", + Cmd: "*btcjson.VerifyMessageCmd", + ResType: "bool", + }, + { + Method: "walletlock", + Handler: "WalletLock", + Cmd: "*None", + ResType: "None", + }, + { + Method: "walletpassphrase", + Handler: "WalletPassphrase", + Cmd: "*btcjson.WalletPassphraseCmd", + ResType: "None", + }, + { + Method: "walletpassphrasechange", + Handler: "WalletPassphraseChange", + Cmd: "*btcjson.WalletPassphraseChangeCmd", + ResType: "None", + }, + { + Method: "createnewaccount", + Handler: "CreateNewAccount", + Cmd: "*btcjson.CreateNewAccountCmd", + ResType: "None", + }, + { + Method: "getbestblock", + Handler: "GetBestBlock", + Cmd: "*None", + ResType: "btcjson.GetBestBlockResult", + }, + { + Method: "getunconfirmedbalance", + Handler: "GetUnconfirmedBalance", + Cmd: "*btcjson.GetUnconfirmedBalanceCmd", + ResType: "float64", + }, + { + Method: "listaddresstransactions", + Handler: "ListAddressTransactions", + Cmd: "*btcjson.ListAddressTransactionsCmd", + ResType: "[]btcjson.ListTransactionsResult", + }, + { + Method: "listalltransactions", + Handler: "ListAllTransactions", + Cmd: "*btcjson.ListAllTransactionsCmd", + ResType: "[]btcjson.ListTransactionsResult", + }, + { + Method: "renameaccount", + Handler: "RenameAccount", + Cmd: "*btcjson.RenameAccountCmd", + ResType: "None", + }, + { + Method: "walletislocked", + Handler: "WalletIsLocked", + Cmd: "*None", + ResType: "bool", + }, + { + Method: "dropwallethistory", + Handler: "HandleDropWalletHistory", + Cmd: "*None", + ResType: "string", + }, +} diff --git a/cmd/wallet/genapi/log.go b/cmd/wallet/genapi/log.go new file mode 100644 index 0000000..5145d3a --- /dev/null +++ b/cmd/wallet/genapi/log.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/wallet/loader.go b/cmd/wallet/loader.go new file mode 100644 index 0000000..5c94e87 --- /dev/null +++ b/cmd/wallet/loader.go @@ -0,0 +1,268 @@ +package wallet + +import ( + "errors" + "os" + "path/filepath" + "sync" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/util/prompt" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pod/config" +) + +// Loader implements the creating of new and opening of existing wallets, while providing a callback system for other +// subsystems to handle the loading of a wallet. This is primarily intended for use by the RPC servers, to enable +// methods and services which require the wallet when the wallet is loaded by another subsystem. +// +// Loader is safe for concurrent access. +type Loader struct { + Callbacks []func(*Wallet) + ChainParams *chaincfg.Params + DDDirPath string + RecoveryWindow uint32 + Wallet *Wallet + Loaded bool + DB walletdb.DB + Mutex sync.Mutex +} + +const () + +var ( + // ErrExists describes the error condition of attempting to create a new wallet when one exists already. + ErrExists = errors.New("wallet already exists") + // ErrLoaded describes the error condition of attempting to load or create a wallet when the loader has already done + // so. + ErrLoaded = errors.New("wallet already loaded") + // ErrNotLoaded describes the error condition of attempting to close a loaded wallet when a wallet has not been + // loaded. + ErrNotLoaded = errors.New("wallet is not loaded") + errNoConsole = errors.New("db upgrade requires console access for additional input") +) + +// CreateNewWallet creates a new wallet using the provided public and private passphrases. The seed is optional. If +// non-nil, addresses are derived from this seed. If nil, a secure random seed is generated. +func (ld *Loader) CreateNewWallet( + pubPassphrase, privPassphrase, seed []byte, + bday time.Time, + noStart bool, + podConfig *config.Config, + quit qu.C, +) (w *Wallet, e error) { + ld.Mutex.Lock() + defer ld.Mutex.Unlock() + if ld.Loaded { + return nil, ErrLoaded + } + // dbPath := filepath.Join(ld.DDDirPath, WalletDbName) + var exists bool + if exists, e = fileExists(ld.DDDirPath); E.Chk(e) { + return nil, e + } + if exists { + return nil, errors.New("Wallet ERROR: " + ld.DDDirPath + " already exists") + } + // Create the wallet database backed by bolt db. + p := filepath.Dir(ld.DDDirPath) + if e = os.MkdirAll(p, 0700); E.Chk(e) { + return nil, e + } + var db walletdb.DB + if db, e = walletdb.Create("bdb", ld.DDDirPath); E.Chk(e) { + return nil, e + } + // Initialize the newly created database for the wallet before opening. + if e = Create(db, pubPassphrase, privPassphrase, seed, ld.ChainParams, + bday); E.Chk(e) { + return nil, e + } + // Open the newly-created wallet. + if w, e = Open(db, pubPassphrase, nil, ld.ChainParams, ld.RecoveryWindow, + podConfig, quit); E.Chk(e) { + return nil, e + } + if !noStart { + w.Start() + ld.onLoaded(db) + } else { + if e = w.db.Close(); E.Chk(e) { + } + } + return w, nil +} + +// LoadedWallet returns the loaded wallet, if any, and a bool for whether the wallet has been loaded or not. If true, +// the wallet pointer should be safe to dereference. +func (ld *Loader) LoadedWallet() (*Wallet, bool) { + ld.Mutex.Lock() + w := ld.Wallet + ld.Mutex.Unlock() + return w, w != nil +} + +// OpenExistingWallet opens the wallet from the loader's wallet database path and the public passphrase. If the loader +// is being called by a context where standard input prompts may be used during wallet upgrades, setting +// canConsolePrompt will enables these prompts. +func (ld *Loader) OpenExistingWallet( + pubPassphrase []byte, + canConsolePrompt bool, + podConfig *config.Config, + quit qu.C, +) (w *Wallet, e error) { + defer ld.Mutex.Unlock() + ld.Mutex.Lock() + I.Ln("opening existing wallet", ld.DDDirPath) + if ld.Loaded { + I.Ln("already loaded wallet") + return nil, ErrLoaded + } + // Ensure that the network directory exists. + if e = checkCreateDir(filepath.Dir(ld.DDDirPath)); E.Chk(e) { + E.Ln("cannot create directory", ld.DDDirPath) + return nil, e + } + D.Ln("directory exists") + // Open the database using the boltdb backend. + dbPath := ld.DDDirPath + I.Ln("opening database", dbPath) + var db walletdb.DB + if db, e = walletdb.Open("bdb", dbPath); E.Chk(e) { + E.Ln("failed to open database '", ld.DDDirPath) + return nil, e + } + I.Ln("opened wallet database") + var cbs *waddrmgr.OpenCallbacks + if canConsolePrompt { + cbs = &waddrmgr.OpenCallbacks{ + ObtainSeed: prompt.ProvideSeed, + ObtainPrivatePass: prompt.ProvidePrivPassphrase, + } + } else { + cbs = &waddrmgr.OpenCallbacks{ + ObtainSeed: noConsole, + ObtainPrivatePass: noConsole, + } + } + D.Ln("opening wallet '" + string(pubPassphrase) + "'") + if w, e = Open( + db, + pubPassphrase, + cbs, + ld.ChainParams, + ld.RecoveryWindow, + podConfig, + quit, + ); E.Chk(e) { + E.Ln("failed to open wallet", e) + // If opening the wallet fails (e.g. because of wrong passphrase), we must close the backing database to allow + // future calls to walletdb.Open(). + if e = db.Close(); E.Chk(e) { + W.Ln("error closing database:", e) + } + return nil, e + } + ld.Wallet = w + D.Ln("starting wallet", w != nil) + w.Start() + D.Ln("waiting for load", db != nil) + ld.onLoaded(db) + D.Ln("wallet opened successfully", w != nil) + return w, nil +} + +// RunAfterLoad adds a function to be executed when the loader creates or opens a wallet. Functions are executed in a +// single goroutine in the order they are added. +func (ld *Loader) RunAfterLoad(fn func(*Wallet)) { + ld.Mutex.Lock() + if ld.Loaded { + // w := ld.Wallet + ld.Mutex.Unlock() + fn(ld.Wallet) + } else { + ld.Callbacks = append(ld.Callbacks, fn) + ld.Mutex.Unlock() + } +} + +// UnloadWallet stops the loaded wallet, if any, and closes the wallet database. This returns ErrNotLoaded if the wallet +// has not been loaded with CreateNewWallet or LoadExistingWallet. The Loader may be reused if this function returns +// without error. +func (ld *Loader) UnloadWallet() (e error) { + F.Ln("unloading wallet") + defer ld.Mutex.Unlock() + ld.Mutex.Lock() + if ld.Wallet == nil { + D.Ln("wallet not loaded") + return ErrNotLoaded + } + F.Ln("wallet stopping") + ld.Wallet.Stop() + F.Ln("waiting for wallet shutdown") + ld.Wallet.WaitForShutdown() + if ld.DB == nil { + D.Ln("there was no database") + return ErrNotLoaded + } + F.Ln("wallet stopped") + e = ld.DB.Close() + if e != nil { + D.Ln("error closing database", e) + return e + } + F.Ln("database closed") + ld.Loaded = false + ld.DB = nil + return nil +} + +// WalletExists returns whether a file exists at the loader's database path. This may return an error for unexpected I/O +// failures. +func (ld *Loader) WalletExists() (bool, error) { + return fileExists(ld.DDDirPath) +} + +// onLoaded executes each added callback and prevents loader from loading any additional wallets. Requires mutex to be +// locked. +func (ld *Loader) onLoaded(db walletdb.DB) { + D.Ln("wallet loader callbacks running ", ld.Wallet != nil) + for i, fn := range ld.Callbacks { + D.Ln("running wallet loader callback", i) + fn(ld.Wallet) + } + D.Ln("wallet loader callbacks finished") + ld.Loaded = true + ld.DB = db + ld.Callbacks = nil // not needed anymore +} + +// NewLoader constructs a Loader with an optional recovery window. If the recovery window is non-zero, the wallet will +// attempt to recovery addresses starting from the last SyncedTo height. +func NewLoader( + chainParams *chaincfg.Params, dbDirPath string, recoveryWindow uint32, +) *Loader { + l := &Loader{ + ChainParams: chainParams, + DDDirPath: dbDirPath, + RecoveryWindow: recoveryWindow, + } + return l +} +func fileExists(filePath string) (bool, error) { + _, e := os.Stat(filePath) + if e != nil { + if os.IsNotExist(e) { + return false, nil + } + return false, e + } + return true, nil +} +func noConsole() ([]byte, error) { + return nil, errNoConsole +} diff --git a/cmd/wallet/log.go b/cmd/wallet/log.go new file mode 100644 index 0000000..b2cde4f --- /dev/null +++ b/cmd/wallet/log.go @@ -0,0 +1,43 @@ +package wallet + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/cmd/wallet/main.go b/cmd/wallet/main.go new file mode 100644 index 0000000..6dacb0f --- /dev/null +++ b/cmd/wallet/main.go @@ -0,0 +1,287 @@ +package wallet + +import ( + "fmt" + // This enables pprof + // _ "net/http/pprof" + "sync" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pod/config" + "github.com/p9c/p9/pod/state" + + "github.com/p9c/p9/pkg/interrupt" + + "github.com/p9c/p9/pkg/chainclient" +) + +// Main is a work-around main function that is required since deferred functions +// (such as log flushing) are not called with calls to os.Exit. Instead, main +// runs this function and checks for a non-nil error, at point any defers have +// already run, and if the error is non-nil, the program can be exited with an +// error exit status. +func Main(cx *state.State) (e error) { + // cx.WaitGroup.Add(1) + cx.WaitAdd() + // if *config.Profile != "" { + // go func() { + // listenAddr := net.JoinHostPort("127.0.0.1", *config.Profile) + // I.Ln("profile server listening on", listenAddr) + // profileRedirect := http.RedirectHandler("/debug/pprof", + // http.StatusSeeOther) + // http.Handle("/", profileRedirect) + // fmt.Println(http.ListenAndServe(listenAddr, nil)) + // }() + // } + loader := NewLoader(cx.ActiveNet, cx.Config.WalletFile.V(), 250) + // Create and start HTTP server to serve wallet client connections. This will be updated with the wallet and chain + // server RPC client created below after each is created. + D.Ln("starting RPC servers") + var legacyServer *Server + if legacyServer, e = startRPCServers(cx, loader); E.Chk(e) { + E.Ln("unable to create RPC servers:", e) + return + } + loader.RunAfterLoad( + func(w *Wallet) { + D.Ln("starting wallet RPC services", w != nil) + startWalletRPCServices(w, legacyServer) + // cx.WalletChan <- w + }, + ) + if !cx.Config.NoInitialLoad.True() { + go func() { + D.Ln("loading wallet", cx.Config.WalletPass.V()) + if e = LoadWallet(loader, cx, legacyServer); E.Chk(e) { + } + }() + } + interrupt.AddHandler(cx.WalletKill.Q) + select { + case <-cx.WalletKill.Wait(): + D.Ln("wallet killswitch activated") + if legacyServer != nil { + D.Ln("stopping wallet RPC server") + legacyServer.Stop() + I.Ln("stopped wallet RPC server") + } + I.Ln("wallet shutdown from killswitch complete") + cx.WaitDone() + return + case <-cx.KillAll.Wait(): + D.Ln("killall") + cx.WalletKill.Q() + case <-interrupt.HandlersDone.Wait(): + } + I.Ln("wallet shutdown complete") + cx.WaitDone() + return +} + +// LoadWallet ... +func LoadWallet( + loader *Loader, cx *state.State, legacyServer *Server, +) (e error) { + T.Ln("starting rpc client connection handler", cx.Config.WalletPass.V()) + // Create and start chain RPC client so it's ready to connect to the wallet when + // loaded later. Load the wallet database. It must have been created already or + // this will return an appropriate error. + var w *Wallet + T.Ln("opening existing wallet, pass:", cx.Config.WalletPass.V()) + if w, e = loader.OpenExistingWallet(cx.Config.WalletPass.Bytes(), true, cx.Config, nil); E.Chk(e) { + T.Ln("failed to open existing wallet") + return + } + T.Ln("opened existing wallet") + // go func() { + // W.Ln("refilling mining addresses", cx.Config, cx.StateCfg) + // addresses.RefillMiningAddresses(w, cx.Config, cx.StateCfg) + // W.Ln("done refilling mining addresses") + // D.S(*cx.Config.MiningAddrs) + // save.Save(cx.Config) + // }() + loader.Wallet = w + // D.Ln("^^^^^^^^^^^ sending back wallet") + // cx.WalletChan <- w + T.Ln("starting rpcClientConnectLoop") + go rpcClientConnectLoop(cx, legacyServer, loader) + T.Ln("adding interrupt handler to unload wallet") + // Add interrupt handlers to shutdown the various process components before + // exiting. Interrupt handlers run in LIFO order, so the wallet (which should be + // closed last) is added first. + interrupt.AddHandler( + func() { + D.Ln("wallet.CtlMain interrupt") + e := loader.UnloadWallet() + if e != nil && e != ErrNotLoaded { + E.Ln("failed to close wallet:", e) + } + }, + ) + if legacyServer != nil { + interrupt.AddHandler( + func() { + D.Ln("stopping wallet RPC server") + legacyServer.Stop() + D.Ln("wallet RPC server shutdown") + }, + ) + } + go func() { + select { + case <-cx.KillAll.Wait(): + case <-legacyServer.RequestProcessShutdownChan().Wait(): + } + interrupt.Request() + }() + return +} + +// rpcClientConnectLoop continuously attempts a connection to the consensus RPC +// server. When a connection is established, the client is used to sync the +// loaded wallet, either immediately or when loaded at a later time. +// +// The legacy RPC is optional. If set, the connected RPC client will be +// associated with the server for RPC pass-through and to enable additional +// methods. +func rpcClientConnectLoop( + cx *state.State, legacyServer *Server, + loader *Loader, +) { + T.Ln("rpcClientConnectLoop", log.Caller("which was started at:", 2)) + // var certs []byte + // if !cx.PodConfig.UseSPV { + certs := cx.Config.ReadCAFile() + // } + for { + var ( + chainClient chainclient.Interface + e error + ) + // if cx.PodConfig.UseSPV { + // var ( + // chainService *neutrino.ChainService + // spvdb walletdb.DB + // ) + // netDir := networkDir(cx.PodConfig.AppDataDir.value, ActiveNet.Params) + // spvdb, e = walletdb.Create("bdb", + // filepath.Join(netDir, "neutrino.db")) + // defer spvdb.Close() + // if e != nil { + // log<-cl.Errorf{"unable to create Neutrino DB: %s", e) + // continue + // } + // chainService, e = neutrino.NewChainService( + // neutrino.Config{ + // DataDir: netDir, + // Database: spvdb, + // ChainParams: *ActiveNet.Params, + // ConnectPeers: cx.PodConfig.ConnectPeers, + // AddPeers: cx.PodConfig.AddPeers, + // }) + // if e != nil { + // log<-cl.Errorf{"couldn't create Neutrino ChainService: %s", e) + // continue + // } + // chainClient = chain.NewNeutrinoClient(ActiveNet.Params, chainService) + // e = chainClient.Start() + // if e != nil { + // log<-cl.Errorf{"couldn't start Neutrino client: %s", e) + // } + // } else { + var cc *chainclient.RPCClient + T.Ln("starting wallet's ChainClient") + cc, e = StartChainRPC(cx.Config, cx.ActiveNet, certs, cx.KillAll) + if e != nil { + E.Ln( + "unable to open connection to consensus RPC server:", e, + ) + continue + } + T.Ln("storing chain client") + cx.ChainClient = cc + cx.ChainClientReady.Q() + chainClient = cc + // Rather than inlining this logic directly into the loader callback, a function + // variable is used to avoid running any of this after the client disconnects by + // setting it to nil. This prevents the callback from associating a wallet + // loaded at a later time with a client that has already disconnected. A mutex + // is used to make this concurrent safe. + associateRPCClient := func(w *Wallet) { + T.Ln("associating chain client") + if w != nil { + w.SynchronizeRPC(chainClient) + } + if legacyServer != nil { + legacyServer.SetChainServer(chainClient) + } + } + T.Ln("adding wallet loader hook to connect to chain") + mu := new(sync.Mutex) + loader.RunAfterLoad( + func(w *Wallet) { + T.Ln("running associate chain client") + mu.Lock() + associate := associateRPCClient + mu.Unlock() + if associate != nil { + associate(w) + T.Ln("wallet is now associated by chain client") + } else { + T.Ln("wallet chain client associate function is nil") + } + }, + ) + chainClient.WaitForShutdown() + mu.Lock() + associateRPCClient = nil + mu.Unlock() + loadedWallet, ok := loader.LoadedWallet() + if ok { + // Do not attempt a reconnect when the wallet was explicitly stopped. + if loadedWallet.ShuttingDown() { + return + } + loadedWallet.SetChainSynced(false) + // TODO: Rework the wallet so changing the RPC client does not + // require stopping and restarting everything. + loadedWallet.Stop() + loadedWallet.WaitForShutdown() + loadedWallet.Start() + } + } +} + +// StartChainRPC opens a RPC client connection to a pod server for blockchain +// services. This function uses the RPC options from the global config and there +// is no recovery in case the server is not available or if there is an +// authentication error. Instead, all requests to the client will simply error. +func StartChainRPC( + config *config.Config, + activeNet *chaincfg.Params, + certs []byte, + quit qu.C, +) (rpcC *chainclient.RPCClient, e error) { + D.F( + "attempting RPC client connection to %v, TLS: %s", + config.RPCConnect.V(), + fmt.Sprint(config.ClientTLS.True()), + ) + if rpcC, e = chainclient.NewRPCClient( + activeNet, + config.RPCConnect.V(), + config.Username.V(), + config.Password.V(), + certs, + config.ClientTLS.True(), + 0, + quit, + ); E.Chk(e) { + return nil, e + } + e = rpcC.Start() + return rpcC, e +} diff --git a/cmd/wallet/methods.go b/cmd/wallet/methods.go new file mode 100644 index 0000000..e8045d2 --- /dev/null +++ b/cmd/wallet/methods.go @@ -0,0 +1,2383 @@ +package wallet + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + js "encoding/json" + "errors" + "fmt" + "sync" + "time" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainclient" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/interrupt" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/txrules" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/wire" + "github.com/p9c/p9/pkg/wtxmgr" +) + +// // confirmed checks whether a transaction at height txHeight has met minconf +// // confirmations for a blockchain at height curHeight. +// func confirmed(// minconf, txHeight, curHeight int32) bool { +// return confirms(txHeight, curHeight) >= minconf +// } + +// Confirms returns the number of confirmations for a transaction in a block at height txHeight (or -1 for an +// unconfirmed tx) given the chain height curHeight. +func Confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +// var RPCHandlers = map[string]struct { +// Handler RequestHandler +// HandlerWithChain RequestHandlerChainRequired +// // Function variables cannot be compared against anything but nil, so +// // use a boolean to record whether help generation is necessary. This +// // is used by the tests to ensure that help can be generated for every +// // implemented method. +// // +// // A single map and this bool is here is used rather than several maps +// // for the unimplemented handlers so every method has exactly one +// // handler function. +// // +// // The Return field returns a new channel of the type returned by this function. This makes it possible to +// // use this for callers to receive a response in the `cpc` library which implements the functions as channel pipes +// NoHelp bool +// Params interface{} +// Return func() interface{} +// }{ +// // Reference implementation wallet methods (implemented) +// "addmultisigaddress": { +// Handler: AddMultiSigAddress, +// Params: make(chan btcjson.AddMultisigAddressCmd), +// Return: func() interface{} { return make(chan AddMultiSigAddressRes) }, +// }, +// "createmultisig": { +// Handler: CreateMultiSig, +// Params: make(chan btcjson.CreateMultisigCmd), +// Return: func() interface{} { return make(chan CreateMultiSigRes) }, +// }, +// "dumpprivkey": { +// Handler: DumpPrivKey, +// Params: make(chan btcjson.DumpPrivKeyCmd), +// Return: func() interface{} { return make(chan DumpPrivKeyRes) }, +// }, +// "getaccount": { +// Handler: GetAccount, +// Params: make(chan btcjson.GetAccountCmd), +// Return: func() interface{} { return make(chan GetAccountRes) }, +// }, +// "getaccountaddress": { +// Handler: GetAccountAddress, +// Params: make(chan btcjson.GetAccountAddressCmd), +// Return: func() interface{} { return make(chan GetAccountAddressRes) }, +// }, +// "getaddressesbyaccount": { +// Handler: GetAddressesByAccount, +// Params: make(chan btcjson.GetAddressesByAccountCmd), +// Return: func() interface{} { return make(chan GetAddressesByAccountRes) }, +// }, +// "getbalance": { +// Handler: GetBalance, +// Params: make(chan btcjson.GetBalanceCmd), +// Return: func() interface{} { return make(chan GetBalanceRes) }, +// }, +// "getbestblockhash": { +// Handler: GetBestBlockHash, +// Return: func() interface{} { return make(chan GetBestBlockHashRes) }, +// }, +// "getblockcount": { +// Handler: GetBlockCount, +// Return: func() interface{} { return make(chan GetBlockCountRes) }, +// }, +// "getinfo": { +// HandlerWithChain: GetInfo, +// Return: func() interface{} { return make(chan GetInfoRes) }, +// }, +// "getnewaddress": { +// Handler: GetNewAddress, +// Params: make(chan btcjson.GetNewAddressCmd), +// Return: func() interface{} { return make(chan GetNewAddressRes) }, +// }, +// "getrawchangeaddress": { +// Handler: GetRawChangeAddress, +// Params: make(chan btcjson.GetRawChangeAddressCmd), +// Return: func() interface{} { return make(chan GetRawChangeAddressRes) }, +// }, +// "getreceivedbyaccount": { +// Handler: GetReceivedByAccount, +// Params: make(chan btcjson.GetReceivedByAccountCmd), +// Return: func() interface{} { return make(chan GetReceivedByAccountRes) }, +// }, +// "getreceivedbyaddress": { +// Handler: GetReceivedByAddress, +// Params: make(chan btcjson.GetReceivedByAddressCmd), +// Return: func() interface{} { return make(chan GetReceivedByAddressRes) }, +// }, +// "gettransaction": { +// Handler: GetTransaction, +// Params: make(chan btcjson.GetTransactionCmd), +// Return: func() interface{} { return make(chan GetTransactionRes) }, +// }, +// "help": { +// Handler: HelpNoChainRPC, +// HandlerWithChain: HelpWithChainRPC, +// Params: make(chan btcjson.HelpCmd), +// Return: func() interface{} { return make(chan HelpNoChainRPCRes) }, +// }, +// "importprivkey": { +// Handler: ImportPrivKey, +// Params: make(chan btcjson.ImportPrivKeyCmd), +// Return: func() interface{} { return make(chan ImportPrivKeyRes) }, +// }, +// "keypoolrefill": { +// Handler: KeypoolRefill, +// Params: qu.T(), +// Return: func() interface{} { return make(chan KeypoolRefillRes) }, +// }, +// "listaccounts": { +// Handler: ListAccounts, +// Params: make(chan btcjson.ListAccountsCmd), +// Return: func() interface{} { return make(chan ListAccountsRes) }, +// }, +// "listlockunspent": { +// Handler: ListLockUnspent, +// Params: qu.T(), +// Return: func() interface{} { return make(chan ListLockUnspentRes) }, +// }, +// "listreceivedbyaccount": { +// Handler: ListReceivedByAccount, +// Params: make(chan btcjson.ListReceivedByAccountCmd), +// Return: func() interface{} { return make(chan ListReceivedByAccountRes) }, +// }, +// "listreceivedbyaddress": { +// Handler: ListReceivedByAddress, +// Params: make(chan btcjson.ListReceivedByAddressCmd), +// Return: func() interface{} { return make(chan ListReceivedByAddressRes) }, +// }, +// "listsinceblock": { +// HandlerWithChain: ListSinceBlock, +// Params: make(chan btcjson.ListSinceBlockCmd), +// Return: func() interface{} { return make(chan ListSinceBlockRes) }, +// }, +// "listtransactions": { +// Handler: ListTransactions, +// Params: make(chan btcjson.ListTransactionsCmd), +// Return: func() interface{} { return make(chan ListTransactionsRes) }, +// }, +// "listunspent": { +// Handler: ListUnspent, +// Params: make(chan btcjson.ListUnspentCmd), +// Return: func() interface{} { return make(chan ListUnspentRes) }, +// }, +// "lockunspent": { +// Handler: LockUnspent, +// Params: make(chan btcjson.LockUnspentCmd), +// Return: func() interface{} { return make(chan LockUnspentRes) }, +// }, +// "sendfrom": { +// HandlerWithChain: SendFrom, +// Params: make(chan btcjson.SendFromCmd), +// Return: func() interface{} { return make(chan SendFromRes) }, +// }, +// "sendmany": { +// Handler: SendMany, +// Params: make(chan btcjson.SendManyCmd), +// Return: func() interface{} { return make(chan SendManyRes) }, +// }, +// "sendtoaddress": { +// Handler: SendToAddress, +// Params: make(chan btcjson.SendToAddressCmd), +// Return: func() interface{} { return make(chan SendToAddressRes) }, +// }, +// "settxfee": { +// Handler: SetTxFee, +// Params: make(chan btcjson.SetTxFeeCmd), +// Return: func() interface{} { return make(chan SetTxFeeRes) }, +// }, +// "signmessage": { +// Handler: SignMessage, +// Params: make(chan btcjson.SignMessageCmd), +// Return: func() interface{} { return make(chan SignMessageRes) }, +// }, +// "signrawtransaction": { +// HandlerWithChain: SignRawTransaction, +// Params: make(chan btcjson.SignRawTransactionCmd), +// Return: func() interface{} { return make(chan SignRawTransactionRes) }, +// }, +// "validateaddress": { +// Handler: ValidateAddress, +// Params: make(chan btcjson.ValidateAddressCmd), +// Return: func() interface{} { return make(chan ValidateAddressRes) }, +// }, +// "verifymessage": { +// Handler: VerifyMessage, +// Params: make(chan btcjson.VerifyMessageCmd), +// Return: func() interface{} { return make(chan VerifyMessageRes) }, +// }, +// "walletlock": { +// Handler: WalletLock, +// Params: qu.T(), +// Return: func() interface{} { return make(chan WalletLockRes) }, +// }, +// "walletpassphrase": { +// Handler: WalletPassphrase, +// Params: make(chan btcjson.WalletPassphraseCmd), +// Return: func() interface{} { return make(chan WalletPassphraseRes) }, +// }, +// "walletpassphrasechange": { +// Handler: WalletPassphraseChange, +// Params: make(chan btcjson.WalletPassphraseChangeCmd), +// Return: func() interface{} { return make(chan WalletPassphraseChangeRes) }, +// }, +// // Reference implementation methods (still unimplemented) +// "backupwallet": {Handler: Unimplemented, NoHelp: true}, +// "dumpwallet": {Handler: Unimplemented, NoHelp: true}, +// "getwalletinfo": {Handler: Unimplemented, NoHelp: true}, +// "importwallet": {Handler: Unimplemented, NoHelp: true}, +// "listaddressgroupings": {Handler: Unimplemented, NoHelp: true}, +// // Reference methods which can't be implemented by btcwallet due to +// // design decision differences +// "encryptwallet": {Handler: Unsupported, NoHelp: true}, +// "move": {Handler: Unsupported, NoHelp: true}, +// "setaccount": {Handler: Unsupported, NoHelp: true}, +// // Extensions to the reference client JSON-RPC API +// "createnewaccount": { +// Handler: CreateNewAccount, +// Params: make(chan btcjson.CreateNewAccountCmd), +// Return: func() interface{} { return make(chan CreateNewAccountRes) }, +// }, +// "getbestblock": { +// Handler: GetBestBlock, +// Params: qu.T(), +// Return: func() interface{} { return make(chan GetBestBlockRes) }, +// }, +// // This was an extension but the reference implementation added it as +// // well, but with a different API (no account parameter). It's listed +// // here because it hasn't been update to use the reference +// // implemenation's API. +// "getunconfirmedbalance": { +// Handler: GetUnconfirmedBalance, +// Params: make(chan btcjson.GetUnconfirmedBalanceCmd), +// Return: func() interface{} { return make(chan GetUnconfirmedBalanceRes) }, +// }, +// "listaddresstransactions": { +// Handler: ListAddressTransactions, +// Params: make(chan btcjson.ListAddressTransactionsCmd), +// Return: func() interface{} { return make(chan ListAddressTransactionsRes) }, +// }, +// "listalltransactions": { +// Handler: ListAllTransactions, +// Params: make(chan btcjson.ListAllTransactionsCmd), +// Return: func() interface{} { return make(chan ListAllTransactionsRes) }, +// }, +// "renameaccount": { +// Handler: RenameAccount, +// Params: make(chan btcjson.RenameAccountCmd), +// Return: func() interface{} { return make(chan RenameAccountRes) }, +// }, +// "walletislocked": { +// Handler: WalletIsLocked, +// Params: qu.T(), +// Return: func() interface{} { return make(chan WalletIsLockedRes) }, +// }, +// "dropwallethistory": { +// Handler: HandleDropWalletHistory, +// Params: qu.T(), +// Return: func() interface{} { return make(chan DropWalletHistoryRes) }, +// }, +// } + +// Unimplemented handles an Unimplemented RPC request with the +// appropiate error. +func Unimplemented(interface{}, *Wallet) (interface{}, error) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Method unimplemented", + } +} + +// Unsupported handles a standard bitcoind RPC request which is Unsupported by btcwallet due to design differences. +func Unsupported(interface{}, *Wallet) (interface{}, error) { + return nil, &btcjson.RPCError{ + Code: -1, + Message: "Request unsupported by wallet", + } +} + +// LazyHandler is a closure over a requestHandler or passthrough request with the RPC server's wallet and chain server +// variables as part of the closure context. +type LazyHandler func() (interface{}, *btcjson.RPCError) + +// LazyApplyHandler looks up the best request handler func for the method, returning a closure that will execute it with +// the (required) wallet and (optional) consensus RPC server. If no handlers are found and the chainClient is not nil, +// the returned handler performs RPC passthrough. +func LazyApplyHandler(request *btcjson.Request, w *Wallet, chainClient chainclient.Interface) LazyHandler { + handlerData, ok := RPCHandlers[request.Method] + D.Ln("LazyApplyHandler >>> >>> >>>", ok, handlerData.Handler != nil, w != nil, chainClient != nil) + if ok && handlerData.Handler != nil && w != nil && chainClient != nil { + D.Ln("found handler for call") + // D.S(request) + return func() (interface{}, *btcjson.RPCError) { + cmd, e := btcjson.UnmarshalCmd(request) + if e != nil { + return nil, btcjson.ErrRPCInvalidRequest + } + switch client := chainClient.(type) { + case *chainclient.RPCClient: + D.Ln("client is a chain.RPCClient") + var resp interface{} + if resp, e = handlerData.Handler(cmd, w, client); E.Chk(e) { + return nil, JSONError(e) + } + D.Ln("handler call succeeded") + return resp, nil + default: + D.Ln("client is unknown") + return nil, &btcjson.RPCError{ + Code: -1, + Message: "Chain RPC is inactive", + } + } + } + } + D.Ln("failed to find handler for call") + // I.Ln("handler", handlerData.Handler, "wallet", w) + if ok && handlerData.Handler != nil && w != nil { + D.Ln("handling", request.Method) + return func() (interface{}, *btcjson.RPCError) { + cmd, e := btcjson.UnmarshalCmd(request) + if e != nil { + return nil, btcjson.ErrRPCInvalidRequest + } + var resp interface{} + if resp, e = handlerData.Handler(cmd, w); E.Chk(e) { + return nil, JSONError(e) + } + return resp, nil + } + } + // Fallback to RPC passthrough + return func() (interface{}, *btcjson.RPCError) { + I.Ln("passing to node", request.Method) + if chainClient == nil { + return nil, &btcjson.RPCError{ + Code: -1, + Message: "Chain RPC is inactive", + } + } + switch client := chainClient.(type) { + case *chainclient.RPCClient: + resp, e := client.RawRequest( + request.Method, + request.Params, + ) + if e != nil { + return nil, JSONError(e) + } + return &resp, nil + default: + return nil, &btcjson.RPCError{ + Code: -1, + Message: "Chain RPC is inactive", + } + } + } +} + +// MakeResponse makes the JSON-RPC response struct for the result and error returned by a requestHandler. The returned +// response is not ready for marshaling and sending off to a client, but must be +func MakeResponse(id, result interface{}, e error) btcjson.Response { + idPtr := IDPointer(id) + if e != nil { + return btcjson.Response{ + ID: idPtr, + Error: JSONError(e), + } + } + var resultBytes []byte + resultBytes, e = js.Marshal(result) + if e != nil { + return btcjson.Response{ + ID: idPtr, + Error: &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, + Message: "Unexpected error marshalling result", + }, + } + } + return btcjson.Response{ + ID: idPtr, + Result: resultBytes, + } +} + +// JSONError creates a JSON-RPC error from the Go error. +func JSONError(e error) *btcjson.RPCError { + if e == nil { + return nil + } + code := btcjson.ErrRPCWallet + switch e := e.(type) { + case btcjson.RPCError: + return &e + case *btcjson.RPCError: + return e + case DeserializationError: + code = btcjson.ErrRPCDeserialization + case InvalidParameterError: + code = btcjson.ErrRPCInvalidParameter + case ParseError: + code = btcjson.ErrRPCParse.Code + case waddrmgr.ManagerError: + switch e.ErrorCode { + case waddrmgr.ErrWrongPassphrase: + code = btcjson.ErrRPCWalletPassphraseIncorrect + } + } + return &btcjson.RPCError{ + Code: code, + Message: e.Error(), + } +} + +// MakeMultiSigScript is a helper function to combine common logic for AddMultiSig and CreateMultiSig. +func MakeMultiSigScript(w *Wallet, keys []string, nRequired int) ([]byte, error) { + keysesPrecious := make([]*btcaddr.PubKey, len(keys)) + // The address list will made up either of addresses (pubkey hash), for which we need to look up the keys in + // wallet, straight pubkeys, or a mixture of the two. + for i, a := range keys { + // try to parse as pubkey address + a, e := DecodeAddress(a, w.ChainParams()) + if e != nil { + return nil, e + } + switch addr := a.(type) { + case *btcaddr.PubKey: + keysesPrecious[i] = addr + default: + pubKey, e := w.PubKeyForAddress(addr) + if e != nil { + return nil, e + } + pubKeyAddr, e := btcaddr.NewPubKey( + pubKey.SerializeCompressed(), w.ChainParams(), + ) + if e != nil { + return nil, e + } + keysesPrecious[i] = pubKeyAddr + } + } + return txscript.MultiSigScript(keysesPrecious, nRequired) +} + +// AddMultiSigAddress handles an addmultisigaddress request by adding a +// multisig address to the given wallet. +func AddMultiSigAddress(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + interface{}, + error, +) { + var msg string + cmd, ok := icmd.(*btcjson.AddMultisigAddressCmd) + // cmd, ok := icmd.(*btcjson.ListTransactionsCmd) + if !ok { + var h string + h = HelpDescsEnUS()["addmultisigaddress"] + msg += h + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: msg, + // "invalid subcommand for addnode", + } + } + // If an account is specified, ensure that is the imported account. + if cmd.Account != nil && *cmd.Account != waddrmgr.ImportedAddrAccountName { + return nil, &ErrNotImportedAccount + } + secp256k1Addrs := make([]btcaddr.Address, len(cmd.Keys)) + for i, k := range cmd.Keys { + addr, e := DecodeAddress(k, w.ChainParams()) + if e != nil { + return nil, ParseError{e} + } + secp256k1Addrs[i] = addr + } + script, e := w.MakeMultiSigScript(secp256k1Addrs, cmd.NRequired) + if e != nil { + return nil, e + } + p2shAddr, e := w.ImportP2SHRedeemScript(script) + if e != nil { + return nil, e + } + return p2shAddr.EncodeAddress(), nil +} + +// CreateMultiSig handles an createmultisig request by returning a multisig address for the given inputs. +func CreateMultiSig(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + var msg string + cmd, ok := icmd.(*btcjson.CreateMultisigCmd) + if !ok { + var h string + h = HelpDescsEnUS()["createmultisig"] + msg += h + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: msg, + // "invalid subcommand for addnode", + } + } + script, e := MakeMultiSigScript(w, cmd.Keys, cmd.NRequired) + if e != nil { + return nil, ParseError{e} + } + address, e := btcaddr.NewScriptHash(script, w.ChainParams()) + if e != nil { + // above is a valid script, shouldn't happen. + return nil, e + } + return btcjson.CreateMultiSigResult{ + Address: address.EncodeAddress(), + RedeemScript: hex.EncodeToString(script), + }, nil +} + +// DumpPrivKey handles a dumpprivkey request with the private key for a single address, or an appropriate error if the +// wallet is locked. +func DumpPrivKey(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + var msg string + cmd, ok := icmd.(*btcjson.DumpPrivKeyCmd) + if !ok { + msg = HelpDescsEnUS()["dumpprivkey"] + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: msg, + // "invalid subcommand for addnode", + } + } + addr, e := DecodeAddress(cmd.Address, w.ChainParams()) + if e != nil { + return nil, e + } + key, e := w.DumpWIFPrivateKey(addr) + if waddrmgr.IsError(e, waddrmgr.ErrLocked) { + // Address was found, but the private key isn't accessible. + return nil, &ErrWalletUnlockNeeded + } + return key, e +} + +// // dumpWallet handles a dumpwallet request by returning all private +// // keys in a wallet, or an appropiate error if the wallet is locked. +// // TODO: finish this to match bitcoind by writing the dump to a file. +// func dumpWallet(// icmd interface{}, w *wallet.Wallet) (interface{}, error) { +// keys, e := w.DumpPrivKeys() +// if waddrmgr.IsError(err, waddrmgr.ErrLocked) { +// return nil, &ErrWalletUnlockNeeded +// } +// return keys, err +// } + +// GetAddressesByAccount handles a getaddressesbyaccount request by returning +// all addresses for an account, or an error if the requested account does not +// exist. +func GetAddressesByAccount(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + interface{}, + error, +) { + cmd, ok := icmd.(*btcjson.GetAddressesByAccountCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["dumpprivkey"], + // "invalid subcommand for addnode", + } + } + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, cmd.Account) + if e != nil { + return nil, e + } + addrs, e := w.AccountAddresses(account) + if e != nil { + return nil, e + } + addrStrs := make([]string, len(addrs)) + for i, a := range addrs { + addrStrs[i] = a.EncodeAddress() + } + return addrStrs, nil +} + +// GetBalance handles a getbalance request by returning the balance for an +// account (wallet), or an error if the requested account does not exist. +func GetBalance(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.GetBalanceCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getbalance"], + // "invalid subcommand for addnode", + } + } + var balance amt.Amount + var e error + accountName := "*" + if cmd.Account != nil { + accountName = *cmd.Account + } + if accountName == "*" { + balance, e = w.CalculateBalance(int32(*cmd.MinConf)) + if e != nil { + return nil, e + } + } else { + var account uint32 + account, e = w.AccountNumber(waddrmgr.KeyScopeBIP0044, accountName) + if e != nil { + return nil, e + } + bals, e := w.CalculateAccountBalances(account, int32(*cmd.MinConf)) + if e != nil { + return nil, e + } + balance = bals.Spendable + } + return balance.ToDUO(), nil +} + +// GetBestBlock handles a getbestblock request by returning a JSON object with +// the height and hash of the most recently processed block. +func GetBestBlock(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + blk := w.Manager.SyncedTo() + result := &btcjson.GetBestBlockResult{ + Hash: blk.Hash.String(), + Height: blk.Height, + } + return result, nil +} + +// GetBestBlockHash handles a getbestblockhash request by returning the hash of +// the most recently processed block. +func GetBestBlockHash(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + blk := w.Manager.SyncedTo() + return blk.Hash.String(), nil +} + +// GetBlockCount handles a getblockcount request by returning the chain height +// of the most recently processed block. +func GetBlockCount(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + blk := w.Manager.SyncedTo() + return blk.Height, nil +} + +// GetInfo handles a getinfo request by returning the a structure containing +// information about the current state of btcwallet. exist. +func GetInfo(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + if len(chainClient) < 1 || chainClient[0] == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoChain, + Message: "there is currently no chain client to get this response", + } + } + // Call down to pod for all of the information in this command known by them. + var info *btcjson.InfoWalletResult + var e error + info, e = chainClient[0].GetInfo() + if e != nil { + return nil, e + } + var bal amt.Amount + bal, e = w.CalculateBalance(1) + if e != nil { + return nil, e + } + // TODO(davec): This should probably have a database version as opposed + // to using the manager version. + info.WalletVersion = int32(waddrmgr.LatestMgrVersion) + info.Balance = bal.ToDUO() + info.PaytxFee = float64(txrules.DefaultRelayFeePerKb) + // We don't set the following since they don't make much sense in the wallet architecture: + // + // - unlocked_until + // - errors + return info, nil +} + +func DecodeAddress(s string, params *chaincfg.Params) (btcaddr.Address, error) { + addr, e := btcaddr.Decode(s, params) + if e != nil { + msg := fmt.Sprintf("Invalid address %q: decode failed with %#q", s, e) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: msg, + } + } + if !addr.IsForNet(params) { + msg := fmt.Sprintf( + "Invalid address %q: not intended for use on %s", + addr, params.Name, + ) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: msg, + } + } + return addr, nil +} + +// GetAccount handles a getaccount request by returning the account name +// associated with a single address. +func GetAccount( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.GetAccountCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getaccount"], + // "invalid subcommand for addnode", + } + } + addr, e := DecodeAddress(cmd.Address, w.ChainParams()) + if e != nil { + return nil, e + } + // Fetch the associated account + account, e := w.AccountOfAddress(addr) + if e != nil { + return nil, &ErrAddressNotInWallet + } + acctName, e := w.AccountName(waddrmgr.KeyScopeBIP0044, account) + if e != nil { + return nil, &ErrAccountNameNotFound + } + return acctName, nil +} + +// GetAccountAddress handles a getaccountaddress by returning the most +// recently-created chained address that has not yet been used (does not yet +// appear in the blockchain, or any tx that has arrived in the pod mempool). +// +// If the most recently-requested address has been used, a new address (the next +// chained address in the keypool) is used. This can fail if the keypool runs +// out (and will return json.ErrRPCWalletKeypoolRanOut if that happens). +func GetAccountAddress(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.GetAccountAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getaccountaddress"], + // "invalid subcommand for addnode", + } + } + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, cmd.Account) + if e != nil { + return nil, e + } + addr, e := w.CurrentAddress(account, waddrmgr.KeyScopeBIP0044) + if e != nil { + return nil, e + } + return addr.EncodeAddress(), e +} + +// GetUnconfirmedBalance handles a getunconfirmedbalance extension request by +// returning the current unconfirmed balance of an account. +func GetUnconfirmedBalance(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + interface{}, + error, +) { + cmd, ok := icmd.(*btcjson.GetUnconfirmedBalanceCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getunconfirmedbalance"], + // "invalid subcommand for addnode", + } + } + acctName := "default" + if cmd.Account != nil { + acctName = *cmd.Account + } + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, acctName) + if e != nil { + return nil, e + } + bals, e := w.CalculateAccountBalances(account, 1) + if e != nil { + return nil, e + } + return (bals.Total - bals.Spendable).ToDUO(), nil +} + +// ImportPrivKey handles an importprivkey request by parsing a WIF-encoded +// private key and adding it to an account. +func ImportPrivKey(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ImportPrivKeyCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["importprivkey"], + // "invalid subcommand for addnode", + } + } + // Ensure that private keys are only imported to the correct account. + // + // Yes, Label is the account name. + if cmd.Label != nil && *cmd.Label != waddrmgr.ImportedAddrAccountName { + return nil, &ErrNotImportedAccount + } + wif, e := util.DecodeWIF(cmd.PrivKey) + if e != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "WIF decode failed: " + e.Error(), + } + } + if !wif.IsForNet(w.ChainParams()) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Key is not intended for " + w.ChainParams().Name, + } + } + // Import the private key, handling any errors. + _, e = w.ImportPrivateKey(waddrmgr.KeyScopeBIP0044, wif, nil, *cmd.Rescan) + switch { + case waddrmgr.IsError(e, waddrmgr.ErrDuplicateAddress): + // Do not return duplicate key errors to the client. + return nil, nil + case waddrmgr.IsError(e, waddrmgr.ErrLocked): + return nil, &ErrWalletUnlockNeeded + } + return nil, e +} + +// KeypoolRefill handles the keypoolrefill command. Since we handle the keypool automatically this does nothing since +// refilling is never manually required. +func KeypoolRefill( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + return nil, nil +} + +// CreateNewAccount handles a createnewaccount request by creating and returning a new account. If the last account has +// no transaction history as per BIP 0044 a new account cannot be created so an error will be returned. +func CreateNewAccount(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.CreateNewAccountCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["createnewaccount"], + // "invalid subcommand for addnode", + } + } + // The wildcard * is reserved by the rpc server with the special meaning of "all + // accounts", so disallow naming accounts to this string. + if cmd.Account == "*" { + return nil, &ErrReservedAccountName + } + _, e := w.NextAccount(waddrmgr.KeyScopeBIP0044, cmd.Account) + if waddrmgr.IsError(e, waddrmgr.ErrLocked) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCWalletUnlockNeeded, + Message: "Creating an account requires the wallet to be unlocked. " + + "Enter the wallet passphrase with walletpassphrase to unlock", + } + } + return nil, e +} + +// RenameAccount handles a renameaccount request by renaming an account. If the +// account does not exist an appropiate error will be returned. +func RenameAccount(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.RenameAccountCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["renameaccount"], + // "invalid subcommand for addnode", + } + } + // The wildcard * is reserved by the rpc server with the special meaning of "all + // accounts", so disallow naming accounts to this string. + if cmd.NewAccount == "*" { + return nil, &ErrReservedAccountName + } + // Chk that given account exists + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, cmd.OldAccount) + if e != nil { + return nil, e + } + return nil, w.RenameAccount(waddrmgr.KeyScopeBIP0044, account, cmd.NewAccount) +} + +// GetNewAddress handles a getnewaddress request by returning a new address for +// an account. If the account does not exist an appropiate error is returned. +// +// TODO: Follow BIP 0044 and warn if number of unused addresses exceeds the gap limit. +func GetNewAddress(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.GetNewAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getnewaddress"], + // "invalid subcommand for addnode", + } + } + acctName := "default" + if cmd.Account != nil { + acctName = *cmd.Account + } + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, acctName) + if e != nil { + return nil, e + } + addr, e := w.NewAddress(account, waddrmgr.KeyScopeBIP0044, false) + if e != nil { + return nil, e + } + // Return the new payment address string. + return addr.EncodeAddress(), nil +} + +// GetRawChangeAddress handles a getrawchangeaddress request by creating and +// returning a new change address for an account. +// +// Note: bitcoind allows specifying the account as an optional parameter, but +// ignores the parameter. +func GetRawChangeAddress(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + interface{}, + error, +) { + cmd, ok := icmd.(*btcjson.GetRawChangeAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getrawchangeaddress"], + // "invalid subcommand for addnode", + } + } + acctName := "default" + if cmd.Account != nil { + acctName = *cmd.Account + } + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, acctName) + if e != nil { + return nil, e + } + addr, e := w.NewChangeAddress(account, waddrmgr.KeyScopeBIP0044) + if e != nil { + return nil, e + } + // Return the new payment address string. + return addr.EncodeAddress(), nil +} + +// GetReceivedByAccount handles a getreceivedbyaccount request by returning the +// total amount received by addresses of an account. +func GetReceivedByAccount(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + ii interface{}, + e error, +) { + cmd, ok := icmd.(*btcjson.GetReceivedByAccountCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getreceivedbyaccount"], + // "invalid subcommand for addnode", + } + } + var account uint32 + account, e = w.AccountNumber(waddrmgr.KeyScopeBIP0044, cmd.Account) + if e != nil { + return nil, e + } + // TODO: This is more inefficient that it could be, but the entire algorithm is + // already dominated by reading every transaction in the wallet's history. + var results []AccountTotalReceivedResult + results, e = w.TotalReceivedForAccounts( + waddrmgr.KeyScopeBIP0044, int32(*cmd.MinConf), + ) + if e != nil { + return nil, e + } + acctIndex := int(account) + if account == waddrmgr.ImportedAddrAccount { + acctIndex = len(results) - 1 + } + return results[acctIndex].TotalReceived.ToDUO(), nil +} + +// GetReceivedByAddress handles a getreceivedbyaddress request by returning the total amount received by a single +// address. +func GetReceivedByAddress(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + interface{}, + error, +) { + cmd, ok := icmd.(*btcjson.GetReceivedByAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["getreceivedbyaddress"], + // "invalid subcommand for addnode", + } + } + addr, e := DecodeAddress(cmd.Address, w.ChainParams()) + if e != nil { + return nil, e + } + total, e := w.TotalReceivedForAddr(addr, int32(*cmd.MinConf)) + if e != nil { + return nil, e + } + return total.ToDUO(), nil +} + +// GetTransaction handles a gettransaction request by returning details about a single transaction saved by wallet. +func GetTransaction(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.GetTransactionCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["gettransaction"], + // "invalid subcommand for addnode", + } + } + txHash, e := chainhash.NewHashFromStr(cmd.Txid) + if e != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDecodeHexString, + Message: "Transaction hash string decode failed: " + e.Error(), + } + } + details, e := ExposeUnstableAPI(w).TxDetails(txHash) + if e != nil { + return nil, e + } + if details == nil { + return nil, &ErrNoTransactionInfo + } + syncBlock := w.Manager.SyncedTo() + // TODO: The serialized transaction is already in the DB, so + // reserializing can be avoided here. + var txBuf bytes.Buffer + txBuf.Grow(details.MsgTx.SerializeSize()) + e = details.MsgTx.Serialize(&txBuf) + if e != nil { + return nil, e + } + // TODO: Add a "generated" field to this result type. "generated":true + // is only added if the transaction is a coinbase. + ret := btcjson.GetTransactionResult{ + TxID: cmd.Txid, + Hex: hex.EncodeToString(txBuf.Bytes()), + Time: details.Received.Unix(), + TimeReceived: details.Received.Unix(), + WalletConflicts: []string{}, // Not saved + // Generated: blockchain.IsCoinBaseTx(&details.MsgTx), + } + if details.Block.Height != -1 { + ret.BlockHash = details.Block.Hash.String() + ret.BlockTime = details.Block.Time.Unix() + ret.Confirmations = int64(Confirms(details.Block.Height, syncBlock.Height)) + } + var ( + debitTotal amt.Amount + creditTotal amt.Amount // Excludes change + fee amt.Amount + feeF64 float64 + ) + for _, deb := range details.Debits { + debitTotal += deb.Amount + } + for _, cred := range details.Credits { + if !cred.Change { + creditTotal += cred.Amount + } + } + // Fee can only be determined if every input is a debit. + if len(details.Debits) == len(details.MsgTx.TxIn) { + var outputTotal amt.Amount + for _, output := range details.MsgTx.TxOut { + outputTotal += amt.Amount(output.Value) + } + fee = debitTotal - outputTotal + feeF64 = fee.ToDUO() + } + if len(details.Debits) == 0 { + // Credits must be set later, but since we know the full length + // of the details slice, allocate it with the correct cap. + ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(details.Credits)) + } else { + ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(details.Credits)+1) + ret.Details[0] = btcjson.GetTransactionDetailsResult{ + // Fields left zeroed: + // InvolvesWatchOnly + // Account + // Address + // VOut + // + // TODO(jrick): Address and VOut should always be set, + // but we're doing the wrong thing here by not matching + // core. Instead, gettransaction should only be adding + // details for transaction outputs, just like + // listtransactions (but using the short result format). + Category: "send", + Amount: (-debitTotal).ToDUO(), // negative since it is a send + Fee: &feeF64, + } + ret.Fee = feeF64 + } + credCat := RecvCategory(details, syncBlock.Height, w.ChainParams()).String() + for _, cred := range details.Credits { + // Change is ignored. + if cred.Change { + continue + } + var address string + var accountName string + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + details.MsgTx.TxOut[cred.Index].PkScript, w.ChainParams(), + ) + if e == nil && len(addrs) == 1 { + addr := addrs[0] + address = addr.EncodeAddress() + account, e := w.AccountOfAddress(addr) + if e == nil { + name, e := w.AccountName(waddrmgr.KeyScopeBIP0044, account) + if e == nil { + accountName = name + } + } + } + ret.Details = append( + ret.Details, btcjson.GetTransactionDetailsResult{ + // Fields left zeroed: + // InvolvesWatchOnly + // Fee + Account: accountName, + Address: address, + Category: credCat, + Amount: cred.Amount.ToDUO(), + Vout: cred.Index, + }, + ) + } + ret.Amount = creditTotal.ToDUO() + return ret, nil +} + +func HandleDropWalletHistory(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + out interface{}, e error, +) { + D.Ln("dropping wallet history") + if e = DropWalletHistory(w, w.PodConfig); E.Chk(e) { + } + D.Ln("dropped wallet history") + // go func() { + // rwt, e := w.Database().BeginReadWriteTx() + // if e != nil { + // L.Script // } + // ns := rwt.ReadWriteBucket([]byte("waddrmgr")) + // w.Manager.SetSyncedTo(ns, nil) + // if e = rwt.Commit(); E.Chk(e) { + // } + // }() + defer interrupt.RequestRestart() + return nil, e +} + +// These generators create the following global variables in this package: +// +// var localeHelpDescs map[string]func() map[string]string +// var requestUsages string +// +// localeHelpDescs maps from locale strings (e.g. "en_US") to a function that builds a map of help texts for each RPC +// server method. This prevents help text maps for every locale map from being rooted and created during init. Instead, +// the appropiate function is looked up when help text is first needed using the current locale and saved to the global +// below for futher reuse. +// +// requestUsages contains single line usages for every supported request, separated by newlines. It is set during init. +// These usages are used for all locales. +// + +var HelpDescs map[string]string +var HelpDescsMutex sync.Mutex // Help may execute concurrently, so synchronize access. + +// HelpWithChainRPC handles the help request when the RPC server has been associated with a consensus RPC client. The +// additional RPC client is used to include help messages for methods implemented by the consensus server via RPC +// passthrough. +func HelpWithChainRPC( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + return Help(icmd, w, chainClient[0]) +} + +// HelpNoChainRPC handles the help request when the RPC server has not been associated with a consensus RPC client. No +// help messages are included for passthrough requests. +func HelpNoChainRPC( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + return Help(icmd, w, nil) +} + +// Help handles the Help request by returning one line usage of all available methods, or full Help for a specific +// method. The chainClient is optional, and this is simply a helper function for the HelpNoChainRPC and HelpWithChainRPC +// handlers. +func Help(icmd interface{}, w *Wallet, chainClient *chainclient.RPCClient) (interface{}, error) { + cmd := icmd.(*btcjson.HelpCmd) + // pod returns different help messages depending on the kind of connection the client is using. Only methods + // availble to HTTP POST clients are available to be used by wallet clients, even though wallet itself is a + // websocket client to pod. Therefore, create a POST client as needed. + // + // Returns nil if chainClient is currently nil or there is an error creating the client. + // + // This is hacky and is probably better handled by exposing help usage texts in a non-internal pod package. + postClient := func() *rpcclient.Client { + if chainClient == nil { + return nil + } + c, e := chainClient.POSTClient() + if e != nil { + return nil + } + return c + } + if cmd.Command == nil || *cmd.Command == "" { + // Prepend chain server usage if it is available. + usages := RequestUsages + client := postClient() + if client != nil { + rawChainUsage, e := client.RawRequest("help", nil) + var chainUsage string + if e == nil { + _ = js.Unmarshal(rawChainUsage, &chainUsage) + } + if chainUsage != "" { + usages = "Chain server usage:\n\n" + chainUsage + "\n\n" + + "Wallet server usage (overrides chain requests):\n\n" + + RequestUsages + } + } + return usages, nil + } + defer HelpDescsMutex.Unlock() + HelpDescsMutex.Lock() + if HelpDescs == nil { + // TODO: Allow other locales to be set via config or determine this from environment variables. For now, + // hardcode US English. + HelpDescs = LocaleHelpDescs["en_US"]() + } + helpText, ok := HelpDescs[*cmd.Command] + if ok { + return helpText, nil + } + // Return the chain server's detailed help if possible. + var chainHelp string + client := postClient() + if client != nil { + param := make([]byte, len(*cmd.Command)+2) + param[0] = '"' + copy(param[1:], *cmd.Command) + param[len(param)-1] = '"' + rawChainHelp, e := client.RawRequest("help", []js.RawMessage{param}) + if e == nil { + _ = js.Unmarshal(rawChainHelp, &chainHelp) + } + } + if chainHelp != "" { + return chainHelp, nil + } + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: fmt.Sprintf("No help for method '%s'", *cmd.Command), + } +} + +// ListAccounts handles a listaccounts request by returning a map of account names to their balances. +func ListAccounts( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ListAccountsCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listaccounts"], + // "invalid subcommand for addnode", + } + } + accountBalances := map[string]float64{} + results, e := w.AccountBalances(waddrmgr.KeyScopeBIP0044, int32(*cmd.MinConf)) + if e != nil { + return nil, e + } + for _, result := range results { + accountBalances[result.AccountName] = result.AccountBalance.ToDUO() + } + // Return the map. This will be marshaled into a JSON object. + return accountBalances, nil +} + +// ListLockUnspent handles a listlockunspent request by returning an slice of all locked outpoints. +func ListLockUnspent( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + return w.LockedOutpoints(), nil +} + +// ListReceivedByAccount handles a listreceivedbyaccount request by returning a slice of objects, each one containing: +// +// "account": the receiving account; +// +// "amount": total amount received by the account; +// +// "confirmations": number of confirmations of the most recent transaction. +// +// It takes two parameters: +// +// "minconf": minimum number of confirmations to consider a transaction - default: one; +// +// "includeempty": whether or not to include addresses that have no transactions - default: false. +func ListReceivedByAccount( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ListReceivedByAccountCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listreceivedbyaccount"], + // "invalid subcommand for addnode", + } + } + results, e := w.TotalReceivedForAccounts( + waddrmgr.KeyScopeBIP0044, int32(*cmd.MinConf), + ) + if e != nil { + return nil, e + } + jsonResults := make([]btcjson.ListReceivedByAccountResult, 0, len(results)) + for _, result := range results { + jsonResults = append( + jsonResults, btcjson.ListReceivedByAccountResult{ + Account: result.AccountName, + Amount: result.TotalReceived.ToDUO(), + Confirmations: uint64(result.LastConfirmation), + }, + ) + } + return jsonResults, nil +} + +// ListReceivedByAddress handles a listreceivedbyaddress request by returning +// a slice of objects, each one containing: +// +// "account": the account of the receiving address; +// +// "address": the receiving address; +// +// "amount": total amount received by the address; +// +// "confirmations": number of confirmations of the most recent transaction. +// +// It takes two parameters: +// +// "minconf": minimum number of confirmations to consider a transaction - default: one; +// +// "includeempty": whether or not to include addresses that have no transactions - default: false. +func ListReceivedByAddress( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ListReceivedByAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listreceivedbyaddress"], + // "invalid subcommand for addnode", + } + } + // Intermediate data for each address. + type AddrData struct { + // Total amount received. + amount amt.Amount + // Number of confirmations of the last transaction. + confirmations int32 + // Merkles of transactions which include an output paying to the address + tx []string + // Account which the address belongs to account string + } + syncBlock := w.Manager.SyncedTo() + // Intermediate data for all addresses. + allAddrData := make(map[string]AddrData) + // Create an AddrData entry for each active address in the account. Otherwise we'll just get addresses from + // transactions later. + sortedAddrs, e := w.SortedActivePaymentAddresses() + if e != nil { + return nil, e + } + for _, address := range sortedAddrs { + // There might be duplicates, just overwrite them. + allAddrData[address] = AddrData{} + } + minConf := *cmd.MinConf + var endHeight int32 + if minConf == 0 { + endHeight = -1 + } else { + endHeight = syncBlock.Height - int32(minConf) + 1 + } + e = ExposeUnstableAPI(w).RangeTransactions( + 0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) { + confirmations := Confirms(details[0].Block.Height, syncBlock.Height) + for _, tx := range details { + for _, cred := range tx.Credits { + pkScript := tx.MsgTx.TxOut[cred.Index].PkScript + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + pkScript, w.ChainParams(), + ) + if e != nil { + // Non standard script, skip. + continue + } + for _, addr := range addrs { + addrStr := addr.EncodeAddress() + addrData, ok := allAddrData[addrStr] + if ok { + addrData.amount += cred.Amount + // Always overwrite confirmations with newer ones. + addrData.confirmations = confirmations + } else { + addrData = AddrData{ + amount: cred.Amount, + confirmations: confirmations, + } + } + addrData.tx = append(addrData.tx, tx.Hash.String()) + allAddrData[addrStr] = addrData + } + } + } + return false, nil + }, + ) + if e != nil { + return nil, e + } + // Massage address data into output format. + numAddresses := len(allAddrData) + ret := make([]btcjson.ListReceivedByAddressResult, numAddresses) + idx := 0 + for address, addrData := range allAddrData { + ret[idx] = btcjson.ListReceivedByAddressResult{ + Address: address, + Amount: addrData.amount.ToDUO(), + Confirmations: uint64(addrData.confirmations), + TxIDs: addrData.tx, + } + idx++ + } + return ret, nil +} + +// ListSinceBlock handles a listsinceblock request by returning an array of maps with details of sent and received +// wallet transactions since the given block. +func ListSinceBlock( + icmd interface{}, w *Wallet, + cc ...*chainclient.RPCClient, +) (interface{}, error) { + if len(cc) < 1 || cc[0] == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoChain, + Message: "there is currently no chain client to get this response", + } + } + chainClient := cc[0] + cmd, ok := icmd.(*btcjson.ListSinceBlockCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listsinceblock"], + // "invalid subcommand for addnode", + } + } + syncBlock := w.Manager.SyncedTo() + targetConf := int64(*cmd.TargetConfirmations) + // For the result we need the block hash for the last block counted in the blockchain due to confirmations. We send + // this off now so that it can arrive asynchronously while we figure out the rest. + gbh := chainClient.GetBlockHashAsync(int64(syncBlock.Height) + 1 - targetConf) + var start int32 + if cmd.BlockHash != nil { + hash, e := chainhash.NewHashFromStr(*cmd.BlockHash) + if e != nil { + return nil, DeserializationError{e} + } + block, e := chainClient.GetBlockVerboseTx(hash) + if e != nil { + return nil, e + } + start = int32(block.Height) + 1 + } + txInfoList, e := w.ListSinceBlock(start, -1, syncBlock.Height) + if e != nil { + return nil, e + } + // Done with work, get the response. + blockHash, e := gbh.Receive() + if e != nil { + return nil, e + } + res := btcjson.ListSinceBlockResult{ + Transactions: txInfoList, + LastBlock: blockHash.String(), + } + return res, nil +} + +// ListTransactions handles a listtransactions request by returning an array of maps with details of sent and recevied +// wallet transactions. +func ListTransactions(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) ( + txs interface{}, + e error, +) { + // D.S(icmd) + // D.Ln("ListTransactions") + if len(chainClient) < 1 || chainClient[0] == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoChain, + Message: "there is currently no chain client to get this response", + } + } + cmd, ok := icmd.(*btcjson.ListTransactionsCmd) + if !ok { // || cmd.From == nil || cmd.Count == nil || cmd.Account != nil { + E.Ln( + "invalid parameter ok", + !ok, + "from", + cmd.From == nil, + "count", + cmd.Count == nil, + "account", + cmd.Account != nil, + ) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listtransactions"], + } + } + // // TODO: ListTransactions does not currently understand the difference + // // between transactions pertaining to one account from another. This + // // will be resolved when wtxmgr is combined with the waddrmgr namespace. + // if *cmd.Account != "*" { + // // For now, don't bother trying to continue if the user specified an account, since this can't be (easily or + // // efficiently) calculated. + // E.Ln("you must use * for account, as transactions are not yet grouped by account") + // return nil, &btcjson.RPCError{ + // Code: btcjson.ErrRPCWallet, + // Message: "Transactions are not yet grouped by account", + // } + // } + txs, e = w.ListTransactions(*cmd.From, *cmd.Count) + return txs, e +} + +// ListAddressTransactions handles a listaddresstransactions request by returning an array of maps with details of spent +// and received wallet transactions. +// +// The form of the reply is identical to listtransactions, but the array elements are limited to transaction details +// which are about the addresess included in the request. +func ListAddressTransactions( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ListAddressTransactionsCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listaddresstransactions"], + // "invalid subcommand for addnode", + } + } + if cmd.Account != nil && *cmd.Account != "*" { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Listing transactions for addresses may only be done for all accounts", + } + } + // Decode addresses. + hash160Map := make(map[string]struct{}) + for _, addrStr := range cmd.Addresses { + addr, e := DecodeAddress(addrStr, w.ChainParams()) + if e != nil { + return nil, e + } + hash160Map[string(addr.ScriptAddress())] = struct{}{} + } + return w.ListAddressTransactions(hash160Map) +} + +// ListAllTransactions handles a listalltransactions request by returning a map with details of sent and received wallet +// transactions. This is similar to ListTransactions, except it takes only a single optional argument for the account +// name and replies with all transactions. +func ListAllTransactions( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ListAllTransactionsCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listalltransactions"], + // "invalid subcommand for addnode", + } + } + if cmd.Account != nil && *cmd.Account != "*" { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Listing all transactions may only be done for all accounts", + } + } + return w.ListAllTransactions() +} + +// ListUnspent handles the listunspent command. +func ListUnspent( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ListUnspentCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["listunspent"], + // "invalid subcommand for addnode", + } + } + var addresses map[string]struct{} + if cmd.Addresses != nil { + addresses = make(map[string]struct{}) + // confirm that all of them are good: + for _, as := range *cmd.Addresses { + a, e := DecodeAddress(as, w.ChainParams()) + if e != nil { + return nil, e + } + addresses[a.EncodeAddress()] = struct{}{} + } + } + return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), addresses) +} + +// LockUnspent handles the lockunspent command. +func LockUnspent( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.LockUnspentCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["lockunspent"], + // "invalid subcommand for addnode", + } + } + switch { + case cmd.Unlock && len(cmd.Transactions) == 0: + w.ResetLockedOutpoints() + default: + for _, input := range cmd.Transactions { + txHash, e := chainhash.NewHashFromStr(input.Txid) + if e != nil { + return nil, ParseError{e} + } + op := wire.OutPoint{Hash: *txHash, Index: input.Vout} + if cmd.Unlock { + w.UnlockOutpoint(op) + } else { + w.LockOutpoint(op) + } + } + } + return true, nil +} + +// MakeOutputs creates a slice of transaction outputs from a pair of address strings to amounts. This is used to create +// the outputs to include in newly created transactions from a JSON object describing the output destinations and +// amounts. +func MakeOutputs(pairs map[string]amt.Amount, chainParams *chaincfg.Params) ([]*wire.TxOut, error) { + outputs := make([]*wire.TxOut, 0, len(pairs)) + for addrStr, amt := range pairs { + addr, e := btcaddr.Decode(addrStr, chainParams) + if e != nil { + return nil, fmt.Errorf("cannot decode address: %s", e) + } + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + return nil, fmt.Errorf("cannot create txout script: %s", e) + } + outputs = append(outputs, wire.NewTxOut(int64(amt), pkScript)) + } + return outputs, nil +} + +// SendPairs creates and sends payment transactions. It returns the transaction hash in string format upon success All +// errors are returned in json.RPCError format +func SendPairs( + w *Wallet, amounts map[string]amt.Amount, + account uint32, minconf int32, feeSatPerKb amt.Amount, +) (string, error) { + outputs, e := MakeOutputs(amounts, w.ChainParams()) + if e != nil { + return "", e + } + var txHash *chainhash.Hash + txHash, e = w.SendOutputs(outputs, account, minconf, feeSatPerKb) + if e != nil { + if e == txrules.ErrAmountNegative { + return "", ErrNeedPositiveAmount + } + if waddrmgr.IsError(e, waddrmgr.ErrLocked) { + return "", &ErrWalletUnlockNeeded + } + switch e.(type) { + case btcjson.RPCError: + return "", e + } + return "", &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, + Message: e.Error(), + } + } + txHashStr := txHash.String() + I.Ln("successfully sent transaction", txHashStr) + return txHashStr, nil +} +func IsNilOrEmpty(s *string) bool { + return s == nil || *s == "" +} + +// SendFrom handles a sendfrom RPC request by creating a new transaction spending unspent transaction outputs for a +// wallet to another payment address. Leftover inputs not sent to the payment address or a fee for the miner are sent +// back to a new address in the wallet. Upon success, the TxID for the created transaction is returned. +func SendFrom(icmd interface{}, w *Wallet, chainClient *chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.SendFromCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["sendfrom"], + // "invalid subcommand for addnode", + } + } + // Transaction comments are not yet supported. ScriptError instead of pretending to save them. + if !IsNilOrEmpty(cmd.Comment) || !IsNilOrEmpty(cmd.CommentTo) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Transaction comments are not yet supported", + } + } + account, e := w.AccountNumber( + waddrmgr.KeyScopeBIP0044, cmd.FromAccount, + ) + if e != nil { + return nil, e + } + // Chk that signed integer parameters are positive. + if cmd.Amount < 0 { + return nil, ErrNeedPositiveAmount + } + minConf := int32(*cmd.MinConf) + if minConf < 0 { + return nil, ErrNeedPositiveMinconf + } + // Create map of address and amount pairs. + amount, e := amt.NewAmount(cmd.Amount) + if e != nil { + return nil, e + } + pairs := map[string]amt.Amount{ + cmd.ToAddress: amount, + } + return SendPairs( + w, pairs, account, minConf, + txrules.DefaultRelayFeePerKb, + ) +} + +// SendMany handles a sendmany RPC request by creating a new transaction spending unspent transaction outputs for a +// wallet to any number of payment addresses. +// +// Leftover inputs not sent to the payment address or a fee for the miner are sent back to a new address in the wallet. +// Upon success, the TxID for the created transaction is returned. +func SendMany( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.SendManyCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["sendmany"], + // "invalid subcommand for addnode", + } + } + // Transaction comments are not yet supported. ScriptError instead of pretending to save them. + if !IsNilOrEmpty(cmd.Comment) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Transaction comments are not yet supported", + } + } + account, e := w.AccountNumber(waddrmgr.KeyScopeBIP0044, cmd.FromAccount) + if e != nil { + return nil, e + } + // Chk that minconf is positive. + minConf := int32(*cmd.MinConf) + if minConf < 0 { + return nil, ErrNeedPositiveMinconf + } + // Recreate address/amount pairs, using dcrutil.Amount. + pairs := make(map[string]amt.Amount, len(cmd.Amounts)) + for k, v := range cmd.Amounts { + amt, e := amt.NewAmount(v) + if e != nil { + return nil, e + } + pairs[k] = amt + } + return SendPairs(w, pairs, account, minConf, txrules.DefaultRelayFeePerKb) +} + +// SendToAddress handles a sendtoaddress RPC request by creating a new transaction spending unspent transaction outputs +// for a wallet to another payment address. +// +// Leftover inputs not sent to the payment address or a fee for the miner are sent back to a new address in the wallet. +// Upon success, the TxID for the created transaction is returned. +func SendToAddress( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.SendToAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["sendtoaddress"], + // "invalid subcommand for addnode", + } + } + // Transaction comments are not yet supported. ScriptError instead of + // pretending to save them. + if !IsNilOrEmpty(cmd.Comment) || !IsNilOrEmpty(cmd.CommentTo) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Transaction comments are not yet supported", + } + } + amount, e := amt.NewAmount(cmd.Amount) + if e != nil { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", e) + return nil, e + } + // Chk that signed integer parameters are positive. + if amount < 0 { + D.Ln(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> need positive amount") + return nil, ErrNeedPositiveAmount + } + // Mock up map of address and amount pairs. + pairs := map[string]amt.Amount{ + cmd.Address: amount, + } + // sendtoaddress always spends from the default account, this matches bitcoind + return SendPairs( + w, pairs, waddrmgr.DefaultAccountNum, 1, + txrules.DefaultRelayFeePerKb, + ) +} + +// SetTxFee sets the transaction fee per kilobyte added to transactions. +func SetTxFee( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.SetTxFeeCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["settxfee"], + // "invalid subcommand for addnode", + } + } + // Chk that amount is not negative. + if cmd.Amount < 0 { + return nil, ErrNeedPositiveAmount + } + // A boolean true result is returned upon success. + return true, nil +} + +// SignMessage signs the given message with the private key for the given address +func SignMessage( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.SignMessageCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["signmessage"], + // "invalid subcommand for addnode", + } + } + addr, e := DecodeAddress(cmd.Address, w.ChainParams()) + if e != nil { + return nil, e + } + privKey, e := w.PrivKeyForAddress(addr) + if e != nil { + return nil, e + } + var buf bytes.Buffer + e = wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n") + if e != nil { + D.Ln(e) + } + e = wire.WriteVarString(&buf, 0, cmd.Message) + if e != nil { + D.Ln(e) + } + messageHash := chainhash.DoubleHashB(buf.Bytes()) + sigbytes, e := ecc.SignCompact( + ecc.S256(), privKey, + messageHash, true, + ) + if e != nil { + return nil, e + } + return base64.StdEncoding.EncodeToString(sigbytes), nil +} + +// SignRawTransaction handles the signrawtransaction command. +func SignRawTransaction( + icmd interface{}, w *Wallet, + cc ...*chainclient.RPCClient, +) (interface{}, error) { + if len(cc) < 1 || cc[0] == nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCNoChain, + Message: "there is currently no chain client to get this response", + } + } + chainClient := cc[0] + cmd, ok := icmd.(*btcjson.SignRawTransactionCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["signrawtransaction"], + // "invalid subcommand for addnode", + } + } + serializedTx, e := DecodeHexStr(cmd.RawTx) + if e != nil { + return nil, e + } + var tx wire.MsgTx + e = tx.Deserialize(bytes.NewBuffer(serializedTx)) + if e != nil { + e = errors.New("TX decode failed") + return nil, DeserializationError{e} + } + var hashType txscript.SigHashType + switch *cmd.Flags { + case "ALL": + hashType = txscript.SigHashAll + case "NONE": + hashType = txscript.SigHashNone + case "SINGLE": + hashType = txscript.SigHashSingle + case "ALL|ANYONECANPAY": + hashType = txscript.SigHashAll | txscript.SigHashAnyOneCanPay + case "NONE|ANYONECANPAY": + hashType = txscript.SigHashNone | txscript.SigHashAnyOneCanPay + case "SINGLE|ANYONECANPAY": + hashType = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + default: + e = errors.New("invalid sighash parameter") + return nil, InvalidParameterError{e} + } + // TODO: really we probably should look these up with pod anyway to + // make sure that they match the blockchain if present. + inputs := make(map[wire.OutPoint][]byte) + scripts := make(map[string][]byte) + var cmdInputs []btcjson.RawTxInput + if cmd.Inputs != nil { + cmdInputs = *cmd.Inputs + } + for _, rti := range cmdInputs { + var inputHash *chainhash.Hash + inputHash, e = chainhash.NewHashFromStr(rti.Txid) + if e != nil { + return nil, DeserializationError{e} + } + var script []byte + script, e = DecodeHexStr(rti.ScriptPubKey) + if e != nil { + return nil, e + } + // redeemScript is only actually used iff the user provided private keys. In which case, it is used to get the + // scripts for signing. If the user did not provide keys then we always get scripts from the wallet. + // + // Empty strings are ok for this one and hex.DecodeString will DTRT. + if cmd.PrivKeys != nil && len(*cmd.PrivKeys) != 0 { + var redeemScript []byte + redeemScript, e = DecodeHexStr(rti.RedeemScript) + if e != nil { + return nil, e + } + var addr *btcaddr.ScriptHash + addr, e = btcaddr.NewScriptHash( + redeemScript, + w.ChainParams(), + ) + if e != nil { + return nil, DeserializationError{e} + } + scripts[addr.String()] = redeemScript + } + inputs[wire.OutPoint{ + Hash: *inputHash, + Index: rti.Vout, + }] = script + } + // Now we go and look for any inputs that we were not provided by querying pod with getrawtransaction. We queue up a + // bunch of async requests and will wait for replies after we have checked the rest of the arguments. + requested := make(map[wire.OutPoint]rpcclient.FutureGetTxOutResult) + for _, txIn := range tx.TxIn { + // Did we get this outpoint from the arguments? + if _, ok := inputs[txIn.PreviousOutPoint]; ok { + continue + } + // Asynchronously request the output script. + requested[txIn.PreviousOutPoint] = chainClient.GetTxOutAsync( + &txIn.PreviousOutPoint.Hash, txIn.PreviousOutPoint.Index, + true, + ) + } + // Parse list of private keys, if present. If there are any keys here they are the keys that we may use for signing. + // If empty we will use any keys known to us already. + var keys map[string]*util.WIF + if cmd.PrivKeys != nil { + keys = make(map[string]*util.WIF) + for _, key := range *cmd.PrivKeys { + var wif *util.WIF + wif, e = util.DecodeWIF(key) + if e != nil { + return nil, DeserializationError{e} + } + if !wif.IsForNet(w.ChainParams()) { + s := "key network doesn't match wallet's" + return nil, DeserializationError{errors.New(s)} + } + var addr *btcaddr.PubKey + addr, e = btcaddr.NewPubKey( + wif.SerializePubKey(), + w.ChainParams(), + ) + if e != nil { + return nil, DeserializationError{e} + } + keys[addr.EncodeAddress()] = wif + } + } + // We have checked the rest of the args. now we can collect the async txs. + // + // TODO: If we don't mind the possibility of wasting work we could move waiting to the following loop and be + // slightly more asynchronous. + for outPoint, resp := range requested { + var result *btcjson.GetTxOutResult + result, e = resp.Receive() + if e != nil { + return nil, e + } + var script []byte + if script, e = hex.DecodeString(result.ScriptPubKey.Hex); E.Chk(e) { + return nil, e + } + inputs[outPoint] = script + } + // All args collected. Now we can sign all the inputs that we can. `complete' denotes that we successfully signed + // all outputs and that all scripts will run to completion. This is returned as part of the reply. + var signErrs []SignatureError + signErrs, e = w.SignTransaction(&tx, hashType, inputs, keys, scripts) + if e != nil { + return nil, e + } + var buf bytes.Buffer + buf.Grow(tx.SerializeSize()) + // All returned errors (not OOM, which panics) encountered during bytes.Buffer writes are unexpected. + if e = tx.Serialize(&buf); E.Chk(e) { + panic(e) + } + signErrors := make([]btcjson.SignRawTransactionError, 0, len(signErrs)) + for _, ee := range signErrs { + input := tx.TxIn[ee.InputIndex] + signErrors = append( + signErrors, btcjson.SignRawTransactionError{ + TxID: input.PreviousOutPoint.Hash.String(), + Vout: input.PreviousOutPoint.Index, + ScriptSig: hex.EncodeToString(input.SignatureScript), + Sequence: input.Sequence, + Error: e.Error(), + }, + ) + } + return btcjson.SignRawTransactionResult{ + Hex: hex.EncodeToString(buf.Bytes()), + Complete: len(signErrors) == 0, + Errors: signErrors, + }, nil +} + +// ValidateAddress handles the validateaddress command. +func ValidateAddress(icmd interface{}, w *Wallet, chainClient ...*chainclient.RPCClient) (interface{}, error) { + cmd, ok := icmd.(*btcjson.ValidateAddressCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["validateaddress"], + // "invalid subcommand for addnode", + } + } + result := btcjson.ValidateAddressWalletResult{} + addr, e := DecodeAddress(cmd.Address, w.ChainParams()) + if e != nil { + // Use result zero value (IsValid=false). + return result, nil + } + // We could put whether or not the address is a script here, by checking the type of "addr", however, the reference + // implementation only puts that information if the script is "ismine", and we follow that behaviour. + result.Address = addr.EncodeAddress() + result.IsValid = true + ainfo, e := w.AddressInfo(addr) + if e != nil { + if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { + // No additional information available about the address. + return result, nil + } + return nil, e + } + // The address lookup was successful which means there is further information about it available and it is "mine". + result.IsMine = true + acctName, e := w.AccountName(waddrmgr.KeyScopeBIP0044, ainfo.Account()) + if e != nil { + return nil, &ErrAccountNameNotFound + } + result.Account = acctName + switch ma := ainfo.(type) { + case waddrmgr.ManagedPubKeyAddress: + result.IsCompressed = ma.Compressed() + result.PubKey = ma.ExportPubKey() + case waddrmgr.ManagedScriptAddress: + result.IsScript = true + // The script is only available if the manager is unlocked, so just break out now if there is an error. + script, e := ma.Script() + if e != nil { + break + } + result.Hex = hex.EncodeToString(script) + // This typically shouldn't fail unless an invalid script was imported. + // + // However, if it fails for any reason, there is no further information available, so just set the script type a + // non-standard and break out now. + class, addrs, reqSigs, e := txscript.ExtractPkScriptAddrs( + script, w.ChainParams(), + ) + if e != nil { + result.Script = txscript.NonStandardTy.String() + break + } + addrStrings := make([]string, len(addrs)) + for i, a := range addrs { + addrStrings[i] = a.EncodeAddress() + } + result.Addresses = addrStrings + // Multi-signature scripts also provide the number of required + // signatures. + result.Script = class.String() + if class == txscript.MultiSigTy { + result.SigsRequired = int32(reqSigs) + } + } + return result, nil +} + +// VerifyMessage handles the verifymessage command by verifying the provided compact signature for the given address and +// message. +func VerifyMessage( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.VerifyMessageCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["verifymessage"], + // "invalid subcommand for addnode", + } + } + addr, e := DecodeAddress(cmd.Address, w.ChainParams()) + if e != nil { + return nil, e + } + // decode base64 signature + sig, e := base64.StdEncoding.DecodeString(cmd.Signature) + if e != nil { + return nil, e + } + // Validate the signature - this just shows that it was valid at all. we will compare it with the key next. + var buf bytes.Buffer + e = wire.WriteVarString(&buf, 0, "Parallelcoin Signed Message:\n") + if e != nil { + D.Ln(e) + } + e = wire.WriteVarString(&buf, 0, cmd.Message) + if e != nil { + D.Ln(e) + } + expectedMessageHash := chainhash.DoubleHashB(buf.Bytes()) + pk, wasCompressed, e := ecc.RecoverCompact( + ecc.S256(), sig, + expectedMessageHash, + ) + if e != nil { + return nil, e + } + var serializedPubKey []byte + if wasCompressed { + serializedPubKey = pk.SerializeCompressed() + } else { + serializedPubKey = pk.SerializeUncompressed() + } + // Verify that the signed-by address matches the given address + switch checkAddr := addr.(type) { + case *btcaddr.PubKeyHash: // ok + return bytes.Equal(btcaddr.Hash160(serializedPubKey), checkAddr.Hash160()[:]), nil + case *btcaddr.PubKey: // ok + return string(serializedPubKey) == checkAddr.String(), nil + default: + return nil, errors.New("address type not supported") + } +} + +// WalletIsLocked handles the walletislocked extension request by returning the current lock state (false for unlocked, +// true for locked) of an account. +func WalletIsLocked( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + return w.Locked(), nil +} + +// WalletLock handles a walletlock request by locking the all account wallets, returning an error if any wallet is not +// encrypted (for example, a watching-only wallet). +func WalletLock( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + w.Lock() + return nil, nil +} + +// WalletPassphrase responds to the walletpassphrase request by unlocking the wallet. The decryption key is saved in the +// wallet until timeout seconds expires, after which the wallet is locked. +func WalletPassphrase( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.WalletPassphraseCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["walletpassphrase"], + // "invalid subcommand for addnode", + } + } + timeout := time.Second * time.Duration(cmd.Timeout) + var unlockAfter <-chan time.Time + if timeout != 0 { + unlockAfter = time.After(timeout) + } + e := w.Unlock([]byte(cmd.Passphrase), unlockAfter) + return nil, e +} + +// WalletPassphraseChange responds to the walletpassphrasechange request by unlocking all accounts with the provided old +// passphrase, and re-encrypting each private key with an AES key derived from the new passphrase. +// +// If the old passphrase is correct and the passphrase is changed, all wallets will be immediately locked. +func WalletPassphraseChange( + icmd interface{}, w *Wallet, + chainClient ...*chainclient.RPCClient, +) (interface{}, error) { + cmd, ok := icmd.(*btcjson.WalletPassphraseChangeCmd) + if !ok { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: HelpDescsEnUS()["walletpassphrasechange"], + // "invalid subcommand for addnode", + } + } + e := w.ChangePrivatePassphrase( + []byte(cmd.OldPassphrase), + []byte(cmd.NewPassphrase), + ) + if waddrmgr.IsError(e, waddrmgr.ErrWrongPassphrase) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCWalletPassphraseIncorrect, + Message: "Incorrect passphrase", + } + } + return nil, e +} + +// DecodeHexStr decodes the hex encoding of a string, possibly prepending a leading '0' character if there is an odd +// number of bytes in the hex string. This is to prevent an error for an invalid hex string when using an odd number of +// bytes when calling hex.Decode. +func DecodeHexStr(hexStr string) ([]byte, error) { + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + decoded, e := hex.DecodeString(hexStr) + if e != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDecodeHexString, + Message: "Hex string decode failed: " + e.Error(), + } + } + return decoded, nil +} diff --git a/cmd/wallet/multisig.go b/cmd/wallet/multisig.go new file mode 100644 index 0000000..681e7cd --- /dev/null +++ b/cmd/wallet/multisig.go @@ -0,0 +1,104 @@ +package wallet + +import ( + "errors" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" +) + +// MakeMultiSigScript creates a multi-signature script that can be redeemed with nRequired signatures of the passed keys +// and addresses. If the address is a P2PKH address, the associated pubkey is looked up by the wallet if possible, +// otherwise an error is returned for a missing pubkey. +// +// This function only works with pubkeys and P2PKH addresses derived from them. +func (w *Wallet) MakeMultiSigScript(addrs []btcaddr.Address, nRequired int) ([]byte, error) { + pubKeys := make([]*btcaddr.PubKey, len(addrs)) + var dbtx walletdb.ReadTx + var addrmgrNs walletdb.ReadBucket + defer func() { + if dbtx != nil { + e := dbtx.Rollback() + if e != nil { + } + } + }() + // The address list will made up either of addreseses (pubkey hash), for which we need to look up the keys in + // wallet, straight pubkeys, or a mixture of the two. + for i, addr := range addrs { + switch addr := addr.(type) { + default: + return nil, errors.New( + "cannot make multisig script for " + + "a non-secp256k1 public key or P2PKH address", + ) + case *btcaddr.PubKey: + pubKeys[i] = addr + case *btcaddr.PubKeyHash: + if dbtx == nil { + var e error + dbtx, e = w.db.BeginReadTx() + if e != nil { + return nil, e + } + addrmgrNs = dbtx.ReadBucket(waddrmgrNamespaceKey) + } + addrInfo, e := w.Manager.Address(addrmgrNs, addr) + if e != nil { + return nil, e + } + serializedPubKey := addrInfo.(waddrmgr.ManagedPubKeyAddress). + PubKey().SerializeCompressed() + pubKeyAddr, e := btcaddr.NewPubKey( + serializedPubKey, w.chainParams, + ) + if e != nil { + return nil, e + } + pubKeys[i] = pubKeyAddr + } + } + return txscript.MultiSigScript(pubKeys, nRequired) +} + +// ImportP2SHRedeemScript adds a P2SH redeem script to the wallet. +func (w *Wallet) ImportP2SHRedeemScript(script []byte) (*btcaddr.ScriptHash, error) { + var p2shAddr *btcaddr.ScriptHash + e := walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + // TODO(oga) blockstamp current block? + bs := &waddrmgr.BlockStamp{ + Hash: *w.ChainParams().GenesisHash, + Height: 0, + } + // As this is a regular P2SH script, we'll import this into the BIP0044 scope. + var bip44Mgr *waddrmgr.ScopedKeyManager + bip44Mgr, e = w.Manager.FetchScopedKeyManager( + waddrmgr.KeyScopeBIP0044, + ) + if e != nil { + return e + } + addrInfo, e := bip44Mgr.ImportScript(addrmgrNs, script, bs) + if e != nil { + // Don't care if it's already there, but still have to set the p2shAddr since the address manager didn't + // return anything useful. + if waddrmgr.IsError(e, waddrmgr.ErrDuplicateAddress) { + // This function will never error as it always hashes the script to the correct length. + p2shAddr, _ = btcaddr.NewScriptHash( + script, + w.chainParams, + ) + return nil + } + return e + } + p2shAddr = addrInfo.Address().(*btcaddr.ScriptHash) + return nil + }, + ) + return p2shAddr, e +} diff --git a/cmd/wallet/notifications.go b/cmd/wallet/notifications.go new file mode 100644 index 0000000..c780b44 --- /dev/null +++ b/cmd/wallet/notifications.go @@ -0,0 +1,612 @@ +package wallet + +import ( + "bytes" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "sync" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wtxmgr" +) + +// AccountBalance associates a total (zero confirmation) balance with an account. Balances for other minimum +// confirmation counts require more expensive logic and it is not clear which minimums a client is interested in, so +// they are not included. +type AccountBalance struct { + Account uint32 + TotalBalance amt.Amount +} + +// AccountNotification contains properties regarding an account, such as its name and the number of derived and imported +// keys. When any of these properties change, the notification is fired. +type AccountNotification struct { + AccountNumber uint32 + AccountName string + ExternalKeyCount uint32 + InternalKeyCount uint32 + ImportedKeyCount uint32 +} + +// AccountNotificationsClient receives AccountNotifications over the channel C. +type AccountNotificationsClient struct { + C chan *AccountNotification + server *NotificationServer +} + +// Block contains the properties and all relevant transactions of an attached +// block. +type Block struct { + Hash *chainhash.Hash + Height int32 + Timestamp int64 + Transactions []TransactionSummary +} + +// TODO: It would be good to send errors during notification creation to the rpc server instead of just logging them +// here so the client is aware that wallet isn't working correctly and notifications are missing. +// +// TODO: Anything dealing with accounts here is expensive because the database is not organized correctly for true +// account support, but do the slow thing instead of the easy thing since the db can be fixed later, and we want the api +// correct now. + +// NotificationServer is a server that interested clients may hook into to receive notifications of changes +// in a wallet. A client is created for each registered notification. Clients are guaranteed to receive messages in the +// order wallet created them, but there is no guaranteed synchronization between different clients. +type NotificationServer struct { + transactions []chan *TransactionNotifications + currentTxNtfn *TransactionNotifications // coalesce this since wallet does not add mined txs together + spentness map[uint32][]chan *SpentnessNotifications + accountClients []chan *AccountNotification + mu sync.Mutex // Only protects registered client channels + wallet *Wallet // smells like hacks +} + +// SpentnessNotifications is a notification that is fired for transaction outputs controlled by some account's keys. The +// notification may be about a newly added unspent transaction output or that a previously unspent output is now spent. +// When spent, the notification includes the spending transaction's hash and input index. +type SpentnessNotifications struct { + hash *chainhash.Hash + spenderHash *chainhash.Hash + index uint32 + spenderIndex uint32 +} + +// SpentnessNotificationsClient receives SpentnessNotifications from the NotificationServer over the channel C. +type SpentnessNotificationsClient struct { + C <-chan *SpentnessNotifications + account uint32 + server *NotificationServer +} + +// TransactionNotifications is a notification of changes to the wallet's transaction set and the current chain tip that +// wallet is considered to be synced with. All transactions added to the blockchain are organized by the block they were +// mined in. +// +// During a chain switch, all removed block hashes are included. Detached blocks are sorted in the reverse order they +// were mined. Attached blocks are sorted in the order mined. +// +// All newly added unmined transactions are included. Removed unmined transactions are not explicitly included. Instead, +// the hashes of all transactions still unmined are included. +// +// If any transactions were involved, each affected account's new total balance is included. +// +// TODO: Because this includes stuff about blocks and can be fired without any changes to transactions, it needs a +// better name. +type TransactionNotifications struct { + AttachedBlocks []Block + DetachedBlocks []*chainhash.Hash + UnminedTransactions []TransactionSummary + UnminedTransactionHashes []*chainhash.Hash + NewBalances []AccountBalance +} + +// TransactionNotificationsClient receives TransactionNotifications from the NotificationServer over the channel C. +type TransactionNotificationsClient struct { + C <-chan *TransactionNotifications + server *NotificationServer +} + +// TransactionSummary contains a transaction relevant to the wallet and marks which inputs and outputs were relevant. +type TransactionSummary struct { + Hash *chainhash.Hash + Transaction []byte + MyInputs []TransactionSummaryInput + MyOutputs []TransactionSummaryOutput + Fee amt.Amount + Timestamp int64 +} + +// TransactionSummaryInput describes a transaction input that is relevant to the wallet. The Index field marks the +// transaction input index of the transaction (not included here). The PreviousAccount and PreviousAmount fields +// describe how much this input debits from a wallet account. +type TransactionSummaryInput struct { + Index uint32 + PreviousAccount uint32 + PreviousAmount amt.Amount +} + +// TransactionSummaryOutput describes wallet properties of a transaction output controlled by the wallet. The Index +// field marks the transaction output index of the transaction (not included here). +type TransactionSummaryOutput struct { + Index uint32 + Account uint32 + Internal bool +} + +// Done unregisters the client from the server and drains any remaining messages. It must be called exactly once when +// the client is finished receiving notifications. +func (c *AccountNotificationsClient) Done() { + go func() { + for range c.C { + } + }() + go func() { + s := c.server + s.mu.Lock() + clients := s.accountClients + for i, ch := range clients { + if c.C == ch { + clients[i] = clients[len(clients)-1] + s.accountClients = clients[:len(clients)-1] + close(ch) + break + } + } + s.mu.Unlock() + }() +} + +// AccountNotifications returns a client for receiving AccountNotifications over a channel. The channel is unbuffered. +// When finished, the client's Done method should be called to disassociate the client from the server. +func (s *NotificationServer) AccountNotifications() AccountNotificationsClient { + c := make(chan *AccountNotification) + s.mu.Lock() + s.accountClients = append(s.accountClients, c) + s.mu.Unlock() + return AccountNotificationsClient{ + C: c, + server: s, + } +} + +// AccountSpentnessNotifications registers a client for spentness changes of outputs controlled by the account. +func (s *NotificationServer) AccountSpentnessNotifications(account uint32) SpentnessNotificationsClient { + c := make(chan *SpentnessNotifications) + s.mu.Lock() + s.spentness[account] = append(s.spentness[account], c) + s.mu.Unlock() + return SpentnessNotificationsClient{ + C: c, + account: account, + server: s, + } +} + +// TransactionNotifications returns a client for receiving TransactionNotifications notifications over a channel. The +// channel is unbuffered. +// +// When finished, the Done method should be called on the client to disassociate it from the server. +func (s *NotificationServer) TransactionNotifications() TransactionNotificationsClient { + c := make(chan *TransactionNotifications) + s.mu.Lock() + s.transactions = append(s.transactions, c) + s.mu.Unlock() + return TransactionNotificationsClient{ + C: c, + server: s, + } +} +func (s *NotificationServer) notifyAccountProperties(props *waddrmgr.AccountProperties) { + defer s.mu.Unlock() + s.mu.Lock() + clients := s.accountClients + if len(clients) == 0 { + return + } + n := &AccountNotification{ + AccountNumber: props.AccountNumber, + AccountName: props.AccountName, + ExternalKeyCount: props.ExternalKeyCount, + InternalKeyCount: props.InternalKeyCount, + ImportedKeyCount: props.ImportedKeyCount, + } + for _, c := range clients { + c <- n + } +} +func (s *NotificationServer) notifyAttachedBlock(dbtx walletdb.ReadTx, block *wtxmgr.BlockMeta) { + if s.currentTxNtfn == nil { + s.currentTxNtfn = &TransactionNotifications{} + } + // Add block details if it wasn't already included for previously notified mined transactions. + n := len(s.currentTxNtfn.AttachedBlocks) + if n == 0 || *s.currentTxNtfn.AttachedBlocks[n-1].Hash != block.Hash { + s.currentTxNtfn.AttachedBlocks = append( + s.currentTxNtfn.AttachedBlocks, Block{ + Hash: &block.Hash, + Height: block.Height, + Timestamp: block.Time.Unix(), + }, + ) + } + // For now (until notification coalescing isn't necessary) just use chain length to determine if this is the new + // best block. + if s.wallet.ChainSynced() { + if len(s.currentTxNtfn.DetachedBlocks) >= len(s.currentTxNtfn.AttachedBlocks) { + return + } + } + defer s.mu.Unlock() + s.mu.Lock() + clients := s.transactions + if len(clients) == 0 { + s.currentTxNtfn = nil + return + } + // The UnminedTransactions field is intentionally not set. Since the hashes of all detached blocks are reported, and + // all transactions moved from a mined block back to unconfirmed are either in the UnminedTransactionHashes slice or + // don't exist due to conflicting with a mined transaction in the new best chain, there is no possiblity of a new, + // previously unseen transaction appearing in unconfirmed. + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + unminedHashes, e := s.wallet.TxStore.UnminedTxHashes(txmgrNs) + if e != nil { + E.Ln( + "cannot fetch unmined transaction hashes:", e, + ) + return + } + s.currentTxNtfn.UnminedTransactionHashes = unminedHashes + bals := make(map[uint32]amt.Amount) + for _, b := range s.currentTxNtfn.AttachedBlocks { + relevantAccounts(s.wallet, bals, b.Transactions) + } + e = totalBalances(dbtx, s.wallet, bals) + if e != nil { + E.Ln( + "cannot determine balances for relevant accounts:", e, + ) + return + } + s.currentTxNtfn.NewBalances = flattenBalanceMap(bals) + for _, c := range clients { + c <- s.currentTxNtfn + } + s.currentTxNtfn = nil +} +func (s *NotificationServer) notifyDetachedBlock(hash *chainhash.Hash) { + if s.currentTxNtfn == nil { + s.currentTxNtfn = &TransactionNotifications{} + } + s.currentTxNtfn.DetachedBlocks = append(s.currentTxNtfn.DetachedBlocks, hash) +} +func (s *NotificationServer) notifyMinedTransaction( + dbtx walletdb.ReadTx, + details *wtxmgr.TxDetails, + block *wtxmgr.BlockMeta, +) { + if s.currentTxNtfn == nil { + s.currentTxNtfn = &TransactionNotifications{} + } + n := len(s.currentTxNtfn.AttachedBlocks) + if n == 0 || *s.currentTxNtfn.AttachedBlocks[n-1].Hash != block.Hash { + s.currentTxNtfn.AttachedBlocks = append( + s.currentTxNtfn.AttachedBlocks, Block{ + Hash: &block.Hash, + Height: block.Height, + Timestamp: block.Time.Unix(), + }, + ) + n++ + } + txs := s.currentTxNtfn.AttachedBlocks[n-1].Transactions + s.currentTxNtfn.AttachedBlocks[n-1].Transactions = + append(txs, makeTxSummary(dbtx, s.wallet, details)) +} + +// // notifySpentOutput notifies registered clients that a previously-unspent +// // output is now spent, and includes the spender hash and input index in the +// // notification. +// func (s *NotificationServer) notifySpentOutput(account uint32, op *wire.OutPoint, spenderHash *chainhash.Hash, spenderIndex uint32) { +// defer s.mu.Unlock() +// s.mu.Lock() +// clients := s.spentness[account] +// if len(clients) == 0 { +// return +// } +// n := &SpentnessNotifications{ +// hash: &op.Hash, +// index: op.Index, +// spenderHash: spenderHash, +// spenderIndex: spenderIndex, +// } +// for _, c := range clients { +// c <- n +// } +// } +func (s *NotificationServer) notifyUnminedTransaction(dbtx walletdb.ReadTx, details *wtxmgr.TxDetails) { + // Sanity check: should not be currently coalescing a notification for mined transactions at the same time that an + // unmined tx is notified. + if s.currentTxNtfn != nil { + E.Ln( + "notifying unmined tx notification (", + details.Hash.String(), + ") while creating notification for blocks", + ) + } + defer s.mu.Unlock() + s.mu.Lock() + clients := s.transactions + if len(clients) == 0 { + return + } + unminedTxs := []TransactionSummary{makeTxSummary(dbtx, s.wallet, details)} + unminedHashes, e := s.wallet.TxStore.UnminedTxHashes(dbtx.ReadBucket(wtxmgrNamespaceKey)) + if e != nil { + E.Ln( + "cannot fetch unmined transaction hashes:", e, + ) + return + } + bals := make(map[uint32]amt.Amount) + relevantAccounts(s.wallet, bals, unminedTxs) + e = totalBalances(dbtx, s.wallet, bals) + if e != nil { + E.Ln( + "cannot determine balances for relevant accounts:", e, + ) + return + } + n := &TransactionNotifications{ + UnminedTransactions: unminedTxs, + UnminedTransactionHashes: unminedHashes, + NewBalances: flattenBalanceMap(bals), + } + for _, c := range clients { + c <- n + } +} + +// notifyUnspentOutput notifies registered clients of a new unspent output that is controlled by the wallet. +func (s *NotificationServer) notifyUnspentOutput(account uint32, hash *chainhash.Hash, index uint32) { + defer s.mu.Unlock() + s.mu.Lock() + clients := s.spentness[account] + if len(clients) == 0 { + return + } + n := &SpentnessNotifications{ + hash: hash, + index: index, + } + for _, c := range clients { + c <- n + } +} + +// Hash returns the transaction hash of the spent output. +func (n *SpentnessNotifications) Hash() *chainhash.Hash { + return n.hash +} + +// Index returns the transaction output index of the spent output. +func (n *SpentnessNotifications) Index() uint32 { + return n.index +} + +// Spender returns the spending transaction's hash and input index, if any. If the output is unspent, the final bool +// return is false. +func (n *SpentnessNotifications) Spender() (*chainhash.Hash, uint32, bool) { + return n.spenderHash, n.spenderIndex, n.spenderHash != nil +} + +// Done unregisters the client from the server and drains any remaining messages. It must be called exactly once when +// the client is finished receiving notifications. +func (c *SpentnessNotificationsClient) Done() { + go func() { + // Drain notifications until the client channel is removed from the server and closed. + for range c.C { + } + }() + go func() { + s := c.server + s.mu.Lock() + clients := s.spentness[c.account] + for i, ch := range clients { + if c.C == ch { + clients[i] = clients[len(clients)-1] + s.spentness[c.account] = clients[:len(clients)-1] + close(ch) + break + } + } + s.mu.Unlock() + }() +} + +// Done unregisters the client from the server and drains any remaining messages. It must be called exactly once when +// the client is finished receiving notifications. +func (c *TransactionNotificationsClient) Done() { + go func() { + // Drain notifications until the client channel is removed from the server and closed. + for range c.C { + } + }() + go func() { + s := c.server + s.mu.Lock() + clients := s.transactions + for i, ch := range clients { + if c.C == ch { + clients[i] = clients[len(clients)-1] + s.transactions = clients[:len(clients)-1] + close(ch) + break + } + } + s.mu.Unlock() + }() +} +func flattenBalanceMap(m map[uint32]amt.Amount) []AccountBalance { + s := make([]AccountBalance, 0, len(m)) + for k, v := range m { + s = append(s, AccountBalance{Account: k, TotalBalance: v}) + } + return s +} +func lookupInputAccount(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRecord) uint32 { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + // TODO: Debits should record which account(s?) they + // debit from so this doesn't need to be looked up. + prevOP := &details.MsgTx.TxIn[deb.Index].PreviousOutPoint + prev, e := w.TxStore.TxDetails(txmgrNs, &prevOP.Hash) + if e != nil { + E.F( + "cannot query previous transaction details for %v: %v", + prevOP.Hash, e, + ) + return 0 + } + if prev == nil { + E.Ln( + "missing previous transaction", prevOP.Hash, + ) + return 0 + } + prevOut := prev.MsgTx.TxOut[prevOP.Index] + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs(prevOut.PkScript, w.chainParams) + var inputAcct uint32 + if e == nil && len(addrs) > 0 { + _, inputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if e != nil { + E.F( + "cannot fetch account for previous output %v: %v", prevOP, e, + ) + inputAcct = 0 + } + return inputAcct +} +func lookupOutputChain( + dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails, + cred wtxmgr.CreditRecord, +) (account uint32, internal bool) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + output := details.MsgTx.TxOut[cred.Index] + var addrs []btcaddr.Address + var e error + _, addrs, _, e = txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + var ma waddrmgr.ManagedAddress + if e == nil && len(addrs) > 0 { + ma, e = w.Manager.Address(addrmgrNs, addrs[0]) + } + if e != nil { + E.Ln( + "cannot fetch account for wallet output:", e, + ) + } else { + account = ma.Account() + internal = ma.Internal() + } + return +} +func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { + serializedTx := details.SerializedTx + if serializedTx == nil { + var buf bytes.Buffer + e := details.MsgTx.Serialize(&buf) + if e != nil { + E.Ln("transaction serialization:", e) + } + serializedTx = buf.Bytes() + } + var fee amt.Amount + if len(details.Debits) == len(details.MsgTx.TxIn) { + for _, deb := range details.Debits { + fee += deb.Amount + } + for _, txOut := range details.MsgTx.TxOut { + fee -= amt.Amount(txOut.Value) + } + } + var inputs []TransactionSummaryInput + if len(details.Debits) != 0 { + inputs = make([]TransactionSummaryInput, len(details.Debits)) + for i, d := range details.Debits { + inputs[i] = TransactionSummaryInput{ + Index: d.Index, + PreviousAccount: lookupInputAccount(dbtx, w, details, d), + PreviousAmount: d.Amount, + } + } + } + outputs := make([]TransactionSummaryOutput, 0, len(details.MsgTx.TxOut)) + for i := range details.MsgTx.TxOut { + credIndex := len(outputs) + mine := len(details.Credits) > credIndex && details.Credits[credIndex].Index == uint32(i) + if !mine { + continue + } + acct, internal := lookupOutputChain(dbtx, w, details, details.Credits[credIndex]) + output := TransactionSummaryOutput{ + Index: uint32(i), + Account: acct, + Internal: internal, + } + outputs = append(outputs, output) + } + return TransactionSummary{ + Hash: &details.Hash, + Transaction: serializedTx, + MyInputs: inputs, + MyOutputs: outputs, + Fee: fee, + Timestamp: details.Received.Unix(), + } +} +func newNotificationServer(wallet *Wallet) *NotificationServer { + return &NotificationServer{ + spentness: make(map[uint32][]chan *SpentnessNotifications), + wallet: wallet, + } +} +func relevantAccounts(w *Wallet, m map[uint32]amt.Amount, txs []TransactionSummary) { + for _, tx := range txs { + for _, d := range tx.MyInputs { + m[d.PreviousAccount] = 0 + } + for _, c := range tx.MyOutputs { + m[c.Account] = 0 + } + } +} +func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]amt.Amount) (e error) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + unspent, e := w.TxStore.UnspentOutputs(dbtx.ReadBucket(wtxmgrNamespaceKey)) + if e != nil { + return e + } + for i := range unspent { + output := &unspent[i] + var outputAcct uint32 + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams, + ) + if e == nil && len(addrs) > 0 { + _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if e == nil { + _, ok := m[outputAcct] + if ok { + m[outputAcct] += output.Amount + } + } + } + return nil +} diff --git a/cmd/wallet/recovery.go b/cmd/wallet/recovery.go new file mode 100644 index 0000000..b96682d --- /dev/null +++ b/cmd/wallet/recovery.go @@ -0,0 +1,352 @@ +package wallet + +import ( + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util/hdkeychain" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wire" + "github.com/p9c/p9/pkg/wtxmgr" +) + +// RecoveryManager maintains the state required to recover previously used addresses, and coordinates batched processing +// of the blocks to search. +type RecoveryManager struct { + // recoveryWindow defines the key-derivation lookahead used when attempting to recover the set of used addresses. + recoveryWindow uint32 + // started is true after the first block has been added to the batch. + started bool + // blockBatch contains a list of blocks that have not yet been searched for recovered addresses. + blockBatch []wtxmgr.BlockMeta + // state encapsulates and allocates the necessary recovery state for all key scopes and subsidiary derivation paths. + state *RecoveryState + // chainParams are the parameters that describe the chain we're trying to recover funds on. + chainParams *chaincfg.Params +} + +// NewRecoveryManager initializes a new RecoveryManager with a derivation look-ahead of `recoveryWindow` child indexes, +// and pre-allocates a backing array for `batchSize` blocks to scan at once. +func NewRecoveryManager( + recoveryWindow, batchSize uint32, + chainParams *chaincfg.Params, +) *RecoveryManager { + return &RecoveryManager{ + recoveryWindow: recoveryWindow, + blockBatch: make([]wtxmgr.BlockMeta, 0, batchSize), + chainParams: chainParams, + state: NewRecoveryState(recoveryWindow), + } +} + +// Resurrect restores all known addresses for the provided scopes that can be found in the walletdb namespace, in +// addition to restoring all outpoints that have been previously found. This method ensures that the recovery state's +// horizons properly start from the last found address of a prior recovery attempt. +func (rm *RecoveryManager) Resurrect( + ns walletdb.ReadBucket, + scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, + credits []wtxmgr.Credit, +) (e error) { + // First, for each scope that we are recovering, rederive all of the addresses up to the last found address known to + // each branch. + for keyScope, scopedMgr := range scopedMgrs { + // Load the current account properties for this scope, using the the default account number. + // + // TODO(conner): rescan for all created accounts if we allow users to use non-default address + scopeState := rm.state.StateForScope(keyScope) + var acctProperties *waddrmgr.AccountProperties + acctProperties, e = scopedMgr.AccountProperties( + ns, waddrmgr.DefaultAccountNum, + ) + if e != nil { + return e + } + // Fetch the external key count, which bounds the indexes we will need to rederive. + externalCount := acctProperties.ExternalKeyCount + // Walk through all indexes through the last external key, deriving each address and adding it to the external + // branch recovery state's set of addresses to look for. + for i := uint32(0); i < externalCount; i++ { + keyPath := externalKeyPath(i) + var addr waddrmgr.ManagedAddress + addr, e = scopedMgr.DeriveFromKeyPath(ns, keyPath) + if e != nil && e != hdkeychain.ErrInvalidChild || addr == nil { + return e + } else if e == hdkeychain.ErrInvalidChild { + scopeState.ExternalBranch.MarkInvalidChild(i) + continue + } + scopeState.ExternalBranch.AddAddr(i, addr.Address()) + } + // Fetch the internal key count, which bounds the indexes we will need to rederive. + internalCount := acctProperties.InternalKeyCount + // Walk through all indexes through the last internal key, deriving each address and adding it to the internal + // branch recovery state's set of addresses to look for. + for i := uint32(0); i < internalCount; i++ { + keyPath := internalKeyPath(i) + var addr waddrmgr.ManagedAddress + addr, e = scopedMgr.DeriveFromKeyPath(ns, keyPath) + if e != nil && e != hdkeychain.ErrInvalidChild || addr == nil { + return e + } else if e == hdkeychain.ErrInvalidChild { + scopeState.InternalBranch.MarkInvalidChild(i) + continue + } + scopeState.InternalBranch.AddAddr(i, addr.Address()) + } + // The key counts will point to the next key that can be derived, so we subtract one to point to last known key. + // If the key count is zero, then no addresses have been found. + if externalCount > 0 { + scopeState.ExternalBranch.ReportFound(externalCount - 1) + } + if internalCount > 0 { + scopeState.InternalBranch.ReportFound(internalCount - 1) + } + } + // In addition, we will re-add any outpoints that are known the wallet to our global set of watched outpoints, so + // that we can watch them for spends. + for _, credit := range credits { + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + credit.PkScript, rm.chainParams, + ) + if e != nil { + return e + } + rm.state.AddWatchedOutPoint(&credit.OutPoint, addrs[0]) + } + return nil +} + +// AddToBlockBatch appends the block information, consisting of hash and height, to the batch of blocks to be searched. +func (rm *RecoveryManager) AddToBlockBatch( + hash *chainhash.Hash, height int32, + timestamp time.Time, +) { + if !rm.started { + T.F( + "Seed birthday surpassed, starting recovery of wallet from height=%d hash=%v with recovery-window=%d", + height, *hash, rm.recoveryWindow, + ) + rm.started = true + } + block := wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: *hash, + Height: height, + }, + Time: timestamp, + } + rm.blockBatch = append(rm.blockBatch, block) +} + +// BlockBatch returns a buffer of blocks that have not yet been searched. +func (rm *RecoveryManager) BlockBatch() []wtxmgr.BlockMeta { + return rm.blockBatch +} + +// ResetBlockBatch resets the internal block buffer to conserve memory. +func (rm *RecoveryManager) ResetBlockBatch() { + rm.blockBatch = rm.blockBatch[:0] +} + +// State returns the current RecoveryState. +func (rm *RecoveryManager) State() *RecoveryState { + return rm.state +} + +// RecoveryState manages the initialization and lookup of ScopeRecoveryStates for any actively used key scopes. +// +// In order to ensure that all addresses are properly recovered, the window should be sized as the sum of maximum +// possible inter-block and intra-block gap between used addresses of a particular branch. +// +// These are defined as: +// +// - Inter-Block Gap: The maximum difference between the derived child indexes of the last addresses used in any block +// and the next address consumed by a later block. +// +// - Intra-Block Gap: The maximum difference between the derived child indexes of the first address used in any block +// and the last address used in the same block. +type RecoveryState struct { + // recoveryWindow defines the key-derivation lookahead used when attempting to recover the set of used addresses. + // This value will be used to instantiate a new RecoveryState for each requested scope. + recoveryWindow uint32 + // scopes maintains a map of each requested key scope to its active RecoveryState. + scopes map[waddrmgr.KeyScope]*ScopeRecoveryState + // watchedOutPoints contains the set of all outpoints known to the wallet. This is updated iteratively as new + // outpoints are found during a rescan. + watchedOutPoints map[wire.OutPoint]btcaddr.Address +} + +// NewRecoveryState creates a new RecoveryState using the provided recoveryWindow. Each RecoveryState that is +// subsequently initialized for a particular key scope will receive the same recoveryWindow. +func NewRecoveryState(recoveryWindow uint32) *RecoveryState { + scopes := make(map[waddrmgr.KeyScope]*ScopeRecoveryState) + return &RecoveryState{ + recoveryWindow: recoveryWindow, + scopes: scopes, + watchedOutPoints: make(map[wire.OutPoint]btcaddr.Address), + } +} + +// StateForScope returns a ScopeRecoveryState for the provided key scope. If one does not already exist, a new one will +// be generated with the RecoveryState's recoveryWindow. +func (rs *RecoveryState) StateForScope( + keyScope waddrmgr.KeyScope, +) *ScopeRecoveryState { + // If the account recovery state already exists, return it. + if scopeState, ok := rs.scopes[keyScope]; ok { + return scopeState + } + // Otherwise, initialize the recovery state for this scope with the chosen recovery window. + rs.scopes[keyScope] = NewScopeRecoveryState(rs.recoveryWindow) + return rs.scopes[keyScope] +} + +// WatchedOutPoints returns the global set of outpoints that are known to belong to the wallet during recovery. +func (rs *RecoveryState) WatchedOutPoints() map[wire.OutPoint]btcaddr.Address { + return rs.watchedOutPoints +} + +// AddWatchedOutPoint updates the recovery state's set of known outpoints that we will monitor for spends during +// recovery. +func (rs *RecoveryState) AddWatchedOutPoint( + outPoint *wire.OutPoint, + addr btcaddr.Address, +) { + rs.watchedOutPoints[*outPoint] = addr +} + +// ScopeRecoveryState is used to manage the recovery of addresses generated under a particular BIP32 account. Each +// account tracks both an external and internal branch recovery state, both of which use the same recovery window. +type ScopeRecoveryState struct { + // ExternalBranch is the recovery state of addresses generated for external use, i.e. receiving addresses. + ExternalBranch *BranchRecoveryState + // InternalBranch is the recovery state of addresses generated for internal use, i.e. change addresses. + InternalBranch *BranchRecoveryState +} + +// NewScopeRecoveryState initializes an ScopeRecoveryState with the chosen recovery window. +func NewScopeRecoveryState(recoveryWindow uint32) *ScopeRecoveryState { + return &ScopeRecoveryState{ + ExternalBranch: NewBranchRecoveryState(recoveryWindow), + InternalBranch: NewBranchRecoveryState(recoveryWindow), + } +} + +// BranchRecoveryState maintains the required state in-order to properly recover addresses derived from a particular +// account's internal or external derivation branch. +// +// A branch recovery state supports operations for: +// +// - Expanding the look-ahead horizon based on which indexes have been found. +// +// - Registering derived addresses with indexes within the horizon. +// +// - Reporting an invalid child index that falls into the horizon. +// +// - Reporting that an address has been found. +// +// - Retrieving all currently derived addresses for the branch. +// +// - Looking up a particular address by its child index. +type BranchRecoveryState struct { + // recoveryWindow defines the key-derivation lookahead used when attempting to recover the set of addresses on this + // branch. + recoveryWindow uint32 + // horizion records the highest child index watched by this branch. + horizon uint32 + // nextUnfound maintains the child index of the successor to the highest index that has been found during recovery + // of this branch. + nextUnfound uint32 + // addresses is a map of child index to address for all actively watched addresses belonging to this branch. + addresses map[uint32]btcaddr.Address + // invalidChildren records the set of child indexes that derive to invalid keys. + invalidChildren map[uint32]struct{} +} + +// NewBranchRecoveryState creates a new BranchRecoveryState that can be used to track either the external or internal +// branch of an account's derivation path. +func NewBranchRecoveryState(recoveryWindow uint32) *BranchRecoveryState { + return &BranchRecoveryState{ + recoveryWindow: recoveryWindow, + addresses: make(map[uint32]btcaddr.Address), + invalidChildren: make(map[uint32]struct{}), + } +} + +// ExtendHorizon returns the current horizon and the number of addresses that must be derived in order to maintain the +// desired recovery window. +func (brs *BranchRecoveryState) ExtendHorizon() (uint32, uint32) { + // Compute the new horizon, which should surpass our last found address by the recovery window. + curHorizon := brs.horizon + nInvalid := brs.NumInvalidInHorizon() + minValidHorizon := brs.nextUnfound + brs.recoveryWindow + nInvalid + // If the current horizon is sufficient, we will not have to derive any new keys. + if curHorizon >= minValidHorizon { + return curHorizon, 0 + } + // Otherwise, the number of addresses we should derive corresponds to the delta of the two horizons, and we update + // our new horizon. + delta := minValidHorizon - curHorizon + brs.horizon = minValidHorizon + return curHorizon, delta +} + +// AddAddr adds a freshly derived address from our lookahead into the map of known addresses for this branch. +func (brs *BranchRecoveryState) AddAddr(index uint32, addr btcaddr.Address) { + brs.addresses[index] = addr +} + +// GetAddr returns the address derived from a given child index. +func (brs *BranchRecoveryState) GetAddr(index uint32) btcaddr.Address { + return brs.addresses[index] +} + +// ReportFound updates the last found index if the reported index exceeds the current value. +func (brs *BranchRecoveryState) ReportFound(index uint32) { + if index >= brs.nextUnfound { + brs.nextUnfound = index + 1 + // Prune all invalid child indexes that fall below our last found index. We don't need to keep these entries any + // longer, since they will not affect our required look-ahead. + for childIndex := range brs.invalidChildren { + if childIndex < index { + delete(brs.invalidChildren, childIndex) + } + } + } +} + +// MarkInvalidChild records that a particular child index results in deriving an invalid address. In addition, the +// branch's horizon is increment, as we expect the caller to perform an additional derivation to replace the invalid +// child. This is used to ensure that we are always have the proper lookahead when an invalid child is encountered. +func (brs *BranchRecoveryState) MarkInvalidChild(index uint32) { + brs.invalidChildren[index] = struct{}{} + brs.horizon++ +} + +// NextUnfound returns the child index of the successor to the highest found child index. +func (brs *BranchRecoveryState) NextUnfound() uint32 { + return brs.nextUnfound +} + +// Addrs returns a map of all currently derived child indexes to the their corresponding addresses. +func (brs *BranchRecoveryState) Addrs() map[uint32]btcaddr.Address { + return brs.addresses +} + +// NumInvalidInHorizon computes the number of invalid child indexes that lie between the last found and current horizon. +// This informs how many additional indexes to derive in order to maintain the proper number of valid addresses within +// our horizon. +func (brs *BranchRecoveryState) NumInvalidInHorizon() uint32 { + var nInvalid uint32 + for childIndex := range brs.invalidChildren { + if brs.nextUnfound <= childIndex && childIndex < brs.horizon { + nInvalid++ + } + } + return nInvalid +} diff --git a/cmd/wallet/recovery_test.go b/cmd/wallet/recovery_test.go new file mode 100644 index 0000000..05d1841 --- /dev/null +++ b/cmd/wallet/recovery_test.go @@ -0,0 +1,185 @@ +package wallet_test + +import ( + "runtime" + "testing" + + "github.com/p9c/p9/cmd/wallet" +) + +// Harness holds the BranchRecoveryState being tested, the recovery window being used, provides access to the test +// object, and tracks the expected horizon and next unfound values. +type Harness struct { + t *testing.T + brs *wallet.BranchRecoveryState + recoveryWindow uint32 + expHorizon uint32 + expNextUnfound uint32 +} +type ( + // Stepper is a generic interface that performs an action or assertion against a test Harness. + Stepper interface { + // Apply performs an action or assertion against branch recovery state held by the Harness. The step index is + // provided so that any failures can report which Step failed. + Apply(step int, harness *Harness) + } + // InitialiDelta is a Step that verifies our first attempt to expand the branch recovery state's horizons tells us + // to derive a number of adddresses equal to the recovery window. + InitialDelta struct{} + // CheckDelta is a Step that expands the branch recovery state's horizon, and checks that the returned delta meets + // our expected `delta`. + CheckDelta struct { + delta uint32 + } + // CheckNumInvalid is a Step that asserts that the branch recovery state reports `total` invalid children with the + // current horizon. + CheckNumInvalid struct { + total uint32 + } + // MarkInvalid is a Step that marks the `child` as invalid in the branch recovery state. + MarkInvalid struct { + child uint32 + } + // ReportFound is a Step that reports `child` as being found to the branch recovery state. + ReportFound struct { + child uint32 + } +) + +// Apply extends the current horizon of the branch recovery state, and checks that the returned delta is equal to the +// test's recovery window. If the assertions pass, the harness's expected horizon is increased by the returned delta. +// +// NOTE: This should be used before applying any CheckDelta steps. +func (_ InitialDelta) Apply(i int, h *Harness) { + curHorizon, delta := h.brs.ExtendHorizon() + assertHorizon(h.t, i, curHorizon, h.expHorizon) + assertDelta(h.t, i, delta, h.recoveryWindow) + h.expHorizon += delta +} + +// Apply extends the current horizon of the branch recovery state, and checks that the returned delta is equal to the +// CheckDelta's child value. +func (d CheckDelta) Apply(i int, h *Harness) { + curHorizon, delta := h.brs.ExtendHorizon() + assertHorizon(h.t, i, curHorizon, h.expHorizon) + assertDelta(h.t, i, delta, d.delta) + h.expHorizon += delta +} + +// Apply queries the branch recovery state for the number of invalid children that lie between the last found address +// and the current horizon, and compares that to the CheckNumInvalid's total. +func (m CheckNumInvalid) Apply(i int, h *Harness) { + assertNumInvalid(h.t, i, h.brs.NumInvalidInHorizon(), m.total) +} + +// Apply marks the MarkInvalid's child index as invalid in the branch recovery state, and increments the harness's +// expected horizon. +func (m MarkInvalid) Apply(i int, h *Harness) { + h.brs.MarkInvalidChild(m.child) + h.expHorizon++ +} + +// Apply reports the ReportFound's child index as found in the branch recovery state. If the child index meets or +// exceeds our expected next unfound value, the expected value will be modified to be the child index + 1. Afterwards, +// this step asserts that the branch recovery state's next reported unfound value matches our potentially-updated value. +func (r ReportFound) Apply(i int, h *Harness) { + h.brs.ReportFound(r.child) + if r.child >= h.expNextUnfound { + h.expNextUnfound = r.child + 1 + } + assertNextUnfound(h.t, i, h.brs.NextUnfound(), h.expNextUnfound) +} + +// Compile-time checks to ensure our steps implement the Step interface. +var _ Stepper = InitialDelta{} +var _ Stepper = CheckDelta{} +var _ Stepper = CheckNumInvalid{} +var _ Stepper = MarkInvalid{} +var _ Stepper = ReportFound{} + +// TestBranchRecoveryState walks the BranchRecoveryState through a sequence of steps, verifying that: +// +// - the horizon is properly expanded in response to found addrs +// +// - report found children below or equal to previously found causes no change +// +// - marking invalid children expands the horizon +func TestBranchRecoveryState(t *testing.T) { + const recoveryWindow = 10 + recoverySteps := []Stepper{ + // First, check that expanding our horizon returns exactly the recovery window (10). + InitialDelta{}, + // Expected horizon: 10. Report finding the 2nd addr, this should cause our horizon to expand by 2. + ReportFound{1}, + CheckDelta{2}, + // Expected horizon: 12. Sanity check that expanding again reports zero delta, as nothing has changed. + CheckDelta{0}, + // Now, report finding the 6th addr, which should expand our horizon to 16 with a detla of 4. + ReportFound{5}, + CheckDelta{4}, + // Expected horizon: 16. Sanity check that expanding again reports zero delta, as nothing has changed. + CheckDelta{0}, + // Report finding child index 5 again, nothing should change. + ReportFound{5}, + CheckDelta{0}, + // Report finding a lower index that what was last found, nothing should change. + ReportFound{4}, + CheckDelta{0}, + // Moving on, report finding the 11th addr, which should extend our horizon to 21. + ReportFound{10}, + CheckDelta{5}, + // Expected horizon: 21. Before testing the lookahead expansion when encountering invalid child keys, check that + // we are correctly starting with no invalid keys. + CheckNumInvalid{0}, + // Now that the window has been expanded, simulate deriving invalid keys in range of addrs that are being + // derived for the first time. The horizon will be incremented by one, as the recovery manager is expected to + // try and derive at least the next address. + MarkInvalid{17}, + CheckNumInvalid{1}, + CheckDelta{0}, + // Expected horizon: 22. Chk that deriving a second invalid key shows both invalid indexes currently within + // the horizon. + MarkInvalid{18}, + CheckNumInvalid{2}, + CheckDelta{0}, + // Expected horizon: 23. Lastly, report finding the addr immediately after our two invalid keys. This should + // return our number of invalid keys within the horizon back to 0. + ReportFound{19}, + CheckNumInvalid{0}, + // As the 20-th key was just marked found, our horizon will need to expand to 30. With the horizon at 23, the + // delta returned should be 7. + CheckDelta{7}, + CheckDelta{0}, + // Expected horizon: 30. + } + brs := wallet.NewBranchRecoveryState(recoveryWindow) + harness := &Harness{ + t: t, + brs: brs, + recoveryWindow: recoveryWindow, + } + for i, step := range recoverySteps { + step.Apply(i, harness) + } +} +func assertHorizon(t *testing.T, i int, have, want uint32) { + assertHaveWant(t, i, "incorrect horizon", have, want) +} +func assertDelta(t *testing.T, i int, have, want uint32) { + assertHaveWant(t, i, "incorrect delta", have, want) +} +func assertNextUnfound(t *testing.T, i int, have, want uint32) { + assertHaveWant(t, i, "incorrect next unfound", have, want) +} +func assertNumInvalid(t *testing.T, i int, have, want uint32) { + assertHaveWant(t, i, "incorrect num invalid children", have, want) +} +func assertHaveWant(t *testing.T, i int, msg string, have, want uint32) { + _, _, line, _ := runtime.Caller(2) + if want != have { + t.Fatalf( + "[line: %d, step: %d] %s: got %d, want %d", + line, i, msg, have, want, + ) + } +} diff --git a/cmd/wallet/rescan.go b/cmd/wallet/rescan.go new file mode 100644 index 0000000..3503134 --- /dev/null +++ b/cmd/wallet/rescan.go @@ -0,0 +1,265 @@ +package wallet + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chainclient" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/wire" + "github.com/p9c/p9/pkg/wtxmgr" +) + +// RescanProgressMsg reports the current progress made by a rescan for a set of +// wallet addresses. +type RescanProgressMsg struct { + Addresses []btcaddr.Address + Notification *chainclient.RescanProgress +} + +// RescanFinishedMsg reports the addresses that were rescanned when a +// rescanfinished message was received rescanning a batch of addresses. +type RescanFinishedMsg struct { + Addresses []btcaddr.Address + Notification *chainclient.RescanFinished +} + +// RescanJob is a job to be processed by the RescanManager. The job includes a +// set of wallet addresses, a starting height to begin the rescan, and outpoints +// spendable by the addresses thought to be unspent. After the rescan completes, +// the error result of the rescan RPC is sent on the Err channel. +type RescanJob struct { + InitialSync bool + Addrs []btcaddr.Address + OutPoints map[wire.OutPoint]btcaddr.Address + BlockStamp waddrmgr.BlockStamp + err chan error +} + +// rescanBatch is a collection of one or more RescanJobs that were merged +// together before a rescan is performed. +type rescanBatch struct { + initialSync bool + addrs []btcaddr.Address + outpoints map[wire.OutPoint]btcaddr.Address + bs waddrmgr.BlockStamp + errChans []chan error +} + +// SubmitRescan submits a RescanJob to the RescanManager. A channel is returned +// with the final error of the rescan. The channel is buffered and does not need +// to be read to prevent a deadlock. +func (w *Wallet) SubmitRescan(job *RescanJob) <-chan error { + errChan := make(chan error, 1) + job.err = errChan + w.rescanAddJob <- job + return errChan +} + +// batch creates the rescanBatch for a single rescan job. +func (job *RescanJob) batch() *rescanBatch { + return &rescanBatch{ + initialSync: job.InitialSync, + addrs: job.Addrs, + outpoints: job.OutPoints, + bs: job.BlockStamp, + errChans: []chan error{job.err}, + } +} + +// merge merges the work from k into j, setting the starting height to the +// minimum of the two jobs. This method does not check for duplicate addresses +// or outpoints. +func (b *rescanBatch) merge(job *RescanJob) { + if job.InitialSync { + b.initialSync = true + } + b.addrs = append(b.addrs, job.Addrs...) + for op, addr := range job.OutPoints { + b.outpoints[op] = addr + } + if job.BlockStamp.Height < b.bs.Height { + b.bs = job.BlockStamp + } + b.errChans = append(b.errChans, job.err) +} + +// done iterates through all error channels, duplicating sending the error to +// inform callers that the rescan finished (or could not complete due to an +// error). +func (b *rescanBatch) done(e error) { + for _, c := range b.errChans { + c <- e + } +} + +// rescanBatchHandler handles incoming rescan request, serializing rescan +// submissions, and possibly batching many waiting requests together so they can +// be handled by a single rescan after the current one completes. +func (w *Wallet) rescanBatchHandler() { + var curBatch, nextBatch *rescanBatch + quit := w.quitChan() +out: + for { + select { + case job := <-w.rescanAddJob: + if curBatch == nil { + // Set current batch as this job and send request. + curBatch = job.batch() + w.rescanBatch <- curBatch + } else { + // Create next batch if it doesn't exist, or merge the job. + if nextBatch == nil { + nextBatch = job.batch() + } else { + nextBatch.merge(job) + } + } + case n := <-w.rescanNotifications: + switch n := n.(type) { + case *chainclient.RescanProgress: + if curBatch == nil { + W.Ln( + "received rescan progress notification but no rescan currently running", + ) + continue + } + w.rescanProgress <- &RescanProgressMsg{ + Addresses: curBatch.addrs, + Notification: n, + } + case *chainclient.RescanFinished: + if curBatch == nil { + W.Ln( + "received rescan finished notification but no rescan currently running", + ) + continue + } + w.rescanFinished <- &RescanFinishedMsg{ + Addresses: curBatch.addrs, + Notification: n, + } + curBatch, nextBatch = nextBatch, nil + if curBatch != nil { + w.rescanBatch <- curBatch + } + default: + // Unexpected message + panic(n) + } + case <-quit.Wait(): + break out + } + } + w.wg.Done() +} + +// rescanProgressHandler handles notifications for partially and fully completed +// rescans by marking each rescanned address as partially or fully synced. +func (w *Wallet) rescanProgressHandler() { + quit := w.quitChan() +out: + for { + // These can't be processed out of order since both chans are unbuffured and are + // sent from same context (the batch handler). + select { + case msg := <-w.rescanProgress: + n := msg.Notification + I.F( + "rescanned through block %v (height %d)", + n.Hash, n.Height, + ) + case msg := <-w.rescanFinished: + n := msg.Notification + addrs := msg.Addresses + noun := log.PickNoun(len(addrs), "address", "addresses") + I.F( + "finished rescan for %d %s (synced to block %s, height %d)", + len(addrs), noun, n.Hash, n.Height, + ) + go w.resendUnminedTxs() + case <-quit.Wait(): + break out + } + } + w.wg.Done() +} + +// rescanRPCHandler reads batch jobs sent by rescanBatchHandler and sends the +// RPC requests to perform a rescan. New jobs are not read until a rescan +// finishes. +func (w *Wallet) rescanRPCHandler() { + chainClient, e := w.requireChainClient() + if e != nil { + E.Ln("rescanRPCHandler called without an RPC client", e) + w.wg.Done() + return + } + quit := w.quitChan() +out: + for { + select { + case batch := <-w.rescanBatch: + // Log the newly-started rescan. + numAddrs := len(batch.addrs) + noun := log.PickNoun(numAddrs, "address", "addresses") + I.F( + "started rescan from block %v (height %d) for %d %s", + batch.bs.Hash, batch.bs.Height, numAddrs, noun, + ) + e := chainClient.Rescan( + &batch.bs.Hash, batch.addrs, + batch.outpoints, + ) + if e != nil { + E.F( + "rescan for %d %s failed: %v", numAddrs, noun, e, + ) + } + batch.done(e) + case <-quit.Wait(): + break out + } + } + w.wg.Done() +} + +// Rescan begins a rescan for all active addresses and unspent outputs of a +// wallet. This is intended to be used to sync a wallet back up to the current +// best block in the main chain, and is considered an initial sync rescan. +func (w *Wallet) Rescan(addrs []btcaddr.Address, unspent []wtxmgr.Credit) (e error) { + return w.rescanWithTarget(addrs, unspent, nil) +} + +// rescanWithTarget performs a rescan starting at the optional startStamp. If +// none is provided, the rescan will begin from the manager's sync tip. +func (w *Wallet) rescanWithTarget( + addrs []btcaddr.Address, + unspent []wtxmgr.Credit, startStamp *waddrmgr.BlockStamp, +) (e error) { + outpoints := make(map[wire.OutPoint]btcaddr.Address, len(unspent)) + for _, output := range unspent { + var outputAddrs []btcaddr.Address + _, outputAddrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams, + ) + if e != nil { + return e + } + outpoints[output.OutPoint] = outputAddrs[0] + } + // If a start block stamp was provided, we will use that as the initial starting + // point for the rescan. + if startStamp == nil { + startStamp = &waddrmgr.BlockStamp{} + *startStamp = w.Manager.SyncedTo() + } + job := &RescanJob{ + InitialSync: true, + Addrs: addrs, + OutPoints: outpoints, + BlockStamp: *startStamp, + } + // Submit merged job and block until rescan completes. + return <-w.SubmitRescan(job) +} diff --git a/cmd/wallet/resulttypes.go b/cmd/wallet/resulttypes.go new file mode 100644 index 0000000..6bbc82a --- /dev/null +++ b/cmd/wallet/resulttypes.go @@ -0,0 +1,195 @@ +package wallet + +// +// import ( +// "github.com/p9c/p9/pkg/btcjson" +// ) +// +// // errors are returned as *btcjson.RPCError type +// type ( +// None struct{} +// AddMultiSigAddressRes struct { +// e error +// Res string +// } +// CreateMultiSigRes struct { +// e error +// Res btcjson.CreateMultiSigResult +// } +// DumpPrivKeyRes struct { +// e error +// Res string +// } +// GetAccountRes struct { +// e error +// Res string +// } +// GetAccountAddressRes struct { +// e error +// Res string +// } +// GetAddressesByAccountRes struct { +// e error +// Res []string +// } +// GetBalanceRes struct { +// e error +// Res float64 +// } +// GetBestBlockHashRes struct { +// e error +// Res string +// } +// GetBlockCountRes struct { +// e error +// Res int32 +// } +// GetInfoRes struct { +// e error +// Res btcjson.InfoWalletResult +// } +// GetNewAddressRes struct { +// e error +// Res string +// } +// GetRawChangeAddressRes struct { +// e error +// Res string +// } +// GetReceivedByAccountRes struct { +// e error +// Res float64 +// } +// GetReceivedByAddressRes struct { +// e error +// Res float64 +// } +// GetTransactionRes struct { +// e error +// Res btcjson.GetTransactionResult +// } +// HelpWithChainRPCRes struct { +// e error +// Res string +// } +// HelpNoChainRPCRes struct { +// e error +// Res string +// } +// ImportPrivKeyRes struct { +// e error +// Res None +// } +// KeypoolRefillRes struct { +// e error +// Res None +// } +// ListAccountsRes struct { +// e error +// Res map[string]float64 +// } +// ListLockUnspentRes struct { +// e error +// Res []btcjson.TransactionInput +// } +// ListReceivedByAccountRes struct { +// e error +// Res []btcjson.ListReceivedByAccountResult +// } +// ListReceivedByAddressRes struct { +// e error +// Res btcjson.ListReceivedByAddressResult +// } +// ListSinceBlockRes struct { +// e error +// Res btcjson.ListSinceBlockResult +// } +// ListTransactionsRes struct { +// e error +// Res []btcjson.ListTransactionsResult +// } +// ListUnspentRes struct { +// e error +// Res []btcjson.ListUnspentResult +// } +// LockUnspentRes struct { +// e error +// Res bool +// } +// SendFromRes struct { +// e error +// Res string +// } +// SendManyRes struct { +// e error +// Res string +// } +// SendToAddressRes struct { +// e error +// Res string +// } +// SetTxFeeRes struct { +// e error +// Res bool +// } +// SignMessageRes struct { +// e error +// Res string +// } +// SignRawTransactionRes struct { +// e error +// Res btcjson.SignRawTransactionResult +// } +// ValidateAddressRes struct { +// e error +// Res btcjson.ValidateAddressWalletResult +// } +// VerifyMessageRes struct { +// e error +// Res bool +// } +// WalletLockRes struct { +// e error +// Res None +// } +// WalletPassphraseRes struct { +// e error +// Res None +// } +// WalletPassphraseChangeRes struct { +// e error +// Res None +// } +// CreateNewAccountRes struct { +// e error +// Res None +// } +// GetBestBlockRes struct { +// e error +// Res btcjson.GetBestBlockResult +// } +// GetUnconfirmedBalanceRes struct { +// e error +// Res float64 +// } +// ListAddressTransactionsRes struct { +// e error +// Res []btcjson.ListTransactionsResult +// } +// ListAllTransactionsRes struct { +// e error +// Res []btcjson.ListTransactionsResult +// } +// RenameAccountRes struct { +// e error +// Res None +// } +// WalletIsLockedRes struct { +// e error +// Res bool +// } +// DropWalletHistoryRes struct { +// e error +// Res string +// } +// ) diff --git a/cmd/wallet/rpchandlers.go b/cmd/wallet/rpchandlers.go new file mode 100644 index 0000000..ebce4c2 --- /dev/null +++ b/cmd/wallet/rpchandlers.go @@ -0,0 +1,3468 @@ +// generated by go run ./genapi/.; DO NOT EDIT +// +//go:generate go run ./genapi/. + +package wallet + +import ( + "io" + "net/rpc" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainclient" +) + +// API stores the channel, parameters and result values from calls via the channel +type API struct { + Ch interface{} + Params interface{} + Result interface{} +} + +// CAPI is the central structure for configuration and access to a net/rpc API access endpoint for this RPC API +type CAPI struct { + Timeout time.Duration + quit qu.C +} + +// NewCAPI returns a new CAPI +func NewCAPI(quit qu.C, timeout ...time.Duration) (c *CAPI) { + c = &CAPI{quit: quit} + if len(timeout)>0 { + c.Timeout = timeout[0] + } else { + c.Timeout = time.Second * 5 + } + return +} + +// CAPIClient is a wrapper around RPC calls +type CAPIClient struct { + *rpc.Client +} + +// NewCAPIClient creates a new client for a kopach_worker. Note that any kind of connection can be used here, +// other than the StdConn +func NewCAPIClient(conn io.ReadWriteCloser) *CAPIClient { + return &CAPIClient{rpc.NewClient(conn)} +} + +type ( + // None means no parameters it is not checked so it can be nil + None struct{} + // AddMultiSigAddressRes is the result from a call to AddMultiSigAddress + AddMultiSigAddressRes struct { Res *string; e error } + // CreateMultiSigRes is the result from a call to CreateMultiSig + CreateMultiSigRes struct { Res *btcjson.CreateMultiSigResult; e error } + // CreateNewAccountRes is the result from a call to CreateNewAccount + CreateNewAccountRes struct { Res *None; e error } + // HandleDropWalletHistoryRes is the result from a call to HandleDropWalletHistory + HandleDropWalletHistoryRes struct { Res *string; e error } + // DumpPrivKeyRes is the result from a call to DumpPrivKey + DumpPrivKeyRes struct { Res *string; e error } + // GetAccountRes is the result from a call to GetAccount + GetAccountRes struct { Res *string; e error } + // GetAccountAddressRes is the result from a call to GetAccountAddress + GetAccountAddressRes struct { Res *string; e error } + // GetAddressesByAccountRes is the result from a call to GetAddressesByAccount + GetAddressesByAccountRes struct { Res *[]string; e error } + // GetBalanceRes is the result from a call to GetBalance + GetBalanceRes struct { Res *float64; e error } + // GetBestBlockRes is the result from a call to GetBestBlock + GetBestBlockRes struct { Res *btcjson.GetBestBlockResult; e error } + // GetBestBlockHashRes is the result from a call to GetBestBlockHash + GetBestBlockHashRes struct { Res *string; e error } + // GetBlockCountRes is the result from a call to GetBlockCount + GetBlockCountRes struct { Res *int32; e error } + // GetInfoRes is the result from a call to GetInfo + GetInfoRes struct { Res *btcjson.InfoWalletResult; e error } + // GetNewAddressRes is the result from a call to GetNewAddress + GetNewAddressRes struct { Res *string; e error } + // GetRawChangeAddressRes is the result from a call to GetRawChangeAddress + GetRawChangeAddressRes struct { Res *string; e error } + // GetReceivedByAccountRes is the result from a call to GetReceivedByAccount + GetReceivedByAccountRes struct { Res *float64; e error } + // GetReceivedByAddressRes is the result from a call to GetReceivedByAddress + GetReceivedByAddressRes struct { Res *float64; e error } + // GetTransactionRes is the result from a call to GetTransaction + GetTransactionRes struct { Res *btcjson.GetTransactionResult; e error } + // GetUnconfirmedBalanceRes is the result from a call to GetUnconfirmedBalance + GetUnconfirmedBalanceRes struct { Res *float64; e error } + // HelpNoChainRPCRes is the result from a call to HelpNoChainRPC + HelpNoChainRPCRes struct { Res *string; e error } + // ImportPrivKeyRes is the result from a call to ImportPrivKey + ImportPrivKeyRes struct { Res *None; e error } + // KeypoolRefillRes is the result from a call to KeypoolRefill + KeypoolRefillRes struct { Res *None; e error } + // ListAccountsRes is the result from a call to ListAccounts + ListAccountsRes struct { Res *map[string]float64; e error } + // ListAddressTransactionsRes is the result from a call to ListAddressTransactions + ListAddressTransactionsRes struct { Res *[]btcjson.ListTransactionsResult; e error } + // ListAllTransactionsRes is the result from a call to ListAllTransactions + ListAllTransactionsRes struct { Res *[]btcjson.ListTransactionsResult; e error } + // ListLockUnspentRes is the result from a call to ListLockUnspent + ListLockUnspentRes struct { Res *[]btcjson.TransactionInput; e error } + // ListReceivedByAccountRes is the result from a call to ListReceivedByAccount + ListReceivedByAccountRes struct { Res *[]btcjson.ListReceivedByAccountResult; e error } + // ListReceivedByAddressRes is the result from a call to ListReceivedByAddress + ListReceivedByAddressRes struct { Res *btcjson.ListReceivedByAddressResult; e error } + // ListSinceBlockRes is the result from a call to ListSinceBlock + ListSinceBlockRes struct { Res *btcjson.ListSinceBlockResult; e error } + // ListTransactionsRes is the result from a call to ListTransactions + ListTransactionsRes struct { Res *[]btcjson.ListTransactionsResult; e error } + // ListUnspentRes is the result from a call to ListUnspent + ListUnspentRes struct { Res *[]btcjson.ListUnspentResult; e error } + // RenameAccountRes is the result from a call to RenameAccount + RenameAccountRes struct { Res *None; e error } + // LockUnspentRes is the result from a call to LockUnspent + LockUnspentRes struct { Res *bool; e error } + // SendManyRes is the result from a call to SendMany + SendManyRes struct { Res *string; e error } + // SendToAddressRes is the result from a call to SendToAddress + SendToAddressRes struct { Res *string; e error } + // SetTxFeeRes is the result from a call to SetTxFee + SetTxFeeRes struct { Res *bool; e error } + // SignMessageRes is the result from a call to SignMessage + SignMessageRes struct { Res *string; e error } + // SignRawTransactionRes is the result from a call to SignRawTransaction + SignRawTransactionRes struct { Res *btcjson.SignRawTransactionResult; e error } + // ValidateAddressRes is the result from a call to ValidateAddress + ValidateAddressRes struct { Res *btcjson.ValidateAddressWalletResult; e error } + // VerifyMessageRes is the result from a call to VerifyMessage + VerifyMessageRes struct { Res *bool; e error } + // WalletIsLockedRes is the result from a call to WalletIsLocked + WalletIsLockedRes struct { Res *bool; e error } + // WalletLockRes is the result from a call to WalletLock + WalletLockRes struct { Res *None; e error } + // WalletPassphraseRes is the result from a call to WalletPassphrase + WalletPassphraseRes struct { Res *None; e error } + // WalletPassphraseChangeRes is the result from a call to WalletPassphraseChange + WalletPassphraseChangeRes struct { Res *None; e error } +) + +// RequestHandler is a handler function to handle an unmarshaled and parsed request into a marshalable response. If the +// error is a *json.RPCError or any of the above special error classes, the server will respond with the JSON-RPC +// appropriate error code. All other errors use the wallet catch-all error code, json.ErrRPCWallet. +type RequestHandler func(interface{}, *Wallet, + ...*chainclient.RPCClient) (interface{}, error) + +// RPCHandlers is all of the RPC calls available +// +// - Handler is the handler function +// +// - Call is a channel carrying a struct containing parameters and error that is listened to in RunAPI to dispatch the +// calls +// +// - Result is a bundle of command parameters and a channel that the result will be sent back on +// +// Get and save the Result function's return, and you can then call the call functions check, result and wait functions +// for asynchronous and synchronous calls to RPC functions +var RPCHandlers = map[string]struct { + Handler RequestHandler + // Function variables cannot be compared against anything but nil, so use a boolean to record whether help + // generation is necessary. This is used by the tests to ensure that help can be generated for every implemented + // method. + // + // A single map and this bool is here is used rather than several maps for the unimplemented handlers so every + // method has exactly one handler function. + // + // The Return field returns a new channel of the type returned by this function. This makes it possible to use this + // for callers to receive a response in the cpc library which implements the functions as channel pipes + NoHelp bool + Call chan API + Params interface{} + Result func() API +}{ + "addmultisigaddress":{ + Handler: AddMultiSigAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan AddMultiSigAddressRes)} }}, + "createmultisig":{ + Handler: CreateMultiSig, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan CreateMultiSigRes)} }}, + "createnewaccount":{ + Handler: CreateNewAccount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan CreateNewAccountRes)} }}, + "dropwallethistory":{ + Handler: HandleDropWalletHistory, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan HandleDropWalletHistoryRes)} }}, + "dumpprivkey":{ + Handler: DumpPrivKey, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan DumpPrivKeyRes)} }}, + "getaccount":{ + Handler: GetAccount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetAccountRes)} }}, + "getaccountaddress":{ + Handler: GetAccountAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetAccountAddressRes)} }}, + "getaddressesbyaccount":{ + Handler: GetAddressesByAccount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetAddressesByAccountRes)} }}, + "getbalance":{ + Handler: GetBalance, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetBalanceRes)} }}, + "getbestblock":{ + Handler: GetBestBlock, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetBestBlockRes)} }}, + "getbestblockhash":{ + Handler: GetBestBlockHash, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetBestBlockHashRes)} }}, + "getblockcount":{ + Handler: GetBlockCount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetBlockCountRes)} }}, + "getinfo":{ + Handler: GetInfo, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetInfoRes)} }}, + "getnewaddress":{ + Handler: GetNewAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetNewAddressRes)} }}, + "getrawchangeaddress":{ + Handler: GetRawChangeAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetRawChangeAddressRes)} }}, + "getreceivedbyaccount":{ + Handler: GetReceivedByAccount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetReceivedByAccountRes)} }}, + "getreceivedbyaddress":{ + Handler: GetReceivedByAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetReceivedByAddressRes)} }}, + "gettransaction":{ + Handler: GetTransaction, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetTransactionRes)} }}, + "getunconfirmedbalance":{ + Handler: GetUnconfirmedBalance, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan GetUnconfirmedBalanceRes)} }}, + "help":{ + Handler: HelpNoChainRPC, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan HelpNoChainRPCRes)} }}, + "importprivkey":{ + Handler: ImportPrivKey, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ImportPrivKeyRes)} }}, + "keypoolrefill":{ + Handler: KeypoolRefill, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan KeypoolRefillRes)} }}, + "listaccounts":{ + Handler: ListAccounts, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListAccountsRes)} }}, + "listaddresstransactions":{ + Handler: ListAddressTransactions, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListAddressTransactionsRes)} }}, + "listalltransactions":{ + Handler: ListAllTransactions, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListAllTransactionsRes)} }}, + "listlockunspent":{ + Handler: ListLockUnspent, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListLockUnspentRes)} }}, + "listreceivedbyaccount":{ + Handler: ListReceivedByAccount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListReceivedByAccountRes)} }}, + "listreceivedbyaddress":{ + Handler: ListReceivedByAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListReceivedByAddressRes)} }}, + "listsinceblock":{ + Handler: ListSinceBlock, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListSinceBlockRes)} }}, + "listtransactions":{ + Handler: ListTransactions, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListTransactionsRes)} }}, + "listunspent":{ + Handler: ListUnspent, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ListUnspentRes)} }}, + "renameaccount":{ + Handler: RenameAccount, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan RenameAccountRes)} }}, + "sendfrom":{ + Handler: LockUnspent, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan LockUnspentRes)} }}, + "sendmany":{ + Handler: SendMany, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan SendManyRes)} }}, + "sendtoaddress":{ + Handler: SendToAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan SendToAddressRes)} }}, + "settxfee":{ + Handler: SetTxFee, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan SetTxFeeRes)} }}, + "signmessage":{ + Handler: SignMessage, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan SignMessageRes)} }}, + "signrawtransaction":{ + Handler: SignRawTransaction, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan SignRawTransactionRes)} }}, + "validateaddress":{ + Handler: ValidateAddress, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan ValidateAddressRes)} }}, + "verifymessage":{ + Handler: VerifyMessage, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan VerifyMessageRes)} }}, + "walletislocked":{ + Handler: WalletIsLocked, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan WalletIsLockedRes)} }}, + "walletlock":{ + Handler: WalletLock, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan WalletLockRes)} }}, + "walletpassphrase":{ + Handler: WalletPassphrase, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan WalletPassphraseRes)} }}, + "walletpassphrasechange":{ + Handler: WalletPassphraseChange, Call: make(chan API, 32), + Result: func() API { return API{Ch: make(chan WalletPassphraseChangeRes)} }}, + +} + +// API functions +// +// The functions here provide access to the RPC through a convenient set of functions generated for each call in the RPC +// API to request, check for, access the results and wait on results + + +// AddMultiSigAddress calls the method with the given parameters +func (a API) AddMultiSigAddress(cmd *btcjson.AddMultisigAddressCmd) (e error) { + RPCHandlers["addmultisigaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// AddMultiSigAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) AddMultiSigAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan AddMultiSigAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// AddMultiSigAddressGetRes returns a pointer to the value in the Result field +func (a API) AddMultiSigAddressGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// AddMultiSigAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) AddMultiSigAddressWait(cmd *btcjson.AddMultisigAddressCmd) (out *string, e error) { + RPCHandlers["addmultisigaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan AddMultiSigAddressRes): + out, e = o.Res, o.e + } + return +} + +// CreateMultiSig calls the method with the given parameters +func (a API) CreateMultiSig(cmd *btcjson.CreateMultisigCmd) (e error) { + RPCHandlers["createmultisig"].Call <- API{a.Ch, cmd, nil} + return +} + +// CreateMultiSigCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) CreateMultiSigCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan CreateMultiSigRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// CreateMultiSigGetRes returns a pointer to the value in the Result field +func (a API) CreateMultiSigGetRes() (out *btcjson.CreateMultiSigResult, e error) { + out, _ = a.Result.(*btcjson.CreateMultiSigResult) + e, _ = a.Result.(error) + return +} + +// CreateMultiSigWait calls the method and blocks until it returns or 5 seconds passes +func (a API) CreateMultiSigWait(cmd *btcjson.CreateMultisigCmd) (out *btcjson.CreateMultiSigResult, e error) { + RPCHandlers["createmultisig"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan CreateMultiSigRes): + out, e = o.Res, o.e + } + return +} + +// CreateNewAccount calls the method with the given parameters +func (a API) CreateNewAccount(cmd *btcjson.CreateNewAccountCmd) (e error) { + RPCHandlers["createnewaccount"].Call <- API{a.Ch, cmd, nil} + return +} + +// CreateNewAccountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) CreateNewAccountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan CreateNewAccountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// CreateNewAccountGetRes returns a pointer to the value in the Result field +func (a API) CreateNewAccountGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// CreateNewAccountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) CreateNewAccountWait(cmd *btcjson.CreateNewAccountCmd) (out *None, e error) { + RPCHandlers["createnewaccount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan CreateNewAccountRes): + out, e = o.Res, o.e + } + return +} + +// HandleDropWalletHistory calls the method with the given parameters +func (a API) HandleDropWalletHistory(cmd *None) (e error) { + RPCHandlers["dropwallethistory"].Call <- API{a.Ch, cmd, nil} + return +} + +// HandleDropWalletHistoryCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) HandleDropWalletHistoryCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan HandleDropWalletHistoryRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// HandleDropWalletHistoryGetRes returns a pointer to the value in the Result field +func (a API) HandleDropWalletHistoryGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// HandleDropWalletHistoryWait calls the method and blocks until it returns or 5 seconds passes +func (a API) HandleDropWalletHistoryWait(cmd *None) (out *string, e error) { + RPCHandlers["dropwallethistory"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan HandleDropWalletHistoryRes): + out, e = o.Res, o.e + } + return +} + +// DumpPrivKey calls the method with the given parameters +func (a API) DumpPrivKey(cmd *btcjson.DumpPrivKeyCmd) (e error) { + RPCHandlers["dumpprivkey"].Call <- API{a.Ch, cmd, nil} + return +} + +// DumpPrivKeyCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) DumpPrivKeyCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan DumpPrivKeyRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// DumpPrivKeyGetRes returns a pointer to the value in the Result field +func (a API) DumpPrivKeyGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// DumpPrivKeyWait calls the method and blocks until it returns or 5 seconds passes +func (a API) DumpPrivKeyWait(cmd *btcjson.DumpPrivKeyCmd) (out *string, e error) { + RPCHandlers["dumpprivkey"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan DumpPrivKeyRes): + out, e = o.Res, o.e + } + return +} + +// GetAccount calls the method with the given parameters +func (a API) GetAccount(cmd *btcjson.GetAccountCmd) (e error) { + RPCHandlers["getaccount"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetAccountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetAccountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetAccountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetAccountGetRes returns a pointer to the value in the Result field +func (a API) GetAccountGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// GetAccountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetAccountWait(cmd *btcjson.GetAccountCmd) (out *string, e error) { + RPCHandlers["getaccount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetAccountRes): + out, e = o.Res, o.e + } + return +} + +// GetAccountAddress calls the method with the given parameters +func (a API) GetAccountAddress(cmd *btcjson.GetAccountAddressCmd) (e error) { + RPCHandlers["getaccountaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetAccountAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetAccountAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetAccountAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetAccountAddressGetRes returns a pointer to the value in the Result field +func (a API) GetAccountAddressGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// GetAccountAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetAccountAddressWait(cmd *btcjson.GetAccountAddressCmd) (out *string, e error) { + RPCHandlers["getaccountaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetAccountAddressRes): + out, e = o.Res, o.e + } + return +} + +// GetAddressesByAccount calls the method with the given parameters +func (a API) GetAddressesByAccount(cmd *btcjson.GetAddressesByAccountCmd) (e error) { + RPCHandlers["getaddressesbyaccount"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetAddressesByAccountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetAddressesByAccountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetAddressesByAccountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetAddressesByAccountGetRes returns a pointer to the value in the Result field +func (a API) GetAddressesByAccountGetRes() (out *[]string, e error) { + out, _ = a.Result.(*[]string) + e, _ = a.Result.(error) + return +} + +// GetAddressesByAccountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetAddressesByAccountWait(cmd *btcjson.GetAddressesByAccountCmd) (out *[]string, e error) { + RPCHandlers["getaddressesbyaccount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetAddressesByAccountRes): + out, e = o.Res, o.e + } + return +} + +// GetBalance calls the method with the given parameters +func (a API) GetBalance(cmd *btcjson.GetBalanceCmd) (e error) { + RPCHandlers["getbalance"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetBalanceCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetBalanceCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetBalanceRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetBalanceGetRes returns a pointer to the value in the Result field +func (a API) GetBalanceGetRes() (out *float64, e error) { + out, _ = a.Result.(*float64) + e, _ = a.Result.(error) + return +} + +// GetBalanceWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetBalanceWait(cmd *btcjson.GetBalanceCmd) (out *float64, e error) { + RPCHandlers["getbalance"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetBalanceRes): + out, e = o.Res, o.e + } + return +} + +// GetBestBlock calls the method with the given parameters +func (a API) GetBestBlock(cmd *None) (e error) { + RPCHandlers["getbestblock"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetBestBlockCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetBestBlockCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetBestBlockRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetBestBlockGetRes returns a pointer to the value in the Result field +func (a API) GetBestBlockGetRes() (out *btcjson.GetBestBlockResult, e error) { + out, _ = a.Result.(*btcjson.GetBestBlockResult) + e, _ = a.Result.(error) + return +} + +// GetBestBlockWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetBestBlockWait(cmd *None) (out *btcjson.GetBestBlockResult, e error) { + RPCHandlers["getbestblock"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetBestBlockRes): + out, e = o.Res, o.e + } + return +} + +// GetBestBlockHash calls the method with the given parameters +func (a API) GetBestBlockHash(cmd *None) (e error) { + RPCHandlers["getbestblockhash"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetBestBlockHashCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetBestBlockHashCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetBestBlockHashRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetBestBlockHashGetRes returns a pointer to the value in the Result field +func (a API) GetBestBlockHashGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// GetBestBlockHashWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetBestBlockHashWait(cmd *None) (out *string, e error) { + RPCHandlers["getbestblockhash"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetBestBlockHashRes): + out, e = o.Res, o.e + } + return +} + +// GetBlockCount calls the method with the given parameters +func (a API) GetBlockCount(cmd *None) (e error) { + RPCHandlers["getblockcount"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetBlockCountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetBlockCountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetBlockCountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetBlockCountGetRes returns a pointer to the value in the Result field +func (a API) GetBlockCountGetRes() (out *int32, e error) { + out, _ = a.Result.(*int32) + e, _ = a.Result.(error) + return +} + +// GetBlockCountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetBlockCountWait(cmd *None) (out *int32, e error) { + RPCHandlers["getblockcount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetBlockCountRes): + out, e = o.Res, o.e + } + return +} + +// GetInfo calls the method with the given parameters +func (a API) GetInfo(cmd *None) (e error) { + RPCHandlers["getinfo"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetInfoCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetInfoCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetInfoRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetInfoGetRes returns a pointer to the value in the Result field +func (a API) GetInfoGetRes() (out *btcjson.InfoWalletResult, e error) { + out, _ = a.Result.(*btcjson.InfoWalletResult) + e, _ = a.Result.(error) + return +} + +// GetInfoWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetInfoWait(cmd *None) (out *btcjson.InfoWalletResult, e error) { + RPCHandlers["getinfo"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetInfoRes): + out, e = o.Res, o.e + } + return +} + +// GetNewAddress calls the method with the given parameters +func (a API) GetNewAddress(cmd *btcjson.GetNewAddressCmd) (e error) { + RPCHandlers["getnewaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetNewAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetNewAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetNewAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetNewAddressGetRes returns a pointer to the value in the Result field +func (a API) GetNewAddressGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// GetNewAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetNewAddressWait(cmd *btcjson.GetNewAddressCmd) (out *string, e error) { + RPCHandlers["getnewaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetNewAddressRes): + out, e = o.Res, o.e + } + return +} + +// GetRawChangeAddress calls the method with the given parameters +func (a API) GetRawChangeAddress(cmd *btcjson.GetRawChangeAddressCmd) (e error) { + RPCHandlers["getrawchangeaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetRawChangeAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetRawChangeAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetRawChangeAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetRawChangeAddressGetRes returns a pointer to the value in the Result field +func (a API) GetRawChangeAddressGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// GetRawChangeAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetRawChangeAddressWait(cmd *btcjson.GetRawChangeAddressCmd) (out *string, e error) { + RPCHandlers["getrawchangeaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetRawChangeAddressRes): + out, e = o.Res, o.e + } + return +} + +// GetReceivedByAccount calls the method with the given parameters +func (a API) GetReceivedByAccount(cmd *btcjson.GetReceivedByAccountCmd) (e error) { + RPCHandlers["getreceivedbyaccount"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetReceivedByAccountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetReceivedByAccountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetReceivedByAccountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetReceivedByAccountGetRes returns a pointer to the value in the Result field +func (a API) GetReceivedByAccountGetRes() (out *float64, e error) { + out, _ = a.Result.(*float64) + e, _ = a.Result.(error) + return +} + +// GetReceivedByAccountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetReceivedByAccountWait(cmd *btcjson.GetReceivedByAccountCmd) (out *float64, e error) { + RPCHandlers["getreceivedbyaccount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetReceivedByAccountRes): + out, e = o.Res, o.e + } + return +} + +// GetReceivedByAddress calls the method with the given parameters +func (a API) GetReceivedByAddress(cmd *btcjson.GetReceivedByAddressCmd) (e error) { + RPCHandlers["getreceivedbyaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetReceivedByAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetReceivedByAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetReceivedByAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetReceivedByAddressGetRes returns a pointer to the value in the Result field +func (a API) GetReceivedByAddressGetRes() (out *float64, e error) { + out, _ = a.Result.(*float64) + e, _ = a.Result.(error) + return +} + +// GetReceivedByAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetReceivedByAddressWait(cmd *btcjson.GetReceivedByAddressCmd) (out *float64, e error) { + RPCHandlers["getreceivedbyaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetReceivedByAddressRes): + out, e = o.Res, o.e + } + return +} + +// GetTransaction calls the method with the given parameters +func (a API) GetTransaction(cmd *btcjson.GetTransactionCmd) (e error) { + RPCHandlers["gettransaction"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetTransactionCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetTransactionCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetTransactionRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetTransactionGetRes returns a pointer to the value in the Result field +func (a API) GetTransactionGetRes() (out *btcjson.GetTransactionResult, e error) { + out, _ = a.Result.(*btcjson.GetTransactionResult) + e, _ = a.Result.(error) + return +} + +// GetTransactionWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetTransactionWait(cmd *btcjson.GetTransactionCmd) (out *btcjson.GetTransactionResult, e error) { + RPCHandlers["gettransaction"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetTransactionRes): + out, e = o.Res, o.e + } + return +} + +// GetUnconfirmedBalance calls the method with the given parameters +func (a API) GetUnconfirmedBalance(cmd *btcjson.GetUnconfirmedBalanceCmd) (e error) { + RPCHandlers["getunconfirmedbalance"].Call <- API{a.Ch, cmd, nil} + return +} + +// GetUnconfirmedBalanceCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) GetUnconfirmedBalanceCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan GetUnconfirmedBalanceRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// GetUnconfirmedBalanceGetRes returns a pointer to the value in the Result field +func (a API) GetUnconfirmedBalanceGetRes() (out *float64, e error) { + out, _ = a.Result.(*float64) + e, _ = a.Result.(error) + return +} + +// GetUnconfirmedBalanceWait calls the method and blocks until it returns or 5 seconds passes +func (a API) GetUnconfirmedBalanceWait(cmd *btcjson.GetUnconfirmedBalanceCmd) (out *float64, e error) { + RPCHandlers["getunconfirmedbalance"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan GetUnconfirmedBalanceRes): + out, e = o.Res, o.e + } + return +} + +// HelpNoChainRPC calls the method with the given parameters +func (a API) HelpNoChainRPC(cmd btcjson.HelpCmd) (e error) { + RPCHandlers["help"].Call <- API{a.Ch, cmd, nil} + return +} + +// HelpNoChainRPCCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) HelpNoChainRPCCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan HelpNoChainRPCRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// HelpNoChainRPCGetRes returns a pointer to the value in the Result field +func (a API) HelpNoChainRPCGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// HelpNoChainRPCWait calls the method and blocks until it returns or 5 seconds passes +func (a API) HelpNoChainRPCWait(cmd btcjson.HelpCmd) (out *string, e error) { + RPCHandlers["help"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan HelpNoChainRPCRes): + out, e = o.Res, o.e + } + return +} + +// ImportPrivKey calls the method with the given parameters +func (a API) ImportPrivKey(cmd *btcjson.ImportPrivKeyCmd) (e error) { + RPCHandlers["importprivkey"].Call <- API{a.Ch, cmd, nil} + return +} + +// ImportPrivKeyCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ImportPrivKeyCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ImportPrivKeyRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ImportPrivKeyGetRes returns a pointer to the value in the Result field +func (a API) ImportPrivKeyGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// ImportPrivKeyWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ImportPrivKeyWait(cmd *btcjson.ImportPrivKeyCmd) (out *None, e error) { + RPCHandlers["importprivkey"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ImportPrivKeyRes): + out, e = o.Res, o.e + } + return +} + +// KeypoolRefill calls the method with the given parameters +func (a API) KeypoolRefill(cmd *None) (e error) { + RPCHandlers["keypoolrefill"].Call <- API{a.Ch, cmd, nil} + return +} + +// KeypoolRefillCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) KeypoolRefillCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan KeypoolRefillRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// KeypoolRefillGetRes returns a pointer to the value in the Result field +func (a API) KeypoolRefillGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// KeypoolRefillWait calls the method and blocks until it returns or 5 seconds passes +func (a API) KeypoolRefillWait(cmd *None) (out *None, e error) { + RPCHandlers["keypoolrefill"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan KeypoolRefillRes): + out, e = o.Res, o.e + } + return +} + +// ListAccounts calls the method with the given parameters +func (a API) ListAccounts(cmd *btcjson.ListAccountsCmd) (e error) { + RPCHandlers["listaccounts"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListAccountsCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListAccountsCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListAccountsRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListAccountsGetRes returns a pointer to the value in the Result field +func (a API) ListAccountsGetRes() (out *map[string]float64, e error) { + out, _ = a.Result.(*map[string]float64) + e, _ = a.Result.(error) + return +} + +// ListAccountsWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListAccountsWait(cmd *btcjson.ListAccountsCmd) (out *map[string]float64, e error) { + RPCHandlers["listaccounts"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListAccountsRes): + out, e = o.Res, o.e + } + return +} + +// ListAddressTransactions calls the method with the given parameters +func (a API) ListAddressTransactions(cmd *btcjson.ListAddressTransactionsCmd) (e error) { + RPCHandlers["listaddresstransactions"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListAddressTransactionsCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListAddressTransactionsCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListAddressTransactionsRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListAddressTransactionsGetRes returns a pointer to the value in the Result field +func (a API) ListAddressTransactionsGetRes() (out *[]btcjson.ListTransactionsResult, e error) { + out, _ = a.Result.(*[]btcjson.ListTransactionsResult) + e, _ = a.Result.(error) + return +} + +// ListAddressTransactionsWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListAddressTransactionsWait(cmd *btcjson.ListAddressTransactionsCmd) (out *[]btcjson.ListTransactionsResult, e error) { + RPCHandlers["listaddresstransactions"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListAddressTransactionsRes): + out, e = o.Res, o.e + } + return +} + +// ListAllTransactions calls the method with the given parameters +func (a API) ListAllTransactions(cmd *btcjson.ListAllTransactionsCmd) (e error) { + RPCHandlers["listalltransactions"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListAllTransactionsCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListAllTransactionsCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListAllTransactionsRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListAllTransactionsGetRes returns a pointer to the value in the Result field +func (a API) ListAllTransactionsGetRes() (out *[]btcjson.ListTransactionsResult, e error) { + out, _ = a.Result.(*[]btcjson.ListTransactionsResult) + e, _ = a.Result.(error) + return +} + +// ListAllTransactionsWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListAllTransactionsWait(cmd *btcjson.ListAllTransactionsCmd) (out *[]btcjson.ListTransactionsResult, e error) { + RPCHandlers["listalltransactions"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListAllTransactionsRes): + out, e = o.Res, o.e + } + return +} + +// ListLockUnspent calls the method with the given parameters +func (a API) ListLockUnspent(cmd *None) (e error) { + RPCHandlers["listlockunspent"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListLockUnspentCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListLockUnspentCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListLockUnspentRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListLockUnspentGetRes returns a pointer to the value in the Result field +func (a API) ListLockUnspentGetRes() (out *[]btcjson.TransactionInput, e error) { + out, _ = a.Result.(*[]btcjson.TransactionInput) + e, _ = a.Result.(error) + return +} + +// ListLockUnspentWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListLockUnspentWait(cmd *None) (out *[]btcjson.TransactionInput, e error) { + RPCHandlers["listlockunspent"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListLockUnspentRes): + out, e = o.Res, o.e + } + return +} + +// ListReceivedByAccount calls the method with the given parameters +func (a API) ListReceivedByAccount(cmd *btcjson.ListReceivedByAccountCmd) (e error) { + RPCHandlers["listreceivedbyaccount"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListReceivedByAccountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListReceivedByAccountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListReceivedByAccountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListReceivedByAccountGetRes returns a pointer to the value in the Result field +func (a API) ListReceivedByAccountGetRes() (out *[]btcjson.ListReceivedByAccountResult, e error) { + out, _ = a.Result.(*[]btcjson.ListReceivedByAccountResult) + e, _ = a.Result.(error) + return +} + +// ListReceivedByAccountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListReceivedByAccountWait(cmd *btcjson.ListReceivedByAccountCmd) (out *[]btcjson.ListReceivedByAccountResult, e error) { + RPCHandlers["listreceivedbyaccount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListReceivedByAccountRes): + out, e = o.Res, o.e + } + return +} + +// ListReceivedByAddress calls the method with the given parameters +func (a API) ListReceivedByAddress(cmd *btcjson.ListReceivedByAddressCmd) (e error) { + RPCHandlers["listreceivedbyaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListReceivedByAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListReceivedByAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListReceivedByAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListReceivedByAddressGetRes returns a pointer to the value in the Result field +func (a API) ListReceivedByAddressGetRes() (out *btcjson.ListReceivedByAddressResult, e error) { + out, _ = a.Result.(*btcjson.ListReceivedByAddressResult) + e, _ = a.Result.(error) + return +} + +// ListReceivedByAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListReceivedByAddressWait(cmd *btcjson.ListReceivedByAddressCmd) (out *btcjson.ListReceivedByAddressResult, e error) { + RPCHandlers["listreceivedbyaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListReceivedByAddressRes): + out, e = o.Res, o.e + } + return +} + +// ListSinceBlock calls the method with the given parameters +func (a API) ListSinceBlock(cmd btcjson.ListSinceBlockCmd) (e error) { + RPCHandlers["listsinceblock"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListSinceBlockCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListSinceBlockCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListSinceBlockRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListSinceBlockGetRes returns a pointer to the value in the Result field +func (a API) ListSinceBlockGetRes() (out *btcjson.ListSinceBlockResult, e error) { + out, _ = a.Result.(*btcjson.ListSinceBlockResult) + e, _ = a.Result.(error) + return +} + +// ListSinceBlockWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListSinceBlockWait(cmd btcjson.ListSinceBlockCmd) (out *btcjson.ListSinceBlockResult, e error) { + RPCHandlers["listsinceblock"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListSinceBlockRes): + out, e = o.Res, o.e + } + return +} + +// ListTransactions calls the method with the given parameters +func (a API) ListTransactions(cmd *btcjson.ListTransactionsCmd) (e error) { + RPCHandlers["listtransactions"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListTransactionsCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListTransactionsCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListTransactionsRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListTransactionsGetRes returns a pointer to the value in the Result field +func (a API) ListTransactionsGetRes() (out *[]btcjson.ListTransactionsResult, e error) { + out, _ = a.Result.(*[]btcjson.ListTransactionsResult) + e, _ = a.Result.(error) + return +} + +// ListTransactionsWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListTransactionsWait(cmd *btcjson.ListTransactionsCmd) (out *[]btcjson.ListTransactionsResult, e error) { + RPCHandlers["listtransactions"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListTransactionsRes): + out, e = o.Res, o.e + } + return +} + +// ListUnspent calls the method with the given parameters +func (a API) ListUnspent(cmd *btcjson.ListUnspentCmd) (e error) { + RPCHandlers["listunspent"].Call <- API{a.Ch, cmd, nil} + return +} + +// ListUnspentCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ListUnspentCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ListUnspentRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ListUnspentGetRes returns a pointer to the value in the Result field +func (a API) ListUnspentGetRes() (out *[]btcjson.ListUnspentResult, e error) { + out, _ = a.Result.(*[]btcjson.ListUnspentResult) + e, _ = a.Result.(error) + return +} + +// ListUnspentWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ListUnspentWait(cmd *btcjson.ListUnspentCmd) (out *[]btcjson.ListUnspentResult, e error) { + RPCHandlers["listunspent"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ListUnspentRes): + out, e = o.Res, o.e + } + return +} + +// RenameAccount calls the method with the given parameters +func (a API) RenameAccount(cmd *btcjson.RenameAccountCmd) (e error) { + RPCHandlers["renameaccount"].Call <- API{a.Ch, cmd, nil} + return +} + +// RenameAccountCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) RenameAccountCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan RenameAccountRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// RenameAccountGetRes returns a pointer to the value in the Result field +func (a API) RenameAccountGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// RenameAccountWait calls the method and blocks until it returns or 5 seconds passes +func (a API) RenameAccountWait(cmd *btcjson.RenameAccountCmd) (out *None, e error) { + RPCHandlers["renameaccount"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan RenameAccountRes): + out, e = o.Res, o.e + } + return +} + +// LockUnspent calls the method with the given parameters +func (a API) LockUnspent(cmd btcjson.LockUnspentCmd) (e error) { + RPCHandlers["sendfrom"].Call <- API{a.Ch, cmd, nil} + return +} + +// LockUnspentCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) LockUnspentCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan LockUnspentRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// LockUnspentGetRes returns a pointer to the value in the Result field +func (a API) LockUnspentGetRes() (out *bool, e error) { + out, _ = a.Result.(*bool) + e, _ = a.Result.(error) + return +} + +// LockUnspentWait calls the method and blocks until it returns or 5 seconds passes +func (a API) LockUnspentWait(cmd btcjson.LockUnspentCmd) (out *bool, e error) { + RPCHandlers["sendfrom"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan LockUnspentRes): + out, e = o.Res, o.e + } + return +} + +// SendMany calls the method with the given parameters +func (a API) SendMany(cmd *btcjson.SendManyCmd) (e error) { + RPCHandlers["sendmany"].Call <- API{a.Ch, cmd, nil} + return +} + +// SendManyCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) SendManyCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan SendManyRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// SendManyGetRes returns a pointer to the value in the Result field +func (a API) SendManyGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// SendManyWait calls the method and blocks until it returns or 5 seconds passes +func (a API) SendManyWait(cmd *btcjson.SendManyCmd) (out *string, e error) { + RPCHandlers["sendmany"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan SendManyRes): + out, e = o.Res, o.e + } + return +} + +// SendToAddress calls the method with the given parameters +func (a API) SendToAddress(cmd *btcjson.SendToAddressCmd) (e error) { + RPCHandlers["sendtoaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// SendToAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) SendToAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan SendToAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// SendToAddressGetRes returns a pointer to the value in the Result field +func (a API) SendToAddressGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// SendToAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) SendToAddressWait(cmd *btcjson.SendToAddressCmd) (out *string, e error) { + RPCHandlers["sendtoaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan SendToAddressRes): + out, e = o.Res, o.e + } + return +} + +// SetTxFee calls the method with the given parameters +func (a API) SetTxFee(cmd *btcjson.SetTxFeeCmd) (e error) { + RPCHandlers["settxfee"].Call <- API{a.Ch, cmd, nil} + return +} + +// SetTxFeeCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) SetTxFeeCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan SetTxFeeRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// SetTxFeeGetRes returns a pointer to the value in the Result field +func (a API) SetTxFeeGetRes() (out *bool, e error) { + out, _ = a.Result.(*bool) + e, _ = a.Result.(error) + return +} + +// SetTxFeeWait calls the method and blocks until it returns or 5 seconds passes +func (a API) SetTxFeeWait(cmd *btcjson.SetTxFeeCmd) (out *bool, e error) { + RPCHandlers["settxfee"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan SetTxFeeRes): + out, e = o.Res, o.e + } + return +} + +// SignMessage calls the method with the given parameters +func (a API) SignMessage(cmd *btcjson.SignMessageCmd) (e error) { + RPCHandlers["signmessage"].Call <- API{a.Ch, cmd, nil} + return +} + +// SignMessageCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) SignMessageCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan SignMessageRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// SignMessageGetRes returns a pointer to the value in the Result field +func (a API) SignMessageGetRes() (out *string, e error) { + out, _ = a.Result.(*string) + e, _ = a.Result.(error) + return +} + +// SignMessageWait calls the method and blocks until it returns or 5 seconds passes +func (a API) SignMessageWait(cmd *btcjson.SignMessageCmd) (out *string, e error) { + RPCHandlers["signmessage"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan SignMessageRes): + out, e = o.Res, o.e + } + return +} + +// SignRawTransaction calls the method with the given parameters +func (a API) SignRawTransaction(cmd btcjson.SignRawTransactionCmd) (e error) { + RPCHandlers["signrawtransaction"].Call <- API{a.Ch, cmd, nil} + return +} + +// SignRawTransactionCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) SignRawTransactionCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan SignRawTransactionRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// SignRawTransactionGetRes returns a pointer to the value in the Result field +func (a API) SignRawTransactionGetRes() (out *btcjson.SignRawTransactionResult, e error) { + out, _ = a.Result.(*btcjson.SignRawTransactionResult) + e, _ = a.Result.(error) + return +} + +// SignRawTransactionWait calls the method and blocks until it returns or 5 seconds passes +func (a API) SignRawTransactionWait(cmd btcjson.SignRawTransactionCmd) (out *btcjson.SignRawTransactionResult, e error) { + RPCHandlers["signrawtransaction"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan SignRawTransactionRes): + out, e = o.Res, o.e + } + return +} + +// ValidateAddress calls the method with the given parameters +func (a API) ValidateAddress(cmd *btcjson.ValidateAddressCmd) (e error) { + RPCHandlers["validateaddress"].Call <- API{a.Ch, cmd, nil} + return +} + +// ValidateAddressCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) ValidateAddressCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan ValidateAddressRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// ValidateAddressGetRes returns a pointer to the value in the Result field +func (a API) ValidateAddressGetRes() (out *btcjson.ValidateAddressWalletResult, e error) { + out, _ = a.Result.(*btcjson.ValidateAddressWalletResult) + e, _ = a.Result.(error) + return +} + +// ValidateAddressWait calls the method and blocks until it returns or 5 seconds passes +func (a API) ValidateAddressWait(cmd *btcjson.ValidateAddressCmd) (out *btcjson.ValidateAddressWalletResult, e error) { + RPCHandlers["validateaddress"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan ValidateAddressRes): + out, e = o.Res, o.e + } + return +} + +// VerifyMessage calls the method with the given parameters +func (a API) VerifyMessage(cmd *btcjson.VerifyMessageCmd) (e error) { + RPCHandlers["verifymessage"].Call <- API{a.Ch, cmd, nil} + return +} + +// VerifyMessageCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) VerifyMessageCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan VerifyMessageRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// VerifyMessageGetRes returns a pointer to the value in the Result field +func (a API) VerifyMessageGetRes() (out *bool, e error) { + out, _ = a.Result.(*bool) + e, _ = a.Result.(error) + return +} + +// VerifyMessageWait calls the method and blocks until it returns or 5 seconds passes +func (a API) VerifyMessageWait(cmd *btcjson.VerifyMessageCmd) (out *bool, e error) { + RPCHandlers["verifymessage"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan VerifyMessageRes): + out, e = o.Res, o.e + } + return +} + +// WalletIsLocked calls the method with the given parameters +func (a API) WalletIsLocked(cmd *None) (e error) { + RPCHandlers["walletislocked"].Call <- API{a.Ch, cmd, nil} + return +} + +// WalletIsLockedCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) WalletIsLockedCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan WalletIsLockedRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// WalletIsLockedGetRes returns a pointer to the value in the Result field +func (a API) WalletIsLockedGetRes() (out *bool, e error) { + out, _ = a.Result.(*bool) + e, _ = a.Result.(error) + return +} + +// WalletIsLockedWait calls the method and blocks until it returns or 5 seconds passes +func (a API) WalletIsLockedWait(cmd *None) (out *bool, e error) { + RPCHandlers["walletislocked"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan WalletIsLockedRes): + out, e = o.Res, o.e + } + return +} + +// WalletLock calls the method with the given parameters +func (a API) WalletLock(cmd *None) (e error) { + RPCHandlers["walletlock"].Call <- API{a.Ch, cmd, nil} + return +} + +// WalletLockCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) WalletLockCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan WalletLockRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// WalletLockGetRes returns a pointer to the value in the Result field +func (a API) WalletLockGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// WalletLockWait calls the method and blocks until it returns or 5 seconds passes +func (a API) WalletLockWait(cmd *None) (out *None, e error) { + RPCHandlers["walletlock"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan WalletLockRes): + out, e = o.Res, o.e + } + return +} + +// WalletPassphrase calls the method with the given parameters +func (a API) WalletPassphrase(cmd *btcjson.WalletPassphraseCmd) (e error) { + RPCHandlers["walletpassphrase"].Call <- API{a.Ch, cmd, nil} + return +} + +// WalletPassphraseCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) WalletPassphraseCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan WalletPassphraseRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// WalletPassphraseGetRes returns a pointer to the value in the Result field +func (a API) WalletPassphraseGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// WalletPassphraseWait calls the method and blocks until it returns or 5 seconds passes +func (a API) WalletPassphraseWait(cmd *btcjson.WalletPassphraseCmd) (out *None, e error) { + RPCHandlers["walletpassphrase"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan WalletPassphraseRes): + out, e = o.Res, o.e + } + return +} + +// WalletPassphraseChange calls the method with the given parameters +func (a API) WalletPassphraseChange(cmd *btcjson.WalletPassphraseChangeCmd) (e error) { + RPCHandlers["walletpassphrasechange"].Call <- API{a.Ch, cmd, nil} + return +} + +// WalletPassphraseChangeCheck checks if a new message arrived on the result channel and returns true if it does, as well as +// storing the value in the Result field +func (a API) WalletPassphraseChangeCheck() (isNew bool) { + select { + case o := <- a.Ch.(chan WalletPassphraseChangeRes): + if o.e != nil { + a.Result = o.e + } else { + a.Result = o.Res + } + isNew = true + default: + } + return +} + +// WalletPassphraseChangeGetRes returns a pointer to the value in the Result field +func (a API) WalletPassphraseChangeGetRes() (out *None, e error) { + out, _ = a.Result.(*None) + e, _ = a.Result.(error) + return +} + +// WalletPassphraseChangeWait calls the method and blocks until it returns or 5 seconds passes +func (a API) WalletPassphraseChangeWait(cmd *btcjson.WalletPassphraseChangeCmd) (out *None, e error) { + RPCHandlers["walletpassphrasechange"].Call <- API{a.Ch, cmd, nil} + select { + case <-time.After(time.Second*5): + break + case o := <- a.Ch.(chan WalletPassphraseChangeRes): + out, e = o.Res, o.e + } + return +} + + +// RunAPI starts up the api handler server that receives rpc.API messages and runs the handler and returns the result +// Note that the parameters are type asserted to prevent the consumer of the API from sending wrong message types not +// because it's necessary since they are interfaces end to end +func RunAPI(chainRPC *chainclient.RPCClient, wallet *Wallet, + quit qu.C) { + nrh := RPCHandlers + go func() { + D.Ln("starting up wallet cAPI") + var e error + var res interface{} + for { + select { + case msg := <-nrh["addmultisigaddress"].Call: + if res, e = nrh["addmultisigaddress"]. + Handler(msg.Params.(*btcjson.AddMultisigAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan AddMultiSigAddressRes) <- AddMultiSigAddressRes{&r, e} } + case msg := <-nrh["createmultisig"].Call: + if res, e = nrh["createmultisig"]. + Handler(msg.Params.(*btcjson.CreateMultisigCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.CreateMultiSigResult); ok { + msg.Ch.(chan CreateMultiSigRes) <- CreateMultiSigRes{&r, e} } + case msg := <-nrh["createnewaccount"].Call: + if res, e = nrh["createnewaccount"]. + Handler(msg.Params.(*btcjson.CreateNewAccountCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan CreateNewAccountRes) <- CreateNewAccountRes{&r, e} } + case msg := <-nrh["dropwallethistory"].Call: + if res, e = nrh["dropwallethistory"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan HandleDropWalletHistoryRes) <- HandleDropWalletHistoryRes{&r, e} } + case msg := <-nrh["dumpprivkey"].Call: + if res, e = nrh["dumpprivkey"]. + Handler(msg.Params.(*btcjson.DumpPrivKeyCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan DumpPrivKeyRes) <- DumpPrivKeyRes{&r, e} } + case msg := <-nrh["getaccount"].Call: + if res, e = nrh["getaccount"]. + Handler(msg.Params.(*btcjson.GetAccountCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan GetAccountRes) <- GetAccountRes{&r, e} } + case msg := <-nrh["getaccountaddress"].Call: + if res, e = nrh["getaccountaddress"]. + Handler(msg.Params.(*btcjson.GetAccountAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan GetAccountAddressRes) <- GetAccountAddressRes{&r, e} } + case msg := <-nrh["getaddressesbyaccount"].Call: + if res, e = nrh["getaddressesbyaccount"]. + Handler(msg.Params.(*btcjson.GetAddressesByAccountCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]string); ok { + msg.Ch.(chan GetAddressesByAccountRes) <- GetAddressesByAccountRes{&r, e} } + case msg := <-nrh["getbalance"].Call: + if res, e = nrh["getbalance"]. + Handler(msg.Params.(*btcjson.GetBalanceCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(float64); ok { + msg.Ch.(chan GetBalanceRes) <- GetBalanceRes{&r, e} } + case msg := <-nrh["getbestblock"].Call: + if res, e = nrh["getbestblock"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.GetBestBlockResult); ok { + msg.Ch.(chan GetBestBlockRes) <- GetBestBlockRes{&r, e} } + case msg := <-nrh["getbestblockhash"].Call: + if res, e = nrh["getbestblockhash"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan GetBestBlockHashRes) <- GetBestBlockHashRes{&r, e} } + case msg := <-nrh["getblockcount"].Call: + if res, e = nrh["getblockcount"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(int32); ok { + msg.Ch.(chan GetBlockCountRes) <- GetBlockCountRes{&r, e} } + case msg := <-nrh["getinfo"].Call: + if res, e = nrh["getinfo"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.InfoWalletResult); ok { + msg.Ch.(chan GetInfoRes) <- GetInfoRes{&r, e} } + case msg := <-nrh["getnewaddress"].Call: + if res, e = nrh["getnewaddress"]. + Handler(msg.Params.(*btcjson.GetNewAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan GetNewAddressRes) <- GetNewAddressRes{&r, e} } + case msg := <-nrh["getrawchangeaddress"].Call: + if res, e = nrh["getrawchangeaddress"]. + Handler(msg.Params.(*btcjson.GetRawChangeAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan GetRawChangeAddressRes) <- GetRawChangeAddressRes{&r, e} } + case msg := <-nrh["getreceivedbyaccount"].Call: + if res, e = nrh["getreceivedbyaccount"]. + Handler(msg.Params.(*btcjson.GetReceivedByAccountCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(float64); ok { + msg.Ch.(chan GetReceivedByAccountRes) <- GetReceivedByAccountRes{&r, e} } + case msg := <-nrh["getreceivedbyaddress"].Call: + if res, e = nrh["getreceivedbyaddress"]. + Handler(msg.Params.(*btcjson.GetReceivedByAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(float64); ok { + msg.Ch.(chan GetReceivedByAddressRes) <- GetReceivedByAddressRes{&r, e} } + case msg := <-nrh["gettransaction"].Call: + if res, e = nrh["gettransaction"]. + Handler(msg.Params.(*btcjson.GetTransactionCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.GetTransactionResult); ok { + msg.Ch.(chan GetTransactionRes) <- GetTransactionRes{&r, e} } + case msg := <-nrh["getunconfirmedbalance"].Call: + if res, e = nrh["getunconfirmedbalance"]. + Handler(msg.Params.(*btcjson.GetUnconfirmedBalanceCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(float64); ok { + msg.Ch.(chan GetUnconfirmedBalanceRes) <- GetUnconfirmedBalanceRes{&r, e} } + case msg := <-nrh["help"].Call: + if res, e = nrh["help"]. + Handler(msg.Params.(btcjson.HelpCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan HelpNoChainRPCRes) <- HelpNoChainRPCRes{&r, e} } + case msg := <-nrh["importprivkey"].Call: + if res, e = nrh["importprivkey"]. + Handler(msg.Params.(*btcjson.ImportPrivKeyCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan ImportPrivKeyRes) <- ImportPrivKeyRes{&r, e} } + case msg := <-nrh["keypoolrefill"].Call: + if res, e = nrh["keypoolrefill"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan KeypoolRefillRes) <- KeypoolRefillRes{&r, e} } + case msg := <-nrh["listaccounts"].Call: + if res, e = nrh["listaccounts"]. + Handler(msg.Params.(*btcjson.ListAccountsCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(map[string]float64); ok { + msg.Ch.(chan ListAccountsRes) <- ListAccountsRes{&r, e} } + case msg := <-nrh["listaddresstransactions"].Call: + if res, e = nrh["listaddresstransactions"]. + Handler(msg.Params.(*btcjson.ListAddressTransactionsCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]btcjson.ListTransactionsResult); ok { + msg.Ch.(chan ListAddressTransactionsRes) <- ListAddressTransactionsRes{&r, e} } + case msg := <-nrh["listalltransactions"].Call: + if res, e = nrh["listalltransactions"]. + Handler(msg.Params.(*btcjson.ListAllTransactionsCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]btcjson.ListTransactionsResult); ok { + msg.Ch.(chan ListAllTransactionsRes) <- ListAllTransactionsRes{&r, e} } + case msg := <-nrh["listlockunspent"].Call: + if res, e = nrh["listlockunspent"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]btcjson.TransactionInput); ok { + msg.Ch.(chan ListLockUnspentRes) <- ListLockUnspentRes{&r, e} } + case msg := <-nrh["listreceivedbyaccount"].Call: + if res, e = nrh["listreceivedbyaccount"]. + Handler(msg.Params.(*btcjson.ListReceivedByAccountCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]btcjson.ListReceivedByAccountResult); ok { + msg.Ch.(chan ListReceivedByAccountRes) <- ListReceivedByAccountRes{&r, e} } + case msg := <-nrh["listreceivedbyaddress"].Call: + if res, e = nrh["listreceivedbyaddress"]. + Handler(msg.Params.(*btcjson.ListReceivedByAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.ListReceivedByAddressResult); ok { + msg.Ch.(chan ListReceivedByAddressRes) <- ListReceivedByAddressRes{&r, e} } + case msg := <-nrh["listsinceblock"].Call: + if res, e = nrh["listsinceblock"]. + Handler(msg.Params.(btcjson.ListSinceBlockCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.ListSinceBlockResult); ok { + msg.Ch.(chan ListSinceBlockRes) <- ListSinceBlockRes{&r, e} } + case msg := <-nrh["listtransactions"].Call: + if res, e = nrh["listtransactions"]. + Handler(msg.Params.(*btcjson.ListTransactionsCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]btcjson.ListTransactionsResult); ok { + msg.Ch.(chan ListTransactionsRes) <- ListTransactionsRes{&r, e} } + case msg := <-nrh["listunspent"].Call: + if res, e = nrh["listunspent"]. + Handler(msg.Params.(*btcjson.ListUnspentCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.([]btcjson.ListUnspentResult); ok { + msg.Ch.(chan ListUnspentRes) <- ListUnspentRes{&r, e} } + case msg := <-nrh["renameaccount"].Call: + if res, e = nrh["renameaccount"]. + Handler(msg.Params.(*btcjson.RenameAccountCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan RenameAccountRes) <- RenameAccountRes{&r, e} } + case msg := <-nrh["sendfrom"].Call: + if res, e = nrh["sendfrom"]. + Handler(msg.Params.(btcjson.LockUnspentCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(bool); ok { + msg.Ch.(chan LockUnspentRes) <- LockUnspentRes{&r, e} } + case msg := <-nrh["sendmany"].Call: + if res, e = nrh["sendmany"]. + Handler(msg.Params.(*btcjson.SendManyCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan SendManyRes) <- SendManyRes{&r, e} } + case msg := <-nrh["sendtoaddress"].Call: + if res, e = nrh["sendtoaddress"]. + Handler(msg.Params.(*btcjson.SendToAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan SendToAddressRes) <- SendToAddressRes{&r, e} } + case msg := <-nrh["settxfee"].Call: + if res, e = nrh["settxfee"]. + Handler(msg.Params.(*btcjson.SetTxFeeCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(bool); ok { + msg.Ch.(chan SetTxFeeRes) <- SetTxFeeRes{&r, e} } + case msg := <-nrh["signmessage"].Call: + if res, e = nrh["signmessage"]. + Handler(msg.Params.(*btcjson.SignMessageCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(string); ok { + msg.Ch.(chan SignMessageRes) <- SignMessageRes{&r, e} } + case msg := <-nrh["signrawtransaction"].Call: + if res, e = nrh["signrawtransaction"]. + Handler(msg.Params.(btcjson.SignRawTransactionCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.SignRawTransactionResult); ok { + msg.Ch.(chan SignRawTransactionRes) <- SignRawTransactionRes{&r, e} } + case msg := <-nrh["validateaddress"].Call: + if res, e = nrh["validateaddress"]. + Handler(msg.Params.(*btcjson.ValidateAddressCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(btcjson.ValidateAddressWalletResult); ok { + msg.Ch.(chan ValidateAddressRes) <- ValidateAddressRes{&r, e} } + case msg := <-nrh["verifymessage"].Call: + if res, e = nrh["verifymessage"]. + Handler(msg.Params.(*btcjson.VerifyMessageCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(bool); ok { + msg.Ch.(chan VerifyMessageRes) <- VerifyMessageRes{&r, e} } + case msg := <-nrh["walletislocked"].Call: + if res, e = nrh["walletislocked"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(bool); ok { + msg.Ch.(chan WalletIsLockedRes) <- WalletIsLockedRes{&r, e} } + case msg := <-nrh["walletlock"].Call: + if res, e = nrh["walletlock"]. + Handler(msg.Params.(*None), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan WalletLockRes) <- WalletLockRes{&r, e} } + case msg := <-nrh["walletpassphrase"].Call: + if res, e = nrh["walletpassphrase"]. + Handler(msg.Params.(*btcjson.WalletPassphraseCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan WalletPassphraseRes) <- WalletPassphraseRes{&r, e} } + case msg := <-nrh["walletpassphrasechange"].Call: + if res, e = nrh["walletpassphrasechange"]. + Handler(msg.Params.(*btcjson.WalletPassphraseChangeCmd), wallet, + chainRPC); E.Chk(e) { + } + if r, ok := res.(None); ok { + msg.Ch.(chan WalletPassphraseChangeRes) <- WalletPassphraseChangeRes{&r, e} } + case <-quit.Wait(): + D.Ln("stopping wallet cAPI") + return + } + } + }() +} + +// RPC API functions to use with net/rpc + +func (c *CAPI) AddMultiSigAddress(req *btcjson.AddMultisigAddressCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["addmultisigaddress"].Result() + res.Params = req + nrh["addmultisigaddress"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) CreateMultiSig(req *btcjson.CreateMultisigCmd, resp btcjson.CreateMultiSigResult) (e error) { + nrh := RPCHandlers + res := nrh["createmultisig"].Result() + res.Params = req + nrh["createmultisig"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.CreateMultiSigResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) CreateNewAccount(req *btcjson.CreateNewAccountCmd, resp None) (e error) { + nrh := RPCHandlers + res := nrh["createnewaccount"].Result() + res.Params = req + nrh["createnewaccount"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) HandleDropWalletHistory(req *None, resp string) (e error) { + nrh := RPCHandlers + res := nrh["dropwallethistory"].Result() + res.Params = req + nrh["dropwallethistory"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) DumpPrivKey(req *btcjson.DumpPrivKeyCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["dumpprivkey"].Result() + res.Params = req + nrh["dumpprivkey"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetAccount(req *btcjson.GetAccountCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["getaccount"].Result() + res.Params = req + nrh["getaccount"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetAccountAddress(req *btcjson.GetAccountAddressCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["getaccountaddress"].Result() + res.Params = req + nrh["getaccountaddress"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetAddressesByAccount(req *btcjson.GetAddressesByAccountCmd, resp []string) (e error) { + nrh := RPCHandlers + res := nrh["getaddressesbyaccount"].Result() + res.Params = req + nrh["getaddressesbyaccount"].Call <- res + select { + case resp = <-res.Ch.(chan []string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetBalance(req *btcjson.GetBalanceCmd, resp float64) (e error) { + nrh := RPCHandlers + res := nrh["getbalance"].Result() + res.Params = req + nrh["getbalance"].Call <- res + select { + case resp = <-res.Ch.(chan float64): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetBestBlock(req *None, resp btcjson.GetBestBlockResult) (e error) { + nrh := RPCHandlers + res := nrh["getbestblock"].Result() + res.Params = req + nrh["getbestblock"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.GetBestBlockResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetBestBlockHash(req *None, resp string) (e error) { + nrh := RPCHandlers + res := nrh["getbestblockhash"].Result() + res.Params = req + nrh["getbestblockhash"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetBlockCount(req *None, resp int32) (e error) { + nrh := RPCHandlers + res := nrh["getblockcount"].Result() + res.Params = req + nrh["getblockcount"].Call <- res + select { + case resp = <-res.Ch.(chan int32): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetInfo(req *None, resp btcjson.InfoWalletResult) (e error) { + nrh := RPCHandlers + res := nrh["getinfo"].Result() + res.Params = req + nrh["getinfo"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.InfoWalletResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetNewAddress(req *btcjson.GetNewAddressCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["getnewaddress"].Result() + res.Params = req + nrh["getnewaddress"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetRawChangeAddress(req *btcjson.GetRawChangeAddressCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["getrawchangeaddress"].Result() + res.Params = req + nrh["getrawchangeaddress"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetReceivedByAccount(req *btcjson.GetReceivedByAccountCmd, resp float64) (e error) { + nrh := RPCHandlers + res := nrh["getreceivedbyaccount"].Result() + res.Params = req + nrh["getreceivedbyaccount"].Call <- res + select { + case resp = <-res.Ch.(chan float64): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetReceivedByAddress(req *btcjson.GetReceivedByAddressCmd, resp float64) (e error) { + nrh := RPCHandlers + res := nrh["getreceivedbyaddress"].Result() + res.Params = req + nrh["getreceivedbyaddress"].Call <- res + select { + case resp = <-res.Ch.(chan float64): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetTransaction(req *btcjson.GetTransactionCmd, resp btcjson.GetTransactionResult) (e error) { + nrh := RPCHandlers + res := nrh["gettransaction"].Result() + res.Params = req + nrh["gettransaction"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.GetTransactionResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) GetUnconfirmedBalance(req *btcjson.GetUnconfirmedBalanceCmd, resp float64) (e error) { + nrh := RPCHandlers + res := nrh["getunconfirmedbalance"].Result() + res.Params = req + nrh["getunconfirmedbalance"].Call <- res + select { + case resp = <-res.Ch.(chan float64): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) HelpNoChainRPC(req btcjson.HelpCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["help"].Result() + res.Params = req + nrh["help"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ImportPrivKey(req *btcjson.ImportPrivKeyCmd, resp None) (e error) { + nrh := RPCHandlers + res := nrh["importprivkey"].Result() + res.Params = req + nrh["importprivkey"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) KeypoolRefill(req *None, resp None) (e error) { + nrh := RPCHandlers + res := nrh["keypoolrefill"].Result() + res.Params = req + nrh["keypoolrefill"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListAccounts(req *btcjson.ListAccountsCmd, resp map[string]float64) (e error) { + nrh := RPCHandlers + res := nrh["listaccounts"].Result() + res.Params = req + nrh["listaccounts"].Call <- res + select { + case resp = <-res.Ch.(chan map[string]float64): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListAddressTransactions(req *btcjson.ListAddressTransactionsCmd, resp []btcjson.ListTransactionsResult) (e error) { + nrh := RPCHandlers + res := nrh["listaddresstransactions"].Result() + res.Params = req + nrh["listaddresstransactions"].Call <- res + select { + case resp = <-res.Ch.(chan []btcjson.ListTransactionsResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListAllTransactions(req *btcjson.ListAllTransactionsCmd, resp []btcjson.ListTransactionsResult) (e error) { + nrh := RPCHandlers + res := nrh["listalltransactions"].Result() + res.Params = req + nrh["listalltransactions"].Call <- res + select { + case resp = <-res.Ch.(chan []btcjson.ListTransactionsResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListLockUnspent(req *None, resp []btcjson.TransactionInput) (e error) { + nrh := RPCHandlers + res := nrh["listlockunspent"].Result() + res.Params = req + nrh["listlockunspent"].Call <- res + select { + case resp = <-res.Ch.(chan []btcjson.TransactionInput): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListReceivedByAccount(req *btcjson.ListReceivedByAccountCmd, resp []btcjson.ListReceivedByAccountResult) (e error) { + nrh := RPCHandlers + res := nrh["listreceivedbyaccount"].Result() + res.Params = req + nrh["listreceivedbyaccount"].Call <- res + select { + case resp = <-res.Ch.(chan []btcjson.ListReceivedByAccountResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListReceivedByAddress(req *btcjson.ListReceivedByAddressCmd, resp btcjson.ListReceivedByAddressResult) (e error) { + nrh := RPCHandlers + res := nrh["listreceivedbyaddress"].Result() + res.Params = req + nrh["listreceivedbyaddress"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.ListReceivedByAddressResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListSinceBlock(req btcjson.ListSinceBlockCmd, resp btcjson.ListSinceBlockResult) (e error) { + nrh := RPCHandlers + res := nrh["listsinceblock"].Result() + res.Params = req + nrh["listsinceblock"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.ListSinceBlockResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListTransactions(req *btcjson.ListTransactionsCmd, resp []btcjson.ListTransactionsResult) (e error) { + nrh := RPCHandlers + res := nrh["listtransactions"].Result() + res.Params = req + nrh["listtransactions"].Call <- res + select { + case resp = <-res.Ch.(chan []btcjson.ListTransactionsResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ListUnspent(req *btcjson.ListUnspentCmd, resp []btcjson.ListUnspentResult) (e error) { + nrh := RPCHandlers + res := nrh["listunspent"].Result() + res.Params = req + nrh["listunspent"].Call <- res + select { + case resp = <-res.Ch.(chan []btcjson.ListUnspentResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) RenameAccount(req *btcjson.RenameAccountCmd, resp None) (e error) { + nrh := RPCHandlers + res := nrh["renameaccount"].Result() + res.Params = req + nrh["renameaccount"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) LockUnspent(req btcjson.LockUnspentCmd, resp bool) (e error) { + nrh := RPCHandlers + res := nrh["sendfrom"].Result() + res.Params = req + nrh["sendfrom"].Call <- res + select { + case resp = <-res.Ch.(chan bool): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) SendMany(req *btcjson.SendManyCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["sendmany"].Result() + res.Params = req + nrh["sendmany"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) SendToAddress(req *btcjson.SendToAddressCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["sendtoaddress"].Result() + res.Params = req + nrh["sendtoaddress"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) SetTxFee(req *btcjson.SetTxFeeCmd, resp bool) (e error) { + nrh := RPCHandlers + res := nrh["settxfee"].Result() + res.Params = req + nrh["settxfee"].Call <- res + select { + case resp = <-res.Ch.(chan bool): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) SignMessage(req *btcjson.SignMessageCmd, resp string) (e error) { + nrh := RPCHandlers + res := nrh["signmessage"].Result() + res.Params = req + nrh["signmessage"].Call <- res + select { + case resp = <-res.Ch.(chan string): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) SignRawTransaction(req btcjson.SignRawTransactionCmd, resp btcjson.SignRawTransactionResult) (e error) { + nrh := RPCHandlers + res := nrh["signrawtransaction"].Result() + res.Params = req + nrh["signrawtransaction"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.SignRawTransactionResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) ValidateAddress(req *btcjson.ValidateAddressCmd, resp btcjson.ValidateAddressWalletResult) (e error) { + nrh := RPCHandlers + res := nrh["validateaddress"].Result() + res.Params = req + nrh["validateaddress"].Call <- res + select { + case resp = <-res.Ch.(chan btcjson.ValidateAddressWalletResult): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) VerifyMessage(req *btcjson.VerifyMessageCmd, resp bool) (e error) { + nrh := RPCHandlers + res := nrh["verifymessage"].Result() + res.Params = req + nrh["verifymessage"].Call <- res + select { + case resp = <-res.Ch.(chan bool): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) WalletIsLocked(req *None, resp bool) (e error) { + nrh := RPCHandlers + res := nrh["walletislocked"].Result() + res.Params = req + nrh["walletislocked"].Call <- res + select { + case resp = <-res.Ch.(chan bool): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) WalletLock(req *None, resp None) (e error) { + nrh := RPCHandlers + res := nrh["walletlock"].Result() + res.Params = req + nrh["walletlock"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) WalletPassphrase(req *btcjson.WalletPassphraseCmd, resp None) (e error) { + nrh := RPCHandlers + res := nrh["walletpassphrase"].Result() + res.Params = req + nrh["walletpassphrase"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +func (c *CAPI) WalletPassphraseChange(req *btcjson.WalletPassphraseChangeCmd, resp None) (e error) { + nrh := RPCHandlers + res := nrh["walletpassphrasechange"].Result() + res.Params = req + nrh["walletpassphrasechange"].Call <- res + select { + case resp = <-res.Ch.(chan None): + case <-time.After(c.Timeout): + case <-c.quit.Wait(): + } + return +} + +// Client call wrappers for a CAPI client with a given Conn + +func (r *CAPIClient) AddMultiSigAddress(cmd ...*btcjson.AddMultisigAddressCmd) (res string, e error) { + var c *btcjson.AddMultisigAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.AddMultiSigAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) CreateMultiSig(cmd ...*btcjson.CreateMultisigCmd) (res btcjson.CreateMultiSigResult, e error) { + var c *btcjson.CreateMultisigCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.CreateMultiSig", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) CreateNewAccount(cmd ...*btcjson.CreateNewAccountCmd) (res None, e error) { + var c *btcjson.CreateNewAccountCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.CreateNewAccount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) HandleDropWalletHistory(cmd ...*None) (res string, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.HandleDropWalletHistory", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) DumpPrivKey(cmd ...*btcjson.DumpPrivKeyCmd) (res string, e error) { + var c *btcjson.DumpPrivKeyCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.DumpPrivKey", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetAccount(cmd ...*btcjson.GetAccountCmd) (res string, e error) { + var c *btcjson.GetAccountCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetAccount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetAccountAddress(cmd ...*btcjson.GetAccountAddressCmd) (res string, e error) { + var c *btcjson.GetAccountAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetAccountAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetAddressesByAccount(cmd ...*btcjson.GetAddressesByAccountCmd) (res []string, e error) { + var c *btcjson.GetAddressesByAccountCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetAddressesByAccount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetBalance(cmd ...*btcjson.GetBalanceCmd) (res float64, e error) { + var c *btcjson.GetBalanceCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetBalance", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetBestBlock(cmd ...*None) (res btcjson.GetBestBlockResult, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetBestBlock", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetBestBlockHash(cmd ...*None) (res string, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetBestBlockHash", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetBlockCount(cmd ...*None) (res int32, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetBlockCount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetInfo(cmd ...*None) (res btcjson.InfoWalletResult, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetInfo", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetNewAddress(cmd ...*btcjson.GetNewAddressCmd) (res string, e error) { + var c *btcjson.GetNewAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetNewAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetRawChangeAddress(cmd ...*btcjson.GetRawChangeAddressCmd) (res string, e error) { + var c *btcjson.GetRawChangeAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetRawChangeAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetReceivedByAccount(cmd ...*btcjson.GetReceivedByAccountCmd) (res float64, e error) { + var c *btcjson.GetReceivedByAccountCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetReceivedByAccount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetReceivedByAddress(cmd ...*btcjson.GetReceivedByAddressCmd) (res float64, e error) { + var c *btcjson.GetReceivedByAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetReceivedByAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetTransaction(cmd ...*btcjson.GetTransactionCmd) (res btcjson.GetTransactionResult, e error) { + var c *btcjson.GetTransactionCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetTransaction", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) GetUnconfirmedBalance(cmd ...*btcjson.GetUnconfirmedBalanceCmd) (res float64, e error) { + var c *btcjson.GetUnconfirmedBalanceCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.GetUnconfirmedBalance", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) HelpNoChainRPC(cmd ...btcjson.HelpCmd) (res string, e error) { + var c btcjson.HelpCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.HelpNoChainRPC", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ImportPrivKey(cmd ...*btcjson.ImportPrivKeyCmd) (res None, e error) { + var c *btcjson.ImportPrivKeyCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ImportPrivKey", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) KeypoolRefill(cmd ...*None) (res None, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.KeypoolRefill", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListAccounts(cmd ...*btcjson.ListAccountsCmd) (res map[string]float64, e error) { + var c *btcjson.ListAccountsCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListAccounts", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListAddressTransactions(cmd ...*btcjson.ListAddressTransactionsCmd) (res []btcjson.ListTransactionsResult, e error) { + var c *btcjson.ListAddressTransactionsCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListAddressTransactions", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListAllTransactions(cmd ...*btcjson.ListAllTransactionsCmd) (res []btcjson.ListTransactionsResult, e error) { + var c *btcjson.ListAllTransactionsCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListAllTransactions", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListLockUnspent(cmd ...*None) (res []btcjson.TransactionInput, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListLockUnspent", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListReceivedByAccount(cmd ...*btcjson.ListReceivedByAccountCmd) (res []btcjson.ListReceivedByAccountResult, e error) { + var c *btcjson.ListReceivedByAccountCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListReceivedByAccount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListReceivedByAddress(cmd ...*btcjson.ListReceivedByAddressCmd) (res btcjson.ListReceivedByAddressResult, e error) { + var c *btcjson.ListReceivedByAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListReceivedByAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListSinceBlock(cmd ...btcjson.ListSinceBlockCmd) (res btcjson.ListSinceBlockResult, e error) { + var c btcjson.ListSinceBlockCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListSinceBlock", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListTransactions(cmd ...*btcjson.ListTransactionsCmd) (res []btcjson.ListTransactionsResult, e error) { + var c *btcjson.ListTransactionsCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListTransactions", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ListUnspent(cmd ...*btcjson.ListUnspentCmd) (res []btcjson.ListUnspentResult, e error) { + var c *btcjson.ListUnspentCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ListUnspent", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) RenameAccount(cmd ...*btcjson.RenameAccountCmd) (res None, e error) { + var c *btcjson.RenameAccountCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.RenameAccount", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) LockUnspent(cmd ...btcjson.LockUnspentCmd) (res bool, e error) { + var c btcjson.LockUnspentCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.LockUnspent", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) SendMany(cmd ...*btcjson.SendManyCmd) (res string, e error) { + var c *btcjson.SendManyCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.SendMany", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) SendToAddress(cmd ...*btcjson.SendToAddressCmd) (res string, e error) { + var c *btcjson.SendToAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.SendToAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) SetTxFee(cmd ...*btcjson.SetTxFeeCmd) (res bool, e error) { + var c *btcjson.SetTxFeeCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.SetTxFee", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) SignMessage(cmd ...*btcjson.SignMessageCmd) (res string, e error) { + var c *btcjson.SignMessageCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.SignMessage", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) SignRawTransaction(cmd ...btcjson.SignRawTransactionCmd) (res btcjson.SignRawTransactionResult, e error) { + var c btcjson.SignRawTransactionCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.SignRawTransaction", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) ValidateAddress(cmd ...*btcjson.ValidateAddressCmd) (res btcjson.ValidateAddressWalletResult, e error) { + var c *btcjson.ValidateAddressCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.ValidateAddress", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) VerifyMessage(cmd ...*btcjson.VerifyMessageCmd) (res bool, e error) { + var c *btcjson.VerifyMessageCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.VerifyMessage", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) WalletIsLocked(cmd ...*None) (res bool, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.WalletIsLocked", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) WalletLock(cmd ...*None) (res None, e error) { + var c *None + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.WalletLock", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) WalletPassphrase(cmd ...*btcjson.WalletPassphraseCmd) (res None, e error) { + var c *btcjson.WalletPassphraseCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.WalletPassphrase", c, &res); E.Chk(e) { + } + return +} + +func (r *CAPIClient) WalletPassphraseChange(cmd ...*btcjson.WalletPassphraseChangeCmd) (res None, e error) { + var c *btcjson.WalletPassphraseChangeCmd + if len(cmd) > 0 { + c = cmd[0] + } + if e = r.Call("CAPI.WalletPassphraseChange", c, &res); E.Chk(e) { + } + return +} + diff --git a/cmd/wallet/rpchelp_test.go b/cmd/wallet/rpchelp_test.go new file mode 100644 index 0000000..fe0986f --- /dev/null +++ b/cmd/wallet/rpchelp_test.go @@ -0,0 +1,88 @@ +package wallet + +import ( + "strings" + "testing" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/rpchelp" +) + +func serverMethods() map[string]struct{} { + m := make(map[string]struct{}) + for method, handlerData := range RPCHandlers { + if !handlerData.NoHelp { + m[method] = struct{}{} + } + } + return m +} + +// TestRPCMethodHelpGeneration ensures that help text can be generated for every method of the RPC server for every +// supported locale. +func TestRPCMethodHelpGeneration(t *testing.T) { + needsGenerate := false + defer func() { + if needsGenerate && !t.Failed() { + t.Error("Generated help texts are out of date: run 'go generate'") + return + } + if t.Failed() { + t.Log("Regenerate help texts with 'go generate' after fixing") + } + }() + for i := range rpchelp.HelpDescs { + svrMethods := serverMethods() + locale := rpchelp.HelpDescs[i].Locale + generatedDescs := LocaleHelpDescs[locale]() + for _, m := range rpchelp.Methods { + delete(svrMethods, m.Method) + helpText, e := btcjson.GenerateHelp(m.Method, rpchelp.HelpDescs[i].Descs, m.ResultTypes...) + if e != nil { + t.Errorf("Cannot generate '%s' help for method '%s': missing description for '%s'", + locale, m.Method, e, + ) + continue + } + if !needsGenerate && helpText != generatedDescs[m.Method] { + needsGenerate = true + } + } + for m := range svrMethods { + t.Errorf("Missing '%s' help for method '%s'", locale, m) + } + } +} + +// TestRPCMethodUsageGeneration ensures that single line usage text can be generated for every supported request of the +// RPC server. +func TestRPCMethodUsageGeneration(t *testing.T) { + needsGenerate := false + defer func() { + if needsGenerate && !t.Failed() { + t.Error("Generated help usages are out of date: run 'go generate'") + return + } + if t.Failed() { + t.Log("Regenerate help usage with 'go generate' after fixing") + } + }() + svrMethods := serverMethods() + usageStrs := make([]string, 0, len(rpchelp.Methods)) + for _, m := range rpchelp.Methods { + delete(svrMethods, m.Method) + usage, e := btcjson.MethodUsageText(m.Method) + if e != nil { + t.Errorf("Cannot generate single line usage for method '%s': %v", + m.Method, e, + ) + } + if !t.Failed() { + usageStrs = append(usageStrs, usage) + } + } + if !t.Failed() { + usages := strings.Join(usageStrs, "\n") + needsGenerate = usages != RequestUsages + } +} diff --git a/cmd/wallet/rpcserver.go b/cmd/wallet/rpcserver.go new file mode 100644 index 0000000..c226509 --- /dev/null +++ b/cmd/wallet/rpcserver.go @@ -0,0 +1,235 @@ +package wallet + +import ( + "crypto/tls" + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pod/config" + "github.com/p9c/p9/pod/state" +) + +type listenFunc func(net string, laddr string) (net.Listener, error) + +// GenerateRPCKeyPair generates a new RPC TLS keypair and writes the cert and possibly also the key in PEM format to the +// paths specified by the config. If successful, the new keypair is returned. +func GenerateRPCKeyPair(config *config.Config, writeKey bool) (tls.Certificate, error) { + D.Ln("generating TLS certificates") + // Create directories for cert and key files if they do not yet exist. + D.Ln("rpc tls ", *config.RPCCert, " ", *config.RPCKey) + certDir, _ := filepath.Split(config.RPCCert.V()) + keyDir, _ := filepath.Split(config.RPCKey.V()) + e := os.MkdirAll(certDir, 0700) + if e != nil { + return tls.Certificate{}, e + } + e = os.MkdirAll(keyDir, 0700) + if e != nil { + return tls.Certificate{}, e + } + // Generate cert pair. + org := "pod/wallet autogenerated cert" + validUntil := time.Now().Add(time.Hour * 24 * 365 * 10) + cert, key, e := util.NewTLSCertPair(org, validUntil, nil) + if e != nil { + return tls.Certificate{}, e + } + keyPair, e := tls.X509KeyPair(cert, key) + if e != nil { + return tls.Certificate{}, e + } + // Write cert and (potentially) the key files. + e = ioutil.WriteFile(config.RPCCert.V(), cert, 0600) + if e != nil { + rmErr := os.Remove(config.RPCCert.V()) + if rmErr != nil { + E.Ln("cannot remove written certificates:", rmErr) + } + return tls.Certificate{}, e + } + e = ioutil.WriteFile(config.CAFile.V(), cert, 0600) + if e != nil { + rmErr := os.Remove(config.RPCCert.V()) + if rmErr != nil { + E.Ln("cannot remove written certificates:", rmErr) + } + return tls.Certificate{}, e + } + if writeKey { + e = ioutil.WriteFile(config.RPCKey.V(), key, 0600) + if e != nil { + rmErr := os.Remove(config.RPCCert.V()) + if rmErr != nil { + E.Ln("cannot remove written certificates:", rmErr) + } + rmErr = os.Remove(config.CAFile.V()) + if rmErr != nil { + E.Ln("cannot remove written certificates:", rmErr) + } + return tls.Certificate{}, e + } + } + I.Ln("done generating TLS certificates") + return keyPair, nil +} + +// makeListeners splits the normalized listen addresses into IPv4 and IPv6 addresses and creates new net.Listeners for +// each with the passed listen func. Invalid addresses are logged and skipped. +func makeListeners(normalizedListenAddrs []string, listen listenFunc) []net.Listener { + ipv4Addrs := make([]string, 0, len(normalizedListenAddrs)*2) + // ipv6Addrs := make([]string, 0, len(normalizedListenAddrs)*2) + for _, addr := range normalizedListenAddrs { + var host string + var e error + host, _, e = net.SplitHostPort(addr) + if e != nil { + // Shouldn't happen due to already being normalized. + E.F( + "`%s` is not a normalized listener address", addr, + ) + continue + } + // Empty host or host of * on plan9 is both IPv4 and IPv6. + if host == "" || (host == "*" && runtime.GOOS == "plan9") { + ipv4Addrs = append(ipv4Addrs, addr) + // ipv6Addrs = append(ipv6Addrs, addr) + continue + } + // Remove the IPv6 zone from the host, if present. The zone prevents ParseIP from correctly parsing the IP + // address. ResolveIPAddr is intentionally not used here due to the possibility of leaking a DNS query over Tor + // if the host is a hostname and not an IP address. + zoneIndex := strings.Index(host, "%") + if zoneIndex != -1 { + host = host[:zoneIndex] + } + ip := net.ParseIP(host) + switch { + case ip == nil: + W.F("`%s` is not a valid IP address", host) + case ip.To4() == nil: + // ipv6Addrs = append(ipv6Addrs, addr) + default: + ipv4Addrs = append(ipv4Addrs, addr) + } + } + listeners := make( + []net.Listener, 0, + // len(ipv6Addrs)+ + len(ipv4Addrs), + ) + for _, addr := range ipv4Addrs { + listener, e := listen("tcp4", addr) + if e != nil { + W.F( + "Can't listen on %s: %v", addr, e, + ) + continue + } + listeners = append(listeners, listener) + } + // for _, addr := range ipv6Addrs { + // listener, e := listen("tcp6", addr) + // if e != nil { + // Warnf( + // "Can't listen on %s: %v", addr, e, + // ) + // continue + // } + // listeners = append(listeners, listener) + // } + return listeners +} + +// OpenRPCKeyPair creates or loads the RPC TLS keypair specified by the +// application config. This function respects the pod.Config.OneTimeTLSKey +// setting. +func OpenRPCKeyPair(config *config.Config) (tls.Certificate, error) { + // Chk for existence of the TLS key file. If one time TLS keys are enabled but a + // key already exists, this function should error since it's possible that a + // persistent certificate was copied to a remote machine. Otherwise, generate a + // new keypair when the key is missing. When generating new persistent keys, + // overwriting an existing cert is acceptable if the previous execution used a + // one time TLS key. Otherwise, both the cert and key should be read from disk. + // If the cert is missing, the read error will occur in LoadX509KeyPair. + _, e := os.Stat(config.RPCKey.V()) + keyExists := !os.IsNotExist(e) + switch { + case config.OneTimeTLSKey.True() && keyExists: + if e = fmt.Errorf( + "one time TLS keys are enabled, but TLS key `%s` already exists", config.RPCKey.V(), + ); E.Chk(e) { + } + return tls.Certificate{}, e + case config.OneTimeTLSKey.True(): + return GenerateRPCKeyPair(config, false) + case !keyExists: + return GenerateRPCKeyPair(config, true) + default: + return tls.LoadX509KeyPair(config.RPCCert.V(), config.RPCKey.V()) + } +} +func startRPCServers(cx *state.State, walletLoader *Loader) (*Server, error) { + T.Ln("startRPCServers") + var ( + legacyServer *Server + walletListen = net.Listen + keyPair tls.Certificate + e error + ) + if cx.Config.ClientTLS.False() { + I.Ln("server TLS is disabled - only legacy RPC may be used") + } else { + keyPair, e = OpenRPCKeyPair(cx.Config) + if e != nil { + return nil, e + } + // Change the standard net.Listen function to the tls one. + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{keyPair}, + MinVersion: tls.VersionTLS12, + NextProtos: []string{"h2"}, // HTTP/2 over TLS + InsecureSkipVerify: cx.Config.TLSSkipVerify.True(), + } + walletListen = func(net string, laddr string) (net.Listener, error) { + return tls.Listen(net, laddr, tlsConfig) + } + } + if cx.Config.Username.V() == "" || cx.Config.Password.V() == "" { + I.Ln("legacy RPC server disabled (requires username and password)") + } else if len(cx.Config.WalletRPCListeners.S()) != 0 { + listeners := makeListeners(cx.Config.WalletRPCListeners.S(), walletListen) + if len(listeners) == 0 { + e := errors.New("failed to create listeners for legacy RPC server") + return nil, e + } + opts := Options{ + Username: cx.Config.Username.V(), + Password: cx.Config.Password.V(), + MaxPOSTClients: int64(cx.Config.WalletRPCMaxClients.V()), + MaxWebsocketClients: int64(cx.Config.WalletRPCMaxWebsockets.V()), + } + legacyServer = NewServer(&opts, walletLoader, listeners, nil) + } + // Error when no legacy RPC servers can be started. + if legacyServer == nil { + return nil, errors.New("no suitable RPC services can be started") + } + return legacyServer, nil +} + +// startWalletRPCServices associates each of the (optionally-nil) RPC servers with a wallet to enable remote wallet +// access. For the legacy JSON-RPC server it enables methods that require a loaded wallet. +func startWalletRPCServices(wallet *Wallet, legacyServer *Server) { + if legacyServer != nil { + D.Ln("starting legacy wallet rpc server") + legacyServer.RegisterWallet(wallet) + } +} diff --git a/cmd/wallet/rpcserver_test.go b/cmd/wallet/rpcserver_test.go new file mode 100644 index 0000000..0289c0a --- /dev/null +++ b/cmd/wallet/rpcserver_test.go @@ -0,0 +1,43 @@ +package wallet + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/p9c/p9/pkg/qu" +) + +func TestThrottle(t *testing.T) { + const threshold = 1 + busy := qu.T() + srv := httptest.NewServer( + ThrottledFn(threshold, + func(w http.ResponseWriter, r *http.Request) { + <-busy + }, + ), + ) + codes := make(chan int, 2) + for i := 0; i < cap(codes); i++ { + go func() { + res, e := http.Get(srv.URL) + if e != nil { + t.Fatal(e) + } + codes <- res.StatusCode + }() + } + got := make(map[int]int, cap(codes)) + for i := 0; i < cap(codes); i++ { + got[<-codes]++ + if i == 0 { + busy.Q() + } + } + want := map[int]int{200: 1, 429: 1} + if !reflect.DeepEqual(want, got) { + t.Fatalf("status codes: want: %v, got: %v", want, got) + } +} diff --git a/cmd/wallet/rpcserverhelp.go b/cmd/wallet/rpcserverhelp.go new file mode 100644 index 0000000..5ab383e --- /dev/null +++ b/cmd/wallet/rpcserverhelp.go @@ -0,0 +1,58 @@ +package wallet + +// AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit. + +func HelpDescsEnUS() map[string]string { + return map[string]string{ + "addmultisigaddress": "addmultisigaddress nrequired [\"key\",...] (\"account\")\n\nGenerates and imports a multisig address and redeeming script to the 'imported' account.\n\nArguments:\n1. nrequired (numeric, required) The number of signatures required to redeem outputs paid to this address\n2. keys (array of string, required) Pubkeys and/or pay-to-pubkey-hash addresses to partially control the multisig address\n3. account (string, optional) DEPRECATED -- Unused (all imported addresses belong to the imported account)\n\nResult:\n\"value\" (string) The imported pay-to-script-hash address\n", + "createmultisig": "createmultisig nrequired [\"key\",...]\n\nGenerate a multisig address and redeem script.\n\nArguments:\n1. nrequired (numeric, required) The number of signatures required to redeem outputs paid to this address\n2. keys (array of string, required) Pubkeys and/or pay-to-pubkey-hash addresses to partially control the multisig address\n\nResult:\n{\n \"address\": \"value\", (string) The generated pay-to-script-hash address\n \"redeemScript\": \"value\", (string) The script required to redeem outputs paid to the multisig address\n} \n", + "dumpprivkey": "dumpprivkey \"address\"\n\nReturns the private key in WIF encoding that controls some wallet address.\n\nArguments:\n1. address (string, required) The address to return a private key for\n\nResult:\n\"value\" (string) The WIF-encoded private key\n", + "getaccount": "getaccount \"address\"\n\nDEPRECATED -- Lookup the account name that some wallet address belongs to.\n\nArguments:\n1. address (string, required) The address to query the account for\n\nResult:\n\"value\" (string) The name of the account that 'address' belongs to\n", + "getaccountaddress": "getaccountaddress \"account\"\n\nDEPRECATED -- Returns the most recent external payment address for an account that has not been seen publicly.\nA new address is generated for the account if the most recently generated address has been seen on the blockchain or in mempool.\n\nArguments:\n1. account (string, required) The account of the returned address\n\nResult:\n\"value\" (string) The unused address for 'account'\n", + "getaddressesbyaccount": "getaddressesbyaccount \"account\"\n\nDEPRECATED -- Returns all addresses strings controlled by a single account.\n\nArguments:\n1. account (string, required) Account name to fetch addresses for\n\nResult:\n[\"value\",...] (array of string) All addresses controlled by 'account'\n", + "getbalance": "getbalance (\"account\" minconf=1)\n\nCalculates and returns the balance of one or all accounts.\n\nArguments:\n1. account (string, optional) DEPRECATED -- The account name to query the balance for, or \"*\" to consider all accounts (default=\"*\")\n2. minconf (numeric, optional, default=1) Minimum number of block confirmations required before an unspent output's value is included in the balance\n\nResult (account != \"*\"):\nn.nnn (numeric) The balance of 'account' valued in bitcoin\n\nResult (account = \"*\"):\nn.nnn (numeric) The balance of all accounts valued in bitcoin\n", + "getbestblockhash": "getbestblockhash\n\nReturns the hash of the newest block in the best chain that wallet has finished syncing with.\n\nArguments:\nNone\n\nResult:\n\"value\" (string) The hash of the most recent synced-to block\n", + "getblockcount": "getblockcount\n\nReturns the blockchain height of the newest block in the best chain that wallet has finished syncing with.\n\nArguments:\nNone\n\nResult:\nn.nnn (numeric) The blockchain height of the most recent synced-to block\n", + "getinfo": "getinfo\n\nReturns a JSON object containing various state info.\n\nArguments:\nNone\n\nResult:\n{\n \"version\": n, (numeric) The version of the server\n \"protocolversion\": n, (numeric) The latest supported protocol version\n \"walletversion\": n, (numeric) The version of the address manager database\n \"balance\": n.nnn, (numeric) The balance of all accounts calculated with one block confirmation\n \"blocks\": n, (numeric) The number of blocks processed\n \"timeoffset\": n, (numeric) The time offset\n \"connections\": n, (numeric) The number of connected peers\n \"proxy\": \"value\", (string) The proxy used by the server\n \"difficulty\": n.nnn, (numeric) The current target difficulty\n \"testnet\": true|false, (boolean) Whether or not server is using testnet\n \"keypoololdest\": n, (numeric) Unset\n \"keypoolsize\": n, (numeric) Unset\n \"unlocked_until\": n, (numeric) Unset\n \"paytxfee\": n.nnn, (numeric) The increment used each time more fee is required for an authored transaction\n \"relayfee\": n.nnn, (numeric) The minimum relay fee for non-free transactions in DUO/KB\n \"errors\": \"value\", (string) Any current errors\n} \n", + "getnewaddress": "getnewaddress (\"account\")\n\nGenerates and returns a new payment address.\n\nArguments:\n1. account (string, optional) DEPRECATED -- Account name the new address will belong to (default=\"default\")\n\nResult:\n\"value\" (string) The payment address\n", + "getrawchangeaddress": "getrawchangeaddress (\"account\")\n\nGenerates and returns a new internal payment address for use as a change address in raw transactions.\n\nArguments:\n1. account (string, optional) Account name the new internal address will belong to (default=\"default\")\n\nResult:\n\"value\" (string) The internal payment address\n", + "getreceivedbyaccount": "getreceivedbyaccount \"account\" (minconf=1)\n\nDEPRECATED -- Returns the total amount received by addresses of some account, including spent outputs.\n\nArguments:\n1. account (string, required) Account name to query total received amount for\n2. minconf (numeric, optional, default=1) Minimum number of block confirmations required before an output's value is included in the total\n\nResult:\nn.nnn (numeric) The total received amount valued in bitcoin\n", + "getreceivedbyaddress": "getreceivedbyaddress \"address\" (minconf=1)\n\nReturns the total amount received by a single address, including spent outputs.\n\nArguments:\n1. address (string, required) Payment address which received outputs to include in total\n2. minconf (numeric, optional, default=1) Minimum number of block confirmations required before an output's value is included in the total\n\nResult:\nn.nnn (numeric) The total received amount valued in bitcoin\n", + "gettransaction": "gettransaction \"txid\" (includewatchonly=false)\n\nReturns a JSON object with details regarding a transaction relevant to this wallet.\n\nArguments:\n1. txid (string, required) Hash of the transaction to query\n2. includewatchonly (boolean, optional, default=false) Also consider transactions involving watched addresses\n\nResult:\n{\n \"amount\": n.nnn, (numeric) The total amount this transaction credits to the wallet, valued in bitcoin\n \"fee\": n.nnn, (numeric) The total input value minus the total output value, or 0 if 'txid' is not a sent transaction\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"txid\": \"value\", (string) The transaction hash\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"details\": [{ (array of object) Additional details for each recorded wallet credit and debit\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) The address an output was paid to, or the empty string if the output is nonstandard or this detail is regarding a transaction input\n \"amount\": n.nnn, (numeric) The amount of a received output\n \"category\": \"value\", (string) The kind of detail: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs\n \"involveswatchonly\": true|false, (boolean) Unset\n \"fee\": n.nnn, (numeric) The included fee for a sent transaction\n \"vout\": n, (numeric) The transaction output index\n },...], \n \"hex\": \"value\", (string) The transaction encoded as a hexadecimal string\n} \n", + "help": "help (\"command\")\n\nReturns a list of all commands or help for a specified command.\n\nArguments:\n1. command (string, optional) The command to retrieve help for\n\nResult (no command provided):\n\"value\" (string) List of commands\n\nResult (command specified):\n\"value\" (string) Help for specified command\n", + "importprivkey": "importprivkey \"privkey\" (\"label\" rescan=true)\n\nImports a WIF-encoded private key to the 'imported' account.\n\nArguments:\n1. privkey (string, required) The WIF-encoded private key\n2. label (string, optional) Unused (must be unset or 'imported')\n3. rescan (boolean, optional, default=true) Rescan the blockchain (since the genesis block) for outputs controlled by the imported key\n\nResult:\nNothing\n", + "keypoolrefill": "keypoolrefill (newsize=100)\n\nDEPRECATED -- This request does nothing since no keypool is maintained.\n\nArguments:\n1. newsize (numeric, optional, default=100) Unused\n\nResult:\nNothing\n", + "listaccounts": "listaccounts (minconf=1)\n\nDEPRECATED -- Returns a JSON object of all accounts and their balances.\n\nArguments:\n1. minconf (numeric, optional, default=1) Minimum number of block confirmations required before an unspent output's value is included in the balance\n\nResult:\n{\n \"The account name\": The account balance valued in bitcoin, (object) JSON object with account names as keys and bitcoin amounts as values\n ...\n}\n", + "listlockunspent": "listlockunspent\n\nReturns a JSON array of outpoints marked as locked (with lockunspent) for this wallet session.\n\nArguments:\nNone\n\nResult:\n[{\n \"txid\": \"value\", (string) The transaction hash of the referenced output\n \"vout\": n, (numeric) The output index of the referenced output\n},...]\n", + "listreceivedbyaccount": "listreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\n\nDEPRECATED -- Returns a JSON array of objects listing all accounts and the total amount received by each account.\n\nArguments:\n1. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction is considered\n2. includeempty (boolean, optional, default=false) Unused\n3. includewatchonly (boolean, optional, default=false) Unused\n\nResult:\n[{\n \"account\": \"value\", (string) The name of the account\n \"amount\": n.nnn, (numeric) Total amount received by payment addresses of the account valued in bitcoin\n \"confirmations\": n, (numeric) Number of block confirmations of the most recent transaction relevant to the account\n},...]\n", + "listreceivedbyaddress": "listreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\n\nReturns a JSON array of objects listing wallet payment addresses and their total received amounts.\n\nArguments:\n1. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction is considered\n2. includeempty (boolean, optional, default=false) Unused\n3. includewatchonly (boolean, optional, default=false) Unused\n\nResult:\n[{\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) The payment address\n \"amount\": n.nnn, (numeric) Total amount received by the payment address valued in bitcoin\n \"confirmations\": n, (numeric) Number of block confirmations of the most recent transaction relevant to the address\n \"txids\": [\"value\",...], (array of string) Transaction hashes of all transactions involving this address\n \"involvesWatchonly\": true|false, (boolean) Unset\n},...]\n", + "listsinceblock": "listsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\n\nReturns a JSON array of objects listing details of all wallet transactions after some block.\n\nArguments:\n1. blockhash (string, optional) Hash of the parent block of the first block to consider transactions from, or unset to list all transactions\n2. targetconfirmations (numeric, optional, default=1) Minimum number of block confirmations of the last block in the result object. Must be 1 or greater. Note: The transactions array in the result object is not affected by this parameter\n3. includewatchonly (boolean, optional, default=false) Unused\n\nResult:\n{\n \"transactions\": [{ (array of object) JSON array of objects containing verbose details of the each transaction\n \"abandoned\": true|false, (boolean) Unset\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) Payment address for a transaction output\n \"amount\": n.nnn, (numeric) The value of the transaction output valued in bitcoin\n \"bip125-replaceable\": \"value\", (string) Unset\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"category\": \"value\", (string) The kind of transaction: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs. Note: A single output may be included multiple times under different categories\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"fee\": n.nnn, (numeric) The total input value minus the total output value for sent transactions\n \"generated\": true|false, (boolean) Whether the transaction output is a coinbase output\n \"involveswatchonly\": true|false, (boolean) Unset\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"trusted\": true|false, (boolean) Unset\n \"txid\": \"value\", (string) The hash of the transaction\n \"vout\": n, (numeric) The transaction output index\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"comment\": \"value\", (string) Unset\n \"otheraccount\": \"value\", (string) Unset\n },...], \n \"lastblock\": \"value\", (string) Hash of the latest-synced block to be used in later calls to listsinceblock\n} \n", + "listtransactions": "listtransactions (\"account\" count=10 from=0 includewatchonly=false)\n\nReturns a JSON array of objects containing verbose details for wallet transactions.\n\nArguments:\n1. account (string, optional) DEPRECATED -- Unused (must be unset or \"*\")\n2. count (numeric, optional, default=10) Maximum number of transactions to create results from\n3. from (numeric, optional, default=0) Number of transactions to skip before results are created\n4. includewatchonly (boolean, optional, default=false) Unused\n\nResult:\n[{\n \"abandoned\": true|false, (boolean) Unset\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) Payment address for a transaction output\n \"amount\": n.nnn, (numeric) The value of the transaction output valued in bitcoin\n \"bip125-replaceable\": \"value\", (string) Unset\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"category\": \"value\", (string) The kind of transaction: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs. Note: A single output may be included multiple times under different categories\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"fee\": n.nnn, (numeric) The total input value minus the total output value for sent transactions\n \"generated\": true|false, (boolean) Whether the transaction output is a coinbase output\n \"involveswatchonly\": true|false, (boolean) Unset\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"trusted\": true|false, (boolean) Unset\n \"txid\": \"value\", (string) The hash of the transaction\n \"vout\": n, (numeric) The transaction output index\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"comment\": \"value\", (string) Unset\n \"otheraccount\": \"value\", (string) Unset\n},...]\n", + "listunspent": "listunspent (minconf=1 maxconf=9999999 [\"address\",...])\n\nReturns a JSON array of objects representing unlocked unspent outputs controlled by wallet keys.\n\nArguments:\n1. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is considered\n2. maxconf (numeric, optional, default=9999999) Maximum number of block confirmations required before a transaction output is excluded\n3. addresses (array of string, optional) If set, limits the returned details to unspent outputs received by any of these payment addresses\n\nResult:\n{\n \"txid\": \"value\", (string) The transaction hash of the referenced output\n \"vout\": n, (numeric) The output index of the referenced output\n \"address\": \"value\", (string) The payment address that received the output\n \"account\": \"value\", (string) The account associated with the receiving payment address\n \"scriptPubKey\": \"value\", (string) The output script encoded as a hexadecimal string\n \"redeemScript\": \"value\", (string) Unset\n \"amount\": n.nnn, (numeric) The amount of the output valued in bitcoin\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"spendable\": true|false, (boolean) Whether the output is entirely controlled by wallet keys/scripts (false for partially controlled multisig outputs or outputs to watch-only addresses)\n} \n", + "lockunspent": "lockunspent unlock [{\"txid\":\"value\",\"vout\":n},...]\n\nLocks or unlocks an unspent output.\nLocked outputs are not chosen for transaction inputs of authored transactions and are not included in 'listunspent' results.\nLocked outputs are volatile and are not saved across wallet restarts.\nIf unlock is true and no transaction outputs are specified, all locked outputs are marked unlocked.\n\nArguments:\n1. unlock (boolean, required) True to unlock outputs, false to lock\n2. transactions (array of object, required) Transaction outputs to lock or unlock\n[{\n \"txid\": \"value\", (string) The transaction hash of the referenced output\n \"vout\": n, (numeric) The output index of the referenced output\n},...]\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", + "sendfrom": "sendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\n\nDEPRECATED -- Authors, signs, and sends a transaction that outputs some amount to a payment address.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) Account to pick unspent outputs from\n2. toaddress (string, required) Address to pay\n3. amount (numeric, required) Amount to send to the payment address valued in bitcoin\n4. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is eligible to be spent\n5. comment (string, optional) Unused\n6. commentto (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", + "sendmany": "sendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\n\nAuthors, signs, and sends a transaction that outputs to many payment addresses.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) DEPRECATED -- Account to pick unspent outputs from\n2. amounts (object, required) Pairs of payment addresses and the output amount to pay each\n{\n \"Address to pay\": Amount to send to the payment address valued in bitcoin, (object) JSON object using payment addresses as keys and output amounts valued in bitcoin to send to each address\n ...\n}\n3. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is eligible to be spent\n4. comment (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", + "sendtoaddress": "sendtoaddress \"address\" amount (\"comment\" \"commentto\")\n\nAuthors, signs, and sends a transaction that outputs some amount to a payment address.\nUnlike sendfrom, outputs are always chosen from the default account.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. address (string, required) Address to pay\n2. amount (numeric, required) Amount to send to the payment address valued in bitcoin\n3. comment (string, optional) Unused\n4. commentto (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", + "settxfee": "settxfee amount\n\nModify the increment used each time more fee is required for an authored transaction.\n\nArguments:\n1. amount (numeric, required) The new fee increment valued in bitcoin\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", + "signmessage": "signmessage \"address\" \"message\"\n\nSigns a message using the private key of a payment address.\n\nArguments:\n1. address (string, required) Payment address of private key used to sign the message with\n2. message (string, required) Message to sign\n\nResult:\n\"value\" (string) The signed message encoded as a base64 string\n", + "signrawtransaction": "signrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\n\nSigns transaction inputs using private keys from this wallet and request.\nThe valid flags options are ALL, NONE, SINGLE, ALL|ANYONECANPAY, NONE|ANYONECANPAY, and SINGLE|ANYONECANPAY.\n\nArguments:\n1. rawtx (string, required) Unsigned or partially unsigned transaction to sign encoded as a hexadecimal string\n2. inputs (array of object, optional) Additional data regarding inputs that this wallet may not be tracking\n3. privkeys (array of string, optional) Additional WIF-encoded private keys to use when creating signatures\n4. flags (string, optional, default=\"ALL\") Sighash flags\n\nResult:\n{\n \"hex\": \"value\", (string) The resulting transaction encoded as a hexadecimal string\n \"complete\": true|false, (boolean) Whether all input signatures have been created\n \"errors\": [{ (array of object) Script verification errors (if exists)\n \"txid\": \"value\", (string) The transaction hash of the referenced previous output\n \"vout\": n, (numeric) The output index of the referenced previous output\n \"scriptSig\": \"value\", (string) The hex-encoded signature script\n \"sequence\": n, (numeric) Script sequence number\n \"error\": \"value\", (string) Verification or signing error related to the input\n },...], \n} \n", + "validateaddress": "validateaddress \"address\"\n\nVerify that an address is valid.\nExtra details are returned if the address is controlled by this wallet.\nThe following fields are valid only when the address is controlled by this wallet (ismine=true): isscript, pubkey, iscompressed, account, addresses, hex, script, and sigsrequired.\nThe following fields are only valid when address has an associated public key: pubkey, iscompressed.\nThe following fields are only valid when address is a pay-to-script-hash address: addresses, hex, and script.\nIf the address is a multisig address controlled by this wallet, the multisig fields will be left unset if the wallet is locked since the redeem script cannot be decrypted.\n\nArguments:\n1. address (string, required) Address to validate\n\nResult:\n{\n \"isvalid\": true|false, (boolean) Whether or not the address is valid\n \"address\": \"value\", (string) The payment address (only when isvalid is true)\n \"ismine\": true|false, (boolean) Whether this address is controlled by the wallet (only when isvalid is true)\n \"iswatchonly\": true|false, (boolean) Unset\n \"isscript\": true|false, (boolean) Whether the payment address is a pay-to-script-hash address (only when isvalid is true)\n \"pubkey\": \"value\", (string) The associated public key of the payment address, if any (only when isvalid is true)\n \"iscompressed\": true|false, (boolean) Whether the address was created by hashing a compressed public key, if any (only when isvalid is true)\n \"account\": \"value\", (string) The account this payment address belongs to (only when isvalid is true)\n \"addresses\": [\"value\",...], (array of string) All associated payment addresses of the script if address is a multisig address (only when isvalid is true)\n \"hex\": \"value\", (string) The redeem script \n \"script\": \"value\", (string) The class of redeem script for a multisig address\n \"sigsrequired\": n, (numeric) The number of required signatures to redeem outputs to the multisig address\n} \n", + "verifymessage": "verifymessage \"address\" \"signature\" \"message\"\n\nVerify a message was signed with the associated private key of some address.\n\nArguments:\n1. address (string, required) Address used to sign message\n2. signature (string, required) The signature to verify\n3. message (string, required) The message to verify\n\nResult:\ntrue|false (boolean) Whether the message was signed with the private key of 'address'\n", + "walletlock": "walletlock\n\nLock the wallet.\n\nArguments:\nNone\n\nResult:\nNothing\n", + "walletpassphrase": "walletpassphrase \"passphrase\" timeout\n\nUnlock the wallet.\n\nArguments:\n1. passphrase (string, required) The wallet passphrase\n2. timeout (numeric, required) The number of seconds to wait before the wallet automatically locks\n\nResult:\nNothing\n", + "walletpassphrasechange": "walletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\n\nChange the wallet passphrase.\n\nArguments:\n1. oldpassphrase (string, required) The old wallet passphrase\n2. newpassphrase (string, required) The new wallet passphrase\n\nResult:\nNothing\n", + "createnewaccount": "createnewaccount \"account\"\n\nCreates a new account.\nThe wallet must be unlocked for this request to succeed.\n\nArguments:\n1. account (string, required) Name of the new account\n\nResult:\nNothing\n", + "exportwatchingwallet": "exportwatchingwallet (\"account\" download=false)\n\nCreates and returns a duplicate of the wallet database without any private keys to be used as a watching-only wallet.\n\nArguments:\n1. account (string, optional) Unused (must be unset or \"*\")\n2. download (boolean, optional, default=false) Unused\n\nResult:\n\"value\" (string) The watching-only database encoded as a base64 string\n", + "getbestblock": "getbestblock\n\nReturns the hash and height of the newest block in the best chain that wallet has finished syncing with.\n\nArguments:\nNone\n\nResult:\n{\n \"hash\": \"value\", (string) The hash of the block\n \"height\": n, (numeric) The blockchain height of the block\n} \n", + "getunconfirmedbalance": "getunconfirmedbalance (\"account\")\n\nCalculates the unspent output value of all unmined transaction outputs for an account.\n\nArguments:\n1. account (string, optional) The account to query the unconfirmed balance for (default=\"default\")\n\nResult:\nn.nnn (numeric) Total amount of all unmined unspent outputs of the account valued in bitcoin.\n", + "listaddresstransactions": "listaddresstransactions [\"address\",...] (\"account\")\n\nReturns a JSON array of objects containing verbose details for wallet transactions pertaining some addresses.\n\nArguments:\n1. addresses (array of string, required) Addresses to filter transaction results by\n2. account (string, optional) Unused (must be unset or \"*\")\n\nResult:\n[{\n \"abandoned\": true|false, (boolean) Unset\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) Payment address for a transaction output\n \"amount\": n.nnn, (numeric) The value of the transaction output valued in bitcoin\n \"bip125-replaceable\": \"value\", (string) Unset\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"category\": \"value\", (string) The kind of transaction: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs. Note: A single output may be included multiple times under different categories\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"fee\": n.nnn, (numeric) The total input value minus the total output value for sent transactions\n \"generated\": true|false, (boolean) Whether the transaction output is a coinbase output\n \"involveswatchonly\": true|false, (boolean) Unset\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"trusted\": true|false, (boolean) Unset\n \"txid\": \"value\", (string) The hash of the transaction\n \"vout\": n, (numeric) The transaction output index\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"comment\": \"value\", (string) Unset\n \"otheraccount\": \"value\", (string) Unset\n},...]\n", + "listalltransactions": "listalltransactions (\"account\")\n\nReturns a JSON array of objects in the same format as 'listtransactions' without limiting the number of returned objects.\n\nArguments:\n1. account (string, optional) Unused (must be unset or \"*\")\n\nResult:\n[{\n \"abandoned\": true|false, (boolean) Unset\n \"account\": \"value\", (string) DEPRECATED -- Unset\n \"address\": \"value\", (string) Payment address for a transaction output\n \"amount\": n.nnn, (numeric) The value of the transaction output valued in bitcoin\n \"bip125-replaceable\": \"value\", (string) Unset\n \"blockhash\": \"value\", (string) The hash of the block this transaction is mined in, or the empty string if unmined\n \"blockindex\": n, (numeric) Unset\n \"blocktime\": n, (numeric) The Unix time of the block header this transaction is mined in, or 0 if unmined\n \"category\": \"value\", (string) The kind of transaction: \"send\" for sent transactions, \"immature\" for immature coinbase outputs, \"generate\" for mature coinbase outputs, or \"recv\" for all other received outputs. Note: A single output may be included multiple times under different categories\n \"confirmations\": n, (numeric) The number of block confirmations of the transaction\n \"fee\": n.nnn, (numeric) The total input value minus the total output value for sent transactions\n \"generated\": true|false, (boolean) Whether the transaction output is a coinbase output\n \"involveswatchonly\": true|false, (boolean) Unset\n \"time\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"timereceived\": n, (numeric) The earliest Unix time this transaction was known to exist\n \"trusted\": true|false, (boolean) Unset\n \"txid\": \"value\", (string) The hash of the transaction\n \"vout\": n, (numeric) The transaction output index\n \"walletconflicts\": [\"value\",...], (array of string) Unset\n \"comment\": \"value\", (string) Unset\n \"otheraccount\": \"value\", (string) Unset\n},...]\n", + "renameaccount": "renameaccount \"oldaccount\" \"newaccount\"\n\nRenames an account.\n\nArguments:\n1. oldaccount (string, required) The old account name to rename\n2. newaccount (string, required) The new name for the account\n\nResult:\nNothing\n", + "walletislocked": "walletislocked\n\nReturns whether or not the wallet is locked.\n\nArguments:\nNone\n\nResult:\ntrue|false (boolean) Whether the wallet is locked\n", + } +} + +var LocaleHelpDescs = map[string]func() map[string]string{ + "en_US": HelpDescsEnUS, +} +var RequestUsages = "addmultisigaddress nrequired [\"key\",...] (\"account\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetblockcount\ngetinfo\ngetnewaddress (\"account\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettransaction \"txid\" (includewatchonly=false)\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n},...]\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsettxfee amount\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked" diff --git a/cmd/wallet/server.go b/cmd/wallet/server.go new file mode 100644 index 0000000..161a855 --- /dev/null +++ b/cmd/wallet/server.go @@ -0,0 +1,643 @@ +package wallet + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + js "encoding/json" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/btcsuite/websocket" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainclient" + "github.com/p9c/p9/pkg/interrupt" +) + +type WebsocketClient struct { + conn *websocket.Conn + authenticated bool + remoteAddr string + allRequests chan []byte + responses chan []byte + quit qu.C // closed on disconnect + wg sync.WaitGroup +} + +func NewWebsocketClient(c *websocket.Conn, authenticated bool, remoteAddr string) *WebsocketClient { + return &WebsocketClient{ + conn: c, + authenticated: authenticated, + remoteAddr: remoteAddr, + allRequests: make(chan []byte), + responses: make(chan []byte), + quit: qu.T(), + } +} +func (c *WebsocketClient) Send(b []byte) (e error) { + select { + case c.responses <- b: + return nil + case <-c.quit.Wait(): + return errors.New("websocket client disconnected") + } +} + +// Server holds the items the RPC server may need to access (auth, config, +// shutdown, etc.) +type Server struct { + HTTPServer http.Server + Wallet *Wallet + WalletLoader *Loader + ChainClient chainclient.Interface + // handlerLookup func(string) (requestHandler, bool) + HandlerMutex sync.Mutex + Listeners []net.Listener + AuthSHA [sha256.Size]byte + Upgrader websocket.Upgrader + MaxPostClients int64 // Max concurrent HTTP POST clients. + MaxWebsocketClients int64 // Max concurrent websocket clients. + WG sync.WaitGroup + Quit qu.C + QuitMutex sync.Mutex + RequestShutdownChan qu.C +} + +// JSONAuthFail sends a message back to the client if the http auth is rejected. +func JSONAuthFail(w http.ResponseWriter) { + w.Header().Add("WWW-Authenticate", `Basic realm="pod RPC"`) + http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) +} + +// NewServer creates a new server for serving legacy RPC client connections, both HTTP POST and websocket. +func NewServer(opts *Options, walletLoader *Loader, listeners []net.Listener, quit qu.C) *Server { + serveMux := http.NewServeMux() + const rpcAuthTimeoutSeconds = 10 + server := &Server{ + HTTPServer: http.Server{ + Handler: serveMux, + // Timeout connections which don't complete the initial handshake within the + // allowed timeframe. + ReadTimeout: time.Second * rpcAuthTimeoutSeconds, + }, + WalletLoader: walletLoader, + MaxPostClients: opts.MaxPOSTClients, + MaxWebsocketClients: opts.MaxWebsocketClients, + Listeners: listeners, + // A hash of the HTTP basic auth string is used for a constant time comparison. + AuthSHA: sha256.Sum256(HTTPBasicAuth(opts.Username, opts.Password)), + Upgrader: websocket.Upgrader{ + // Allow all origins. + CheckOrigin: func(r *http.Request) bool { return true }, + }, + Quit: quit, + RequestShutdownChan: qu.Ts(1), + } + serveMux.Handle( + "/", ThrottledFn( + opts.MaxPOSTClients, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "close") + w.Header().Set("Content-Type", "application/json") + r.Close = true + if e := server.CheckAuthHeader(r); E.Chk(e) { + W.Ln("unauthorized client connection attempt") + JSONAuthFail(w) + return + } + server.WG.Add(1) + server.POSTClientRPC(w, r) + server.WG.Done() + }, + ), + ) + serveMux.Handle( + "/ws", ThrottledFn( + opts.MaxWebsocketClients, + func(w http.ResponseWriter, r *http.Request) { + authenticated := false + switch server.CheckAuthHeader(r) { + case nil: + authenticated = true + case ErrNoAuth: + // nothing + default: + // If auth was supplied but incorrect, rather than simply being missing, + // immediately terminate the connection. + W.Ln("disconnecting improperly authorized websocket client") + JSONAuthFail(w) + return + } + conn, e := server.Upgrader.Upgrade(w, r, nil) + if e != nil { + W.F( + "cannot websocket upgrade client %s: %v", + r.RemoteAddr, e, + ) + return + } + wsc := NewWebsocketClient(conn, authenticated, r.RemoteAddr) + server.WebsocketClientRPC(wsc) + }, + ), + ) + for _, lis := range listeners { + server.Serve(lis) + } + return server +} + +// HTTPBasicAuth returns the UTF-8 bytes of the HTTP Basic authentication string: +// +// "Basic " + base64(username + ":" + password) +func HTTPBasicAuth(username, password string) []byte { + const header = "Basic " + b64 := base64.StdEncoding + b64InputLen := len(username) + len(":") + len(password) + b64Input := make([]byte, 0, b64InputLen) + b64Input = append(b64Input, username...) + b64Input = append(b64Input, ':') + b64Input = append(b64Input, password...) + output := make([]byte, len(header)+b64.EncodedLen(b64InputLen)) + copy(output, header) + b64.Encode(output[len(header):], b64Input) + return output +} + +// Serve serves HTTP POST and websocket RPC for the legacy JSON-RPC RPC server. +// This function does not block on lis.Accept. +func (s *Server) Serve(lis net.Listener) { + s.WG.Add(1) + go func() { + I.Ln("wallet RPC server listening on ", lis.Addr()) + var e error + if e = s.HTTPServer.Serve(lis); E.Chk(e) { + } + D.Ln("finished serving wallet RPC:", e) + s.WG.Done() + }() +} + +// RegisterWallet associates the legacy RPC server with the wallet. This +// function must be called before any wallet RPCs can be called by clients. +func (s *Server) RegisterWallet(w *Wallet) { + s.HandlerMutex.Lock() + s.Wallet = w + s.HandlerMutex.Unlock() +} + +// Stop gracefully shuts down the rpc server by stopping and disconnecting all +// clients, disconnecting the chain server connection, and closing the wallet's +// account files. This blocks until shutdown completes. +func (s *Server) Stop() { + s.QuitMutex.Lock() + select { + case <-s.Quit.Wait(): + s.QuitMutex.Unlock() + return + default: + } + // Stop the connected wllt and chain server, if any. + s.HandlerMutex.Lock() + wllt := s.Wallet + chainClient := s.ChainClient + s.HandlerMutex.Unlock() + if wllt != nil { + wllt.Stop() + } + if chainClient != nil { + chainClient.Stop() + } + // Stop all the listeners. + for _, listener := range s.Listeners { + e := listener.Close() + if e != nil { + E.F( + "cannot close listener `%s`: %v %s", + listener.Addr(), e, + ) + } + } + // Signal the remaining goroutines to stop. + s.Quit.Q() + s.QuitMutex.Unlock() + // First wait for the wallet and chain server to stop, if they were ever set. + if wllt != nil { + wllt.WaitForShutdown() + } + if chainClient != nil { + chainClient.WaitForShutdown() + } + // Wait for all remaining goroutines to exit. + s.WG.Wait() +} + +// SetChainServer sets the chain server client component needed to run a fully +// functional bitcoin wallet RPC server. This can be called to enable RPC +// passthrough even before a loaded wallet is set, but the wallet's RPC client +// is preferred. +func (s *Server) SetChainServer(chainClient chainclient.Interface) { + s.HandlerMutex.Lock() + s.ChainClient = chainClient + s.HandlerMutex.Unlock() +} + +// HandlerClosure creates a closure function for handling requests of the given +// method. This may be a request that is handled directly by btcwallet, or a +// chain server request that is handled by passing the request down to pod. +// +// NOTE: These handlers do not handle special cases, such as the authenticate +// method. Each of these must be checked beforehand (the method is already +// known) and handled accordingly. +func (s *Server) HandlerClosure(request *btcjson.Request) LazyHandler { + s.HandlerMutex.Lock() + // With the lock held, make copies of these pointers for the closure. + wllt := s.Wallet + chainClient := s.ChainClient + if wllt != nil && chainClient == nil { + chainClient = wllt.ChainClient() + s.ChainClient = chainClient + D.Ln("HandlerClosure got the ChainClient") + } + s.HandlerMutex.Unlock() + return LazyApplyHandler(request, wllt, chainClient) +} + +// ErrNoAuth represents an error where authentication could not succeed due to a +// missing Authorization HTTP header. +var ErrNoAuth = errors.New("no auth") + +// CheckAuthHeader checks the HTTP Basic authentication supplied by a client in +// the HTTP request r. It errors with ErrNoAuth if the request does not contain +// the Authorization header, or another non-nil error if the authentication was +// provided but incorrect. +// +// This check is time-constant. +func (s *Server) CheckAuthHeader(r *http.Request) (e error) { + authHdr := r.Header["Authorization"] + if len(authHdr) == 0 { + return ErrNoAuth + } + authSHA := sha256.Sum256([]byte(authHdr[0])) + cmp := subtle.ConstantTimeCompare(authSHA[:], s.AuthSHA[:]) + if cmp != 1 { + return errors.New("bad auth") + } + return nil +} + +// ThrottledFn wraps an http.HandlerFunc with throttling of concurrent active +// clients by responding with an HTTP 429 when the threshold is crossed. +func ThrottledFn(threshold int64, f http.HandlerFunc) http.Handler { + return Throttled(threshold, f) +} + +// Throttled wraps an http.Handler with throttling of concurrent active clients +// by responding with an HTTP 429 when the threshold is crossed. +func Throttled(threshold int64, h http.Handler) http.Handler { + var active int64 + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + current := atomic.AddInt64(&active, 1) + defer atomic.AddInt64(&active, -1) + if current-1 >= threshold { + W.F( + "reached threshold of %d concurrent active clients", threshold, + ) + http.Error(w, "429 Too Many Requests", 429) + return + } + h.ServeHTTP(w, r) + }, + ) +} + +// // sanitizeRequest returns a sanitized string for the request which may be +// // safely logged. It is intended to strip private keys, passphrases, and any +// // other secrets from request parameters before they may be saved to a log file. +// func sanitizeRequest(// r *json.Request) string { +// // These are considered unsafe to log, so sanitize parameters. +// switch r.Method { +// case "encryptwallet", "importprivkey", "importwallet", +// "signrawtransaction", "walletpassphrase", +// "walletpassphrasechange": +// return fmt.Sprintf(`{"id":%v,"method":"%s","netparams":SANITIZED %d parameters}`, +// r.ID, r.Method, len(r.Params)) +// } +// return fmt.Sprintf(`{"id":%v,"method":"%s","netparams":%v}`, r.ID, +// r.Method, r.Params) +// } + +// IDPointer returns a pointer to the passed ID, or nil if the interface is nil. Interface pointers are usually a red +// flag of doing something incorrectly, but this is only implemented here to work around an oddity with json, which uses +// empty interface pointers for response IDs. +func IDPointer(id interface{}) (p *interface{}) { + if id != nil { + p = &id + } + return +} + +// InvalidAuth checks whether a websocket request is a valid (parsable) authenticate request and checks the supplied +// username and passphrase against the server auth. +func (s *Server) InvalidAuth(req *btcjson.Request) bool { + cmd, e := btcjson.UnmarshalCmd(req) + if e != nil { + return false + } + authCmd, ok := cmd.(*btcjson.AuthenticateCmd) + if !ok { + return false + } + // Chk credentials. + login := authCmd.Username + ":" + authCmd.Passphrase + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) + authSha := sha256.Sum256([]byte(auth)) + return subtle.ConstantTimeCompare(authSha[:], s.AuthSHA[:]) != 1 +} +func (s *Server) WebsocketClientRead(wsc *WebsocketClient) { + for { + _, request, e := wsc.conn.ReadMessage() + if e != nil { + if e != io.EOF && e != io.ErrUnexpectedEOF { + W.F( + "websocket receive failed from client %s: %v", + wsc.remoteAddr, e, + ) + } + close(wsc.allRequests) + break + } + wsc.allRequests <- request + } +} +func (s *Server) WebsocketClientRespond(wsc *WebsocketClient) { + // A for-select with a read of the quit channel is used instead of a for-range to provide clean shutdown. This is + // necessary due to WebsocketClientRead (which sends to the allRequests chan) not closing allRequests during + // shutdown if the remote websocket client is still connected. +out: + for { + select { + case reqBytes, ok := <-wsc.allRequests: + if !ok { + // client disconnected + break out + } + var req btcjson.Request + e := js.Unmarshal(reqBytes, &req) + if e != nil { + if !wsc.authenticated { + // Disconnect immediately. + break out + } + resp := MakeResponse( + req.ID, nil, + btcjson.ErrRPCInvalidRequest, + ) + mResp, e := js.Marshal(resp) + // We expect the marshal to succeed. If it doesn't, it indicates some non-marshalable type in the + // response. + if e != nil { + panic(e) + } + e = wsc.Send(mResp) + if e != nil { + break out + } + continue + } + if req.Method == "authenticate" { + if wsc.authenticated || s.InvalidAuth(&req) { + // Disconnect immediately. + break out + } + wsc.authenticated = true + resp := MakeResponse(req.ID, nil, nil) + // Expected to never fail. + mResp, e := js.Marshal(resp) + if e != nil { + panic(e) + } + e = wsc.Send(mResp) + if e != nil { + break out + } + continue + } + if !wsc.authenticated { + // Disconnect immediately. + break out + } + switch req.Method { + case "stop": + resp := MakeResponse( + req.ID, + "wallet stopping.", nil, + ) + mResp, e := js.Marshal(resp) + // Expected to never fail. + if e != nil { + panic(e) + } + e = wsc.Send(mResp) + if e != nil { + break out + } + s.RequestProcessShutdown() + // break + case "restart": + resp := MakeResponse( + req.ID, + "wallet restarting.", nil, + ) + mResp, e := js.Marshal(resp) + // Expected to never fail. + if e != nil { + panic(e) + } + e = wsc.Send(mResp) + if e != nil { + break out + } + interrupt.Restart = true + s.RequestProcessShutdown() + // break + default: + req := req // Copy for the closure + f := s.HandlerClosure(&req) + wsc.wg.Add(1) + go func() { + resp, jsonErr := f() + mResp, e := btcjson.MarshalResponse(req.ID, resp, jsonErr) + if e != nil { + E.Ln( + "unable to marshal response:", e, + ) + } else { + _ = wsc.Send(mResp) + } + wsc.wg.Done() + }() + } + case <-s.Quit.Wait(): + break out + } + } + // allow client to disconnect after all handler goroutines are done + wsc.wg.Wait() + close(wsc.responses) + s.WG.Done() +} +func (s *Server) WebsocketClientSend(wsc *WebsocketClient) { + const deadline = 2 * time.Second +out: + for { + select { + case response, ok := <-wsc.responses: + if !ok { + // client disconnected + break out + } + e := wsc.conn.SetWriteDeadline(time.Now().Add(deadline)) + if e != nil { + W.F( + "cannot set write deadline on client %s: %v", + wsc.remoteAddr, e, + ) + } + e = wsc.conn.WriteMessage( + websocket.TextMessage, + response, + ) + if e != nil { + W.F( + "failed websocket send to client %s: %v", wsc.remoteAddr, e, + ) + break out + } + case <-s.Quit.Wait(): + break out + } + } + wsc.quit.Q() + I.Ln("disconnected websocket client", wsc.remoteAddr) + s.WG.Done() +} + +// WebsocketClientRPC starts the goroutines to serve JSON-RPC requests over a websocket connection for a single client. +func (s *Server) WebsocketClientRPC(wsc *WebsocketClient) { + I.F("new websocket client %s", wsc.remoteAddr) + // Clear the read deadline set before the websocket hijacked + // the connection. + if e := wsc.conn.SetReadDeadline(time.Time{}); E.Chk(e) { + W.Ln("cannot remove read deadline:", e) + } + // WebsocketClientRead is intentionally not run with the waitgroup so it is ignored during shutdown. This is to + // prevent a hang during shutdown where the goroutine is blocked on a read of the websocket connection if the client + // is still connected. + go s.WebsocketClientRead(wsc) + s.WG.Add(2) + go s.WebsocketClientRespond(wsc) + go s.WebsocketClientSend(wsc) + <-wsc.quit +} + +// MaxRequestSize specifies the maximum number of bytes in the request body that may be read from a client. This is +// currently limited to 4MB. +const MaxRequestSize = 1024 * 1024 * 4 + +// POSTClientRPC processes and replies to a JSON-RPC client request. +func (s *Server) POSTClientRPC(w http.ResponseWriter, r *http.Request) { + body := http.MaxBytesReader(w, r.Body, MaxRequestSize) + rpcRequest, e := ioutil.ReadAll(body) + if e != nil { + // TODO: what if the underlying reader errored? + http.Error( + w, "413 Request Too Large.", + http.StatusRequestEntityTooLarge, + ) + return + } + // First check whether wallet has a handler for this request's method. If unfound, the request is sent to the chain + // server for further processing. While checking the methods, disallow authenticate requests, as they are invalid + // for HTTP POST clients. + var req btcjson.Request + e = js.Unmarshal(rpcRequest, &req) + if e != nil { + var resp []byte + resp, e = btcjson.MarshalResponse(req.ID, nil, btcjson.ErrRPCInvalidRequest) + if e != nil { + E.Ln( + "Unable to marshal response:", e, + ) + http.Error( + w, "500 Internal Server BTCJSONError", + http.StatusInternalServerError, + ) + return + } + _, e = w.Write(resp) + if e != nil { + W.Ln( + "cannot write invalid request request to client:", e, + ) + } + return + } + // Create the response and error from the request. Two special cases are handled for the authenticate and stop + // request methods. + var res interface{} + var jsonErr *btcjson.RPCError + var stop bool + switch req.Method { + case "authenticate": + // Drop it. + return + case "stop": + stop = true + res = "pod/wallet stopping" + case "restart": + stop = true + res = "pod/wallet restarting" + default: + res, jsonErr = s.HandlerClosure(&req)() + } + // Marshal and send. + mResp, e := btcjson.MarshalResponse(req.ID, res, jsonErr) + if e != nil { + E.Ln( + "unable to marshal response:", e, + ) + http.Error(w, "500 Internal Server BTCJSONError", http.StatusInternalServerError) + return + } + _, e = w.Write(mResp) + if e != nil { + W.Ln( + "unable to respond to client:", e, + ) + } + if stop { + s.RequestProcessShutdown() + } +} +func (s *Server) RequestProcessShutdown() { + select { + case s.RequestShutdownChan <- struct{}{}: + default: + } +} + +// RequestProcessShutdownChan returns a channel that is sent to when an authorized client requests remote shutdown. +func (s *Server) RequestProcessShutdownChan() qu.C { + return s.RequestShutdownChan +} diff --git a/cmd/wallet/unstable.go b/cmd/wallet/unstable.go new file mode 100644 index 0000000..14f7f2b --- /dev/null +++ b/cmd/wallet/unstable.go @@ -0,0 +1,42 @@ +package wallet + +import ( + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wtxmgr" +) + +// UnstableAPI exposes unstable api in the wallet +type UnstableAPI struct { + w *Wallet +} + +// ExposeUnstableAPI exposes additional unstable public APIs for a Wallet. These APIs may be changed or removed at any +// time. Currently this type exists to ease the transation (particularly for the legacy JSON-RPC server) from using +// exported manager packages to a unified wallet package that exposes all functionality by itself. New code should not +// be written using this API. +func ExposeUnstableAPI(w *Wallet) UnstableAPI { + return UnstableAPI{w} +} + +// TxDetails calls wtxmgr.Store.TxDetails under a single database view transaction. +func (u UnstableAPI) TxDetails(txHash *chainhash.Hash) (details *wtxmgr.TxDetails, e error) { + e = walletdb.View( + u.w.db, func(dbtx walletdb.ReadTx) (e error) { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + details, e = u.w.TxStore.TxDetails(txmgrNs, txHash) + return e + }, + ) + return +} + +// RangeTransactions calls wtxmgr.Store.RangeTransactions under a single database view transaction. +func (u UnstableAPI) RangeTransactions(begin, end int32, f func([]wtxmgr.TxDetails) (bool, error)) error { + return walletdb.View( + u.w.db, func(dbtx walletdb.ReadTx) (e error) { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + return u.w.TxStore.RangeTransactions(txmgrNs, begin, end, f) + }, + ) +} diff --git a/cmd/wallet/utxos.go b/cmd/wallet/utxos.go new file mode 100644 index 0000000..5818bd1 --- /dev/null +++ b/cmd/wallet/utxos.go @@ -0,0 +1,79 @@ +package wallet + +import ( + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wire" +) + +// OutputSelectionPolicy describes the rules for selecting an output from the wallet. +type OutputSelectionPolicy struct { + Account uint32 + RequiredConfirmations int32 +} + +func (p *OutputSelectionPolicy) meetsRequiredConfs(txHeight, curHeight int32) bool { + return confirmed(p.RequiredConfirmations, txHeight, curHeight) +} + +// UnspentOutputs fetches all unspent outputs from the wallet that match rules described in the passed policy. +func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOutput, error) { + var outputResults []*TransactionOutput + e := walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + syncBlock := w.Manager.SyncedTo() + // TODO: actually stream outputs from the db instead of fetching all of them at once. + outputs, e := w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return e + } + for _, output := range outputs { + // Ignore outputs that haven't reached the required number of confirmations. + if !policy.meetsRequiredConfs(output.Height, syncBlock.Height) { + continue + } + // Ignore outputs that are not controlled by the account. + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, + w.chainParams, + ) + if e != nil || len(addrs) == 0 { + // Cannot determine which account this belongs to without a valid address. + // + // TODO: Fix this by saving outputs per account, or accounts per output. + continue + } + var outputAcct uint32 + _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + if e != nil { + return e + } + if outputAcct != policy.Account { + continue + } + // Stakebase isn't exposed by wtxmgr so those will be OutputKindNormal for now. + outputSource := OutputKindNormal + if output.FromCoinBase { + outputSource = OutputKindCoinbase + } + result := &TransactionOutput{ + OutPoint: output.OutPoint, + Output: wire.TxOut{ + Value: int64(output.Amount), + PkScript: output.PkScript, + }, + OutputKind: outputSource, + ContainingBlock: BlockIdentity(output.Block), + ReceiveTime: output.Received, + } + outputResults = append(outputResults, result) + } + return nil + }, + ) + return outputResults, e +} diff --git a/cmd/wallet/wallet.go b/cmd/wallet/wallet.go new file mode 100644 index 0000000..478c740 --- /dev/null +++ b/cmd/wallet/wallet.go @@ -0,0 +1,3348 @@ +package wallet + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/davecgh/go-spew/spew" + + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pod/config" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainclient" + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/txauthor" + "github.com/p9c/p9/pkg/txrules" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/util/hdkeychain" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wire" + "github.com/p9c/p9/pkg/wtxmgr" +) + +const ( + // InsecurePubPassphrase is the default outer encryption passphrase used for public data (everything but private + // keys). Using a non-default public passphrase can prevent an attacker without the public passphrase from + // discovering all past and future wallet addresses if they gain access to the wallet database. + // + // NOTE: at time of writing, public encryption only applies to public data in the waddrmgr namespace. Transactions + // are not yet encrypted. + InsecurePubPassphrase = "" + // walletDbWatchingOnlyName = "wowallet.db" recoveryBatchSize is the default number of blocks that will be scanned + // successively by the recovery manager, in the event that the wallet is started in recovery mode. + recoveryBatchSize = 2000 +) + +// ErrNotSynced describes an error where an operation cannot complete due wallet being out of sync (and perhaps +// currently syncing with) the remote chain server. +var ErrNotSynced = errors.New("wallet is not synchronized with the chain server") + +// Namespace bucket keys. +var ( + waddrmgrNamespaceKey = []byte("waddrmgr") + wtxmgrNamespaceKey = []byte("wtxmgr") +) + +// Wallet is a structure containing all the components for a complete wallet. It contains the Armory-style key store +// addresses and keys), +type Wallet struct { + publicPassphrase []byte + // Data stores + db walletdb.DB + Manager *waddrmgr.Manager + TxStore *wtxmgr.Store + chainClient chainclient.Interface + chainClientLock sync.Mutex + chainClientSynced bool + chainClientSyncMtx sync.Mutex + lockedOutpoints map[wire.OutPoint]struct{} + recoveryWindow uint32 + // Channels for rescan processing. Requests are added and merged with any waiting requests, before being sent to + // another goroutine to call the rescan RPC. + rescanAddJob chan *RescanJob + rescanBatch chan *rescanBatch + rescanNotifications chan interface{} // From chain server + rescanProgress chan *RescanProgressMsg + rescanFinished chan *RescanFinishedMsg + // Channel for transaction creation requests. + createTxRequests chan createTxRequest + // Channels for the manager locker. + unlockRequests chan unlockRequest + lockRequests qu.C + holdUnlockRequests chan chan heldUnlock + lockState chan bool + changePassphrase chan changePassphraseRequest + changePassphrases chan changePassphrasesRequest + // Information for reorganization handling. + // reorganizingLock sync.Mutex + // reorganizeToHash chainhash.Hash + // reorganizing bool + NtfnServer *NotificationServer + PodConfig *config.Config + chainParams *chaincfg.Params + wg sync.WaitGroup + started bool + quit qu.C + quitMu sync.Mutex + Update qu.C +} + +// Start starts the goroutines necessary to manage a wallet. +func (w *Wallet) Start() { + T.Ln("starting wallet") + w.quitMu.Lock() + T.Ln("locked wallet quit mutex") + select { + case <-w.quit.Wait(): + T.Ln("waiting for wallet shutdown") + // Restart the wallet goroutines after shutdown finishes. + w.WaitForShutdown() + w.quit = qu.T() + default: + if w.started { + // Ignore when the wallet is still running. + I.Ln("wallet already started") + w.quitMu.Unlock() + return + } + w.started = true + } + w.quitMu.Unlock() + T.Ln("wallet quit mutex unlocked") + w.wg.Add(2) + go w.txCreator() + go w.walletLocker() +} + +// SynchronizeRPC associates the wallet with the consensus RPC client, synchronizes the wallet with the latest changes +// to the blockchain, and continuously updates the wallet through RPC notifications. +// +// This method is unstable and will be removed when all syncing logic is moved outside of the wallet package. +func (w *Wallet) SynchronizeRPC(chainClient chainclient.Interface) { + T.Ln("SynchronizeRPC") + w.quitMu.Lock() + select { + case <-w.quit.Wait(): + w.quitMu.Unlock() + return + default: + } + w.quitMu.Unlock() + // TODO: Ignoring the new client when one is already set breaks callers + // who are replacing the client, perhaps after a disconnect. + T.Ln("locking wallet chain client mutex") + w.chainClientLock.Lock() + if w.chainClient != nil { + T.Ln("chain client is nil, unlocking wallet chain client mutex") + w.chainClientLock.Unlock() + return + } + w.chainClient = chainClient + // If the chain client is a NeutrinoClient instance, set a birthday so we don't download all the filters as we go. + switch cc := chainClient.(type) { + // case *chainclient.NeutrinoClient: + // cc.SetStartTime(w.Manager.Birthday()) + case *chainclient.BitcoindClient: + cc.SetBirthday(w.Manager.Birthday()) + } + T.Ln("unlocking wallet chain client mutex") + w.chainClientLock.Unlock() + T.Ln("unlocked wallet chain client mutex") + // TODO: It would be preferable to either run these goroutines separately from the wallet (use wallet mutator + // functions to make changes from the RPC client) and not have to stop and restart them each time the client + // disconnects and reconnets. + w.wg.Add(4) + go w.handleChainNotifications() + go w.rescanBatchHandler() + go w.rescanProgressHandler() + go w.rescanRPCHandler() +} + +// requireChainClient marks that a wallet method can only be completed when the consensus RPC server is set. This +// function and all functions that call it are unstable and will need to be moved when the syncing code is moved out of +// the wallet. +func (w *Wallet) requireChainClient() (chainclient.Interface, error) { + T.Ln("requireChainClient") + w.chainClientLock.Lock() + chainClient := w.chainClient + w.chainClientLock.Unlock() + if chainClient == nil { + T.Ln("chain client is nil") + return nil, errors.New("wallet->chain RPC is inactive") + } + return chainClient, nil +} + +// ChainClient returns the optional consensus RPC client associated with the wallet. +// +// This function is unstable and will be removed once sync logic is moved out of the wallet. +func (w *Wallet) ChainClient() chainclient.Interface { + T.Ln("wallet acquiring connect to chain RPC") + w.chainClientLock.Lock() + T.Ln("chainClientLock locked", w.chainClient == nil) + chainClient := w.chainClient + w.chainClientLock.Unlock() + T.Ln("chainClientLock unlocked") + return chainClient +} + +// quitChan atomically reads the quit channel. +func (w *Wallet) quitChan() qu.C { + w.quitMu.Lock() + c := w.quit + w.quitMu.Unlock() + return c +} + +// Stop signals all wallet goroutines to shutdown. +func (w *Wallet) Stop() { + // T.Ln("w", w, "w.quitMu", w.quitMu) + w.quitMu.Lock() + defer w.quitMu.Unlock() + select { + case <-w.quit.Wait(): + default: + w.chainClientLock.Lock() + if w.chainClient != nil { + w.chainClient.Stop() + w.chainClient = nil + } + w.chainClientLock.Unlock() + w.quit.Q() + // return + } +} + +// ShuttingDown returns whether the wallet is currently in the process of shutting down or not. +func (w *Wallet) ShuttingDown() bool { + select { + case <-w.quitChan().Wait(): + return true + default: + return false + } +} + +// WaitForShutdown blocks until all wallet goroutines have finished executing. +func (w *Wallet) WaitForShutdown() { + T.Ln("waiting for shutdown") + w.chainClientLock.Lock() + T.Ln("locked", w.chainClient) + if w.chainClient != nil { + T.Ln("calling WaitForShutdown") + w.chainClient.WaitForShutdown() + } + T.Ln("unlocking") + w.chainClientLock.Unlock() + // T.Ln("waiting on waitgroup") + // w.wg.Wait() +} + +// SynchronizingToNetwork returns whether the wallet is currently synchronizing with the Bitcoin network. +func (w *Wallet) SynchronizingToNetwork() bool { + // At the moment, RPC is the only synchronization method. In the future, when SPV is added, a separate check will + // also be needed, or SPV could always be enabled if RPC was not explicitly specified when creating the wallet. + w.chainClientSyncMtx.Lock() + syncing := w.chainClient != nil + w.chainClientSyncMtx.Unlock() + return syncing +} + +// ChainSynced returns whether the wallet has been attached to a chain server and synced up to the best block on the +// main chain. +func (w *Wallet) ChainSynced() bool { + w.chainClientSyncMtx.Lock() + synced := w.chainClientSynced + w.chainClientSyncMtx.Unlock() + return synced +} + +// SetChainSynced marks whether the wallet is connected to and currently in sync with the latest block notified by the +// chain server. +// +// NOTE: Due to an API limitation with rpcclient, this may return true after the client disconnected (and is attempting +// a reconnect). This will be unknown until the reconnect notification is received, at which point the wallet can be +// marked out of sync again until after the next rescan completes. +func (w *Wallet) SetChainSynced(synced bool) { + w.chainClientSyncMtx.Lock() + w.chainClientSynced = synced + w.chainClientSyncMtx.Unlock() +} + +// activeData returns the currently-active receiving addresses and all unspent outputs. This is primarily intended to +// provide the parameters for a rescan request. +func (w *Wallet) activeData(dbtx walletdb.ReadTx) ( + addrs []btcaddr.Address, unspent []wtxmgr.Credit, e error, +) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + if e = w.Manager.ForEachActiveAddress( + addrmgrNs, func(addr btcaddr.Address) (e error) { + addrs = append(addrs, addr) + return nil + }, + ); E.Chk(e) { + return nil, nil, e + } + if unspent, e = w.TxStore.UnspentOutputs(txmgrNs); E.Chk(e) { + } + return addrs, unspent, e +} + +// syncWithChain brings the wallet up to date with the current chain server connection. It creates a rescan request and +// blocks until the rescan has finished. +func (w *Wallet) syncWithChain() (e error) { + T.Ln("syncWithChain") + var chainClient chainclient.Interface + chainClient, e = w.requireChainClient() + if e != nil { + return e + } + // Request notifications for transactions sending to all wallet addresses. + var ( + addrs []btcaddr.Address + unspent []wtxmgr.Credit + ) + e = walletdb.View( + w.db, func(dbtx walletdb.ReadTx) (e error) { + addrs, unspent, e = w.activeData(dbtx) + return e + }, + ) + if e != nil { + W.Ln("error starting sync", e) + return e + } + startHeight := w.Manager.SyncedTo().Height + // We'll mark this as our first sync if we don't have any unspent outputs as known by the wallet. This will allow us + // to skip a full rescan at this height, and instead wait for the backend to catch up. + isInitialSync := len(unspent) == 0 + isRecovery := w.recoveryWindow > 0 + birthday := w.Manager.Birthday() + // If an initial sync is attempted, we will try and find the block stamp of the first block past our birthday. This + // will be fed into the rescan to ensure we catch transactions that are sent while performing the initial sync. + var birthdayStamp *waddrmgr.BlockStamp + // TODO(jrick): How should this handle a synced height earlier than the chain server best block? When no addresses + // have been generated for the wallet, the rescan can be skipped. + // + // TODO: This is only correct because activeData above returns all addresses ever created, including those that + // don't need to be watched anymore. This code should be updated when this assumption is no longer true, but worst + // case would result in an unnecessary rescan. + if isInitialSync || isRecovery { + // Find the latest checkpoint's height. This lets us catch up to at least that checkpoint, since we're + // synchronizing from scratch, and lets us avoid a bunch of costly DB transactions in the case when we're using + // BDB for the walletdb backend and Neutrino for the chain.Interface backend, and the chain backend starts + // synchronizing at the same time as the wallet. + var bestHeight int32 + _, bestHeight, e = chainClient.GetBestBlock() + if e != nil { + return e + } + T.Ln("bestHeight", bestHeight) + checkHeight := bestHeight + if len(w.chainParams.Checkpoints) > 0 { + checkHeight = w.chainParams.Checkpoints[len( + w.chainParams.Checkpoints, + )-1].Height + } + logHeight := checkHeight + if bestHeight > logHeight { + logHeight = bestHeight + } + I.F( + "catching up block hashes to height %d, this will take a while", + logHeight, + ) + // Initialize the first database transaction. + var tx walletdb.ReadWriteTx + tx, e = w.db.BeginReadWriteTx() + if e != nil { + return e + } + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + // Only allocate the recoveryMgr if we are actually in recovery mode. + recoveryMgr := &RecoveryManager{} + if isRecovery { + I.Ln( + "RECOVERY MODE ENABLED -- rescanning for used addresses with recovery_window =", + w.recoveryWindow, + ) + // Initialize the recovery manager with a default batch size of 2000. + I.Ln("initialising recovery manager") + recoveryMgr = NewRecoveryManager( + w.recoveryWindow, recoveryBatchSize, + w.chainParams, + ) + // In the event that this recovery is being resumed, we will need to repopulate all found addresses from the + // database. For basic recovery, we will only do so for the default scopes. + var scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager + I.Ln("getting scope managers") + scopedMgrs, e = w.defaultScopeManagers() + if e != nil { + return e + } + I.Ln("opening read bucket") + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + var credits []wtxmgr.Credit + I.Ln("getting unspent outputs") + credits, e = w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return e + } + I.Ln("resurrecting the dead") + e = recoveryMgr.Resurrect(ns, scopedMgrs, credits) + if e != nil { + return e + } + I.Ln("all deads now shambling") + } + I.Ln("startHeight", startHeight, "bestHeight", bestHeight) + for height := startHeight; height <= bestHeight; height++ { + I.Ln("current height", height) + var hash *chainhash.Hash + if hash, e = chainClient.GetBlockHash(int64(height)); E.Chk(e) { + if e = tx.Rollback(); E.Chk(e) { + } + return e + } + // // If we're using the Neutrino backend, we can check if it's current or not. For other backends we'll assume + // // it is current if the best height has reached the last checkpoint. + // isCurrent := func(bestHeight int32) bool { + // switch c := chainClient.(type) { + // case *chainclient.NeutrinoClient: + // return c.CS.IsCurrent() + // } + // return bestHeight >= checkHeight + // } + // If we've found the best height the backend knows about, and the backend is still synchronizing, we'll + // wait. We can give it a little bit of time to synchronize further before updating the best height based on + // the backend. Once we see that the backend has advanced, we can catch up to it. + for height == bestHeight { // && !isCurrent(bestHeight) { + I.Ln("getting best height from chain") + if _, bestHeight, e = chainClient.GetBestBlock(); E.Chk(e) { + if e = tx.Rollback(); E.Chk(e) { + } + return e + } else { + break + } + time.Sleep(time.Second) + } + var header *wire.BlockHeader + header, e = chainClient.GetBlockHeader(hash) + if e != nil { + return e + } + // Chk to see if this header's timestamp has surpassed our birthday or if we've surpassed one previously. + timestamp := header.Timestamp + if timestamp.After(birthday) || birthdayStamp != nil { + // If this is the first block past our birthday, record the block stamp so that we can use this as the + // starting point for the rescan. This will ensure we don't miss transactions that are sent to the + // wallet during an initial sync. + // + // NOTE: The birthday persisted by the wallet is two days before the actual wallet birthday, to deal + // with potentially inaccurate header timestamps. + if birthdayStamp == nil { + birthdayStamp = &waddrmgr.BlockStamp{ + Height: height, + Hash: *hash, + Timestamp: timestamp, + } + } + // If we are in recovery mode and the check passes, we will add this block to our list of blocks to scan + // for recovered addresses. + if isRecovery { + recoveryMgr.AddToBlockBatch( + hash, height, timestamp, + ) + } + } + e = w.Manager.SetSyncedTo( + ns, &waddrmgr.BlockStamp{ + Hash: *hash, + Height: height, + Timestamp: timestamp, + }, + ) + if e != nil { + e = tx.Rollback() + if e != nil { + } + return e + } + // If we are in recovery mode, attempt a recovery on blocks that have been added to the recovery manager's + // block batch thus far. If block batch is empty, this will be a NOP. + if isRecovery && height%recoveryBatchSize == 0 { + e = w.recoverDefaultScopes( + chainClient, tx, ns, + recoveryMgr.BlockBatch(), + recoveryMgr.State(), + ) + if e != nil { + e = tx.Rollback() + if e != nil { + } + return e + } + // Clear the batch of all processed blocks. + recoveryMgr.ResetBlockBatch() + } + // Every 10K blocks, commit and start a new database TX. + if height%10000 == 0 { + e = tx.Commit() + if e != nil { + e = tx.Rollback() + if e != nil { + } + return e + } + I.Ln( + "caught up to height", height, + ) + tx, e = w.db.BeginReadWriteTx() + if e != nil { + return e + } + ns = tx.ReadWriteBucket(waddrmgrNamespaceKey) + } + } + // Perform one last recovery attempt for all blocks that were not batched at the default granularity of 2000 + // blocks. + if isRecovery { + I.Ln("isRecovery") + e = w.recoverDefaultScopes( + chainClient, tx, ns, recoveryMgr.BlockBatch(), + recoveryMgr.State(), + ) + if e != nil { + e = tx.Rollback() + if e != nil { + } + return e + } + } + // Commit (or roll back) the final database transaction. + if e = tx.Commit(); E.Chk(e) { + if e = tx.Rollback(); E.Chk(e) { + } + return e + } + I.Ln("done catching up block hashes") + // Since we've spent some time catching up block hashes, we might have new addresses waiting for us that were + // requested during initial sync. Make sure we have those before we request a rescan later on. + e = walletdb.View( + w.db, func(dbtx walletdb.ReadTx) (e error) { + addrs, unspent, e = w.activeData(dbtx) + return e + }, + ) + if e != nil { + return e + } + } + // Compare previously-seen blocks against the chain server. If any of these blocks no longer exist, rollback all of + // the missing blocks before catching up with the rescan. + rollback := false + rollbackStamp := w.Manager.SyncedTo() + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey) + for height := rollbackStamp.Height; true; height-- { + hash, e := w.Manager.BlockHash(addrmgrNs, height) + if e != nil { + return e + } + chainHash, e := chainClient.GetBlockHash(int64(height)) + if e != nil { + return e + } + header, e := chainClient.GetBlockHeader(chainHash) + if e != nil { + return e + } + rollbackStamp.Hash = *chainHash + rollbackStamp.Height = height + rollbackStamp.Timestamp = header.Timestamp + if bytes.Equal(hash[:], chainHash[:]) { + break + } + rollback = true + } + if rollback { + e := w.Manager.SetSyncedTo(addrmgrNs, &rollbackStamp) + if e != nil { + return e + } + // Rollback unconfirms transactions at and beyond the passed height, so add one to the new synced-to height + // to prevent unconfirming txs from the synced-to block. + e = w.TxStore.Rollback(txmgrNs, rollbackStamp.Height+1) + if e != nil { + return e + } + } + return nil + }, + ) + if e != nil { + return e + } + // If a birthday stamp was found during the initial sync and the rollback causes us to revert it, update the + // birthday stamp so that it points at the new tip. + if birthdayStamp != nil && rollbackStamp.Height <= birthdayStamp.Height { + birthdayStamp = &rollbackStamp + } + // Request notifications for connected and disconnected blocks. + // + // TODO(jrick): Either request this notification only once, or when rpcclient is modified to allow some notification + // request to not automatically resent on reconnect, include the notifyblocks request as well. I am leaning towards + // allowing off all rpcclient notification re-registrations, in which case the code here should be left as is. + e = chainClient.NotifyBlocks() + if e != nil { + return e + } + return w.rescanWithTarget(addrs, unspent, birthdayStamp) +} + +// defaultScopeManagers fetches the ScopedKeyManagers from the wallet using the default set of key scopes. +func (w *Wallet) defaultScopeManagers() ( + map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, error, +) { + scopedMgrs := make(map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager) + for _, scope := range waddrmgr.DefaultKeyScopes { + scopedMgr, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, e + } + scopedMgrs[scope] = scopedMgr + } + return scopedMgrs, nil +} + +// recoverDefaultScopes attempts to recover any addresses belonging to any active scoped key managers known to the +// wallet. Recovery of each scope's default account will be done iteratively against the same batch of blocks. +// +// TODO(conner): parallelize/pipeline/cache intermediate network requests +func (w *Wallet) recoverDefaultScopes( + chainClient chainclient.Interface, + tx walletdb.ReadWriteTx, + ns walletdb.ReadWriteBucket, + batch []wtxmgr.BlockMeta, + recoveryState *RecoveryState, +) (e error) { + scopedMgrs, e := w.defaultScopeManagers() + if e != nil { + return e + } + return w.recoverScopedAddresses( + chainClient, tx, ns, batch, recoveryState, scopedMgrs, + ) +} + +// recoverAccountAddresses scans a range of blocks in attempts to recover any previously used addresses for a particular +// account derivation path. At a high level, the algorithm works as follows: +// +// 1) Ensure internal and external branch horizons are fully expanded. +// +// 2) Filter the entire range of blocks, stopping if a non-zero number of address are contained in a particular block. +// +// 3) Record all internal and external addresses found in the block. +// +// 4) Record any outpoints found in the block that should be watched for spends +// +// 5) Trim the range of blocks up to and including the one reporting the addrs. +// +// 6) Repeat from (1) if there are still more blocks in the range. +func (w *Wallet) recoverScopedAddresses( + chainClient chainclient.Interface, + tx walletdb.ReadWriteTx, + ns walletdb.ReadWriteBucket, + batch []wtxmgr.BlockMeta, + recoveryState *RecoveryState, + scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, +) (e error) { + // If there are no blocks in the batch, we are done. + if len(batch) == 0 { + return nil + } + I.F( + "scanning %d blocks for recoverable addresses", + len(batch), + ) +expandHorizons: + for scope, scopedMgr := range scopedMgrs { + scopeState := recoveryState.StateForScope(scope) + e = expandScopeHorizons(ns, scopedMgr, scopeState) + if e != nil { + return e + } + } + // With the internal and external horizons properly expanded, we now construct the filter blocks request. The + // request includes the range of blocks we intend to scan, in addition to the scope-index -> addr map for all + // internal and external branches. + filterReq := newFilterBlocksRequest(batch, scopedMgrs, recoveryState) + // Initiate the filter blocks request using our chain backend. If an error occurs, we are unable to proceed with the + // recovery. + filterResp, e := chainClient.FilterBlocks(filterReq) + if e != nil { + return e + } + // If the filter response is empty, this signals that the rest of the batch was completed, and no other addresses + // were discovered. As a result, no further modifications to our recovery state are required and we can proceed to + // the next batch. + if filterResp == nil { + return nil + } + // Otherwise, retrieve the block info for the block that detected a non-zero number of address matches. + block := batch[filterResp.BatchIndex] + // Log any non-trivial findings of addresses or outpoints. + logFilterBlocksResp(block, filterResp) + // Report any external or internal addresses found as a result of the appropriate branch recovery state. Adding + // indexes above the last-found index of either will result in the horizons being expanded upon the next iteration. + // Any found addresses are also marked used using the scoped key manager. + e = extendFoundAddresses(ns, filterResp, scopedMgrs, recoveryState) + if e != nil { + return e + } + // Update the global set of watched outpoints with any that were found in the block. + for outPoint, addr := range filterResp.FoundOutPoints { + recoveryState.AddWatchedOutPoint(&outPoint, addr) + } + // Finally, record all of the relevant transactions that were returned in the filter blocks response. This ensures + // that these transactions and their outputs are tracked when the final rescan is performed. + for _, txn := range filterResp.RelevantTxns { + txRecord, e := wtxmgr.NewTxRecordFromMsgTx( + txn, filterResp.BlockMeta.Time, + ) + if e != nil { + return e + } + e = w.addRelevantTx(tx, txRecord, &filterResp.BlockMeta) + if e != nil { + return e + } + } + // Update the batch to indicate that we've processed all block through the one that returned found addresses. + batch = batch[filterResp.BatchIndex+1:] + // If this was not the last block in the batch, we will repeat the filtering process again after expanding our + // horizons. + if len(batch) > 0 { + goto expandHorizons + } + return nil +} + +// expandScopeHorizons ensures that the ScopeRecoveryState has an adequately sized look ahead for both its internal and +// external branches. The keys derived here are added to the scope's recovery state, but do not affect the persistent +// state of the wallet. If any invalid child keys are detected, the horizon will be properly extended such that our +// lookahead always includes the proper number of valid child keys. +func expandScopeHorizons( + ns walletdb.ReadWriteBucket, + scopedMgr *waddrmgr.ScopedKeyManager, + scopeState *ScopeRecoveryState, +) (e error) { + // Compute the current external horizon and the number of addresses we must derive to ensure we maintain a + // sufficient recovery window for the external branch. + exHorizon, exWindow := scopeState.ExternalBranch.ExtendHorizon() + count, childIndex := uint32(0), exHorizon + for count < exWindow { + keyPath := externalKeyPath(childIndex) + var ep error + var addr waddrmgr.ManagedAddress + addr, ep = scopedMgr.DeriveFromKeyPath(ns, keyPath) + switch { + case ep == hdkeychain.ErrInvalidChild: + // Record the existence of an invalid child with the external branch's recovery state. This also increments + // the branch's horizon so that it accounts for this skipped child index. + scopeState.ExternalBranch.MarkInvalidChild(childIndex) + childIndex++ + continue + case ep != nil: + return ep + } + // Register the newly generated external address and child index with the external branch recovery state. + scopeState.ExternalBranch.AddAddr(childIndex, addr.Address()) + childIndex++ + count++ + } + // Compute the current internal horizon and the number of addresses we must derive to ensure we maintain a + // sufficient recovery window for the internal branch. + inHorizon, inWindow := scopeState.InternalBranch.ExtendHorizon() + count, childIndex = 0, inHorizon + for count < inWindow { + keyPath := internalKeyPath(childIndex) + addr, e := scopedMgr.DeriveFromKeyPath(ns, keyPath) + switch { + case e == hdkeychain.ErrInvalidChild: + // Record the existence of an invalid child with the internal branch's recovery state. This also increments + // the branch's horizon so that it accounts for this skipped child index. + scopeState.InternalBranch.MarkInvalidChild(childIndex) + childIndex++ + continue + case e != nil: + return e + } + // Register the newly generated internal address and child index with the internal branch recovery state. + scopeState.InternalBranch.AddAddr(childIndex, addr.Address()) + childIndex++ + count++ + } + return nil +} + +// externalKeyPath returns the relative external derivation path /0/0/index. +func externalKeyPath(index uint32) waddrmgr.DerivationPath { + return waddrmgr.DerivationPath{ + Account: waddrmgr.DefaultAccountNum, + Branch: waddrmgr.ExternalBranch, + Index: index, + } +} + +// internalKeyPath returns the relative internal derivation path /0/1/index. +func internalKeyPath(index uint32) waddrmgr.DerivationPath { + return waddrmgr.DerivationPath{ + Account: waddrmgr.DefaultAccountNum, + Branch: waddrmgr.InternalBranch, + Index: index, + } +} + +// newFilterBlocksRequest constructs FilterBlocksRequests using our current block range, scoped managers, and recovery +// state. +func newFilterBlocksRequest( + batch []wtxmgr.BlockMeta, + scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, + recoveryState *RecoveryState, +) *chainclient.FilterBlocksRequest { + filterReq := &chainclient.FilterBlocksRequest{ + Blocks: batch, + ExternalAddrs: make(map[waddrmgr.ScopedIndex]btcaddr.Address), + InternalAddrs: make(map[waddrmgr.ScopedIndex]btcaddr.Address), + WatchedOutPoints: recoveryState.WatchedOutPoints(), + } + // Populate the external and internal addresses by merging the addresses sets belong to all currently tracked + // scopes. + for scope := range scopedMgrs { + scopeState := recoveryState.StateForScope(scope) + for index, addr := range scopeState.ExternalBranch.Addrs() { + scopedIndex := waddrmgr.ScopedIndex{ + Scope: scope, + Index: index, + } + filterReq.ExternalAddrs[scopedIndex] = addr + } + for index, addr := range scopeState.InternalBranch.Addrs() { + scopedIndex := waddrmgr.ScopedIndex{ + Scope: scope, + Index: index, + } + filterReq.InternalAddrs[scopedIndex] = addr + } + } + return filterReq +} + +// extendFoundAddresses accepts a filter blocks response that contains addresses found on chain, and advances the state +// of all relevant derivation paths to match the highest found child index for each branch. +func extendFoundAddresses( + ns walletdb.ReadWriteBucket, + filterResp *chainclient.FilterBlocksResponse, + scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager, + recoveryState *RecoveryState, +) (e error) { + // Mark all recovered external addresses as used. This will be done only for scopes that reported a non-zero number + // of external addresses in this block. + for scope, indexes := range filterResp.FoundExternalAddrs { + // First, report all external child indexes found for this scope. This ensures that the external last-found + // index will be updated to include the maximum child index seen thus far. + scopeState := recoveryState.StateForScope(scope) + for index := range indexes { + scopeState.ExternalBranch.ReportFound(index) + } + scopedMgr := scopedMgrs[scope] + // Now, with all found addresses reported, derive and extend all external addresses up to and including the + // current last found index for this scope. + exNextUnfound := scopeState.ExternalBranch.NextUnfound() + exLastFound := exNextUnfound + if exLastFound > 0 { + exLastFound-- + } + e := scopedMgr.ExtendExternalAddresses( + ns, waddrmgr.DefaultAccountNum, exLastFound, + ) + if e != nil { + return e + } + // Finally, with the scope's addresses extended, we mark used the external addresses that were found in the + // block and belong to this scope. + for index := range indexes { + addr := scopeState.ExternalBranch.GetAddr(index) + e := scopedMgr.MarkUsed(ns, addr) + if e != nil { + return e + } + } + } + // Mark all recovered internal addresses as used. This will be done only for scopes that reported a non-zero number + // of internal addresses in this block. + for scope, indexes := range filterResp.FoundInternalAddrs { + // First, report all internal child indexes found for this scope. This ensures that the internal last-found + // index will be updated to include the maximum child index seen thus far. + scopeState := recoveryState.StateForScope(scope) + for index := range indexes { + scopeState.InternalBranch.ReportFound(index) + } + scopedMgr := scopedMgrs[scope] + // Now, with all found addresses reported, derive and extend all internal addresses up to and including the + // current last found index for this scope. + inNextUnfound := scopeState.InternalBranch.NextUnfound() + inLastFound := inNextUnfound + if inLastFound > 0 { + inLastFound-- + } + e := scopedMgr.ExtendInternalAddresses( + ns, waddrmgr.DefaultAccountNum, inLastFound, + ) + if e != nil { + return e + } + // Finally, with the scope's addresses extended, we mark used the internal addresses that were found in the + // block and belong to this scope. + for index := range indexes { + addr := scopeState.InternalBranch.GetAddr(index) + e := scopedMgr.MarkUsed(ns, addr) + if e != nil { + return e + } + } + } + return nil +} + +// logFilterBlocksResp provides useful logging information when filtering succeeded in finding relevant transactions. +func logFilterBlocksResp( + block wtxmgr.BlockMeta, + resp *chainclient.FilterBlocksResponse, +) { + // Log the number of external addresses found in this block. + var nFoundExternal int + for _, indexes := range resp.FoundExternalAddrs { + nFoundExternal += len(indexes) + } + if nFoundExternal > 0 { + T.F( + "recovered %d external addrs at height=%d hash=%v", + nFoundExternal, block.Height, block.Hash, + ) + } + // Log the number of internal addresses found in this block. + var nFoundInternal int + for _, indexes := range resp.FoundInternalAddrs { + nFoundInternal += len(indexes) + } + if nFoundInternal > 0 { + T.F( + "recovered %d internal addrs at height=%d hash=%v", + nFoundInternal, block.Height, block.Hash, + ) + } + // Log the number of outpoints found in this block. + nFoundOutPoints := len(resp.FoundOutPoints) + if nFoundOutPoints > 0 { + T.F( + "found %d spends from watched outpoints at height=%d hash=%v", + nFoundOutPoints, block.Height, block.Hash, + ) + } +} + +type ( + createTxRequest struct { + account uint32 + outputs []*wire.TxOut + minconf int32 + feeSatPerKB amt.Amount + resp chan createTxResponse + } + createTxResponse struct { + tx *txauthor.AuthoredTx + e error + } +) + +// txCreator is responsible for the input selection and creation of transactions. These functions are the responsibility +// of this method (designed to be run as its own goroutine) since input selection must be serialized, or else it is +// possible to create double spends by choosing the same inputs for multiple transactions. Along with input selection, +// this method is also responsible for the signing of transactions, since we don't want to end up in a situation where +// we run out of inputs as multiple transactions are being created. In this situation, it would then be possible for +// both requests, rather than just one, to fail due to not enough available inputs. +func (w *Wallet) txCreator() { + quit := w.quitChan() +out: + for { + select { + case txr := <-w.createTxRequests: + var e error + var h heldUnlock + h, e = w.holdUnlock() + if e != nil { + txr.resp <- createTxResponse{nil, e} + continue + } + var tx *txauthor.AuthoredTx + tx, e = w.txToOutputs( + txr.outputs, txr.account, + txr.minconf, txr.feeSatPerKB, + ) + h.release() + txr.resp <- createTxResponse{tx, e} + case <-quit.Wait(): + break out + } + } + w.wg.Done() +} + +// CreateSimpleTx creates a new signed transaction spending unspent P2PKH outputs with at least minconf confirmations +// spending to any number of address/amount pairs. Change and an appropriate transaction fee are automatically included, +// if necessary. All transaction creation through this function is serialized to prevent the creation of many +// transactions which spend the same outputs. +func (w *Wallet) CreateSimpleTx( + account uint32, outputs []*wire.TxOut, + minconf int32, satPerKb amt.Amount, +) (*txauthor.AuthoredTx, error) { + req := createTxRequest{ + account: account, + outputs: outputs, + minconf: minconf, + feeSatPerKB: satPerKb, + resp: make(chan createTxResponse), + } + w.createTxRequests <- req + resp := <-req.resp + return resp.tx, resp.e +} + +type ( + unlockRequest struct { + passphrase []byte + lockAfter <-chan time.Time // nil prevents the timeout. + err chan error + } + changePassphraseRequest struct { + old, new []byte + private bool + err chan error + } + changePassphrasesRequest struct { + publicOld, publicNew []byte + privateOld, privateNew []byte + err chan error + } + // heldUnlock is a tool to prevent the wallet from automatically locking after some timeout before an operation + // which needed the unlocked wallet has finished. Any acquired heldUnlock *must* be released (preferably with a + // defer) or the wallet will forever remain unlocked. + heldUnlock qu.C +) + +// walletLocker manages the locked/unlocked state of a wallet. +func (w *Wallet) walletLocker() { + var timeout <-chan time.Time + holdChan := make(heldUnlock) + quit := w.quitChan() + // this flips to false once the first unlock has been done, for runasservice opt which shuts down on lock + // first := true + var e error +out: + for { + select { + case req := <-w.unlockRequests: + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.Unlock(addrmgrNs, req.passphrase) + }, + ) + if e != nil { + req.err <- e + continue + } + timeout = req.lockAfter + if timeout == nil { + I.Ln("the wallet has been unlocked without a time limit") + } else { + I.Ln("the wallet has been temporarily unlocked") + } + req.err <- nil + continue + case req := <-w.changePassphrase: + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.Manager.ChangePassphrase( + addrmgrNs, req.old, req.new, req.private, + &waddrmgr.DefaultScryptOptions, + ) + }, + ) + req.err <- e + continue + case req := <-w.changePassphrases: + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + e = w.Manager.ChangePassphrase( + addrmgrNs, req.publicOld, req.publicNew, + false, &waddrmgr.DefaultScryptOptions, + ) + if e != nil { + return e + } + return w.Manager.ChangePassphrase( + addrmgrNs, req.privateOld, req.privateNew, + true, &waddrmgr.DefaultScryptOptions, + ) + }, + ) + req.err <- e + continue + case req := <-w.holdUnlockRequests: + if w.Manager.IsLocked() { + close(req) + continue + } + req <- holdChan + <-holdChan // Block until the lock is released. + // If, after holding onto the unlocked wallet for some time, the timeout has expired, lock it now instead of + // hoping it gets unlocked next time the top level select runs. + select { + case <-timeout: + // Let the top level select fallthrough so the wallet is locked. + default: + continue + } + case w.lockState <- w.Manager.IsLocked(): + continue + case <-quit.Wait(): + break out + case <-w.lockRequests.Wait(): + // first = false + case <-timeout: + // first = false + } + // Select statement fell through by an explicit lock or the timer expiring. Lock the manager here. + timeout = nil + e = w.Manager.Lock() + if e != nil && !waddrmgr.IsError(e, waddrmgr.ErrLocked) { + E.Ln("could not lock wallet:", e) + } else { + I.Ln("the wallet has been locked") + } + // if *w.PodConfig.RunAsService && !first { + // // if we are running as a service this means shut down on lock as unlocking happens only at startup + // break out + // } + } + w.wg.Done() +} + +// Unlock unlocks the wallet's address manager and relocks it after timeout has expired. If the wallet is already +// unlocked and the new passphrase is correct, the current timeout is replaced with the new one. The wallet will be +// locked if the passphrase is incorrect or any other error occurs during the unlock. +func (w *Wallet) Unlock(passphrase []byte, lock <-chan time.Time) (e error) { + eC := make(chan error, 1) + w.unlockRequests <- unlockRequest{ + passphrase: passphrase, + lockAfter: lock, + err: eC, + } + return <-eC +} + +// Lock locks the wallet's address manager. +func (w *Wallet) Lock() { + w.lockRequests <- struct{}{} +} + +// Locked returns whether the account manager for a wallet is locked. +func (w *Wallet) Locked() bool { + return <-w.lockState +} + +// holdUnlock prevents the wallet from being locked. The heldUnlock object *must* be released, or the wallet will +// forever remain unlocked. +// +// TODO: To prevent the above scenario, perhaps closures should be passed to the walletLocker goroutine and disallow +// callers from explicitly handling the locking mechanism. +func (w *Wallet) holdUnlock() (heldUnlock, error) { + req := make(chan heldUnlock) + w.holdUnlockRequests <- req + hl, ok := <-req + if !ok { + // TODO(davec): This should be defined and exported from waddrmgr. + return nil, waddrmgr.ManagerError{ + ErrorCode: waddrmgr.ErrLocked, + Description: "address manager is locked", + } + } + return hl, nil +} + +// release releases the hold on the unlocked-state of the wallet and allows the wallet to be locked again. If a lock +// timeout has already expired, the wallet is locked again as soon as release is called. +func (c heldUnlock) release() { + c <- struct{}{} +} + +// ChangePrivatePassphrase attempts to change the passphrase for a wallet from old to new. Changing the passphrase is +// synchronized with all other address manager locking and unlocking. The lock state will be the same as it was before +// the password change. +func (w *Wallet) ChangePrivatePassphrase(old, new []byte) (e error) { + errChan := make(chan error, 1) + w.changePassphrase <- changePassphraseRequest{ + old: old, + new: new, + private: true, + err: errChan, + } + return <-errChan +} + +// ChangePublicPassphrase modifies the public passphrase of the wallet. +func (w *Wallet) ChangePublicPassphrase(old, new []byte) (e error) { + errChan := make(chan error, 1) + w.changePassphrase <- changePassphraseRequest{ + old: old, + new: new, + private: false, + err: errChan, + } + return <-errChan +} + +// ChangePassphrases modifies the public and private passphrase of the wallet atomically. +func (w *Wallet) ChangePassphrases( + publicOld, publicNew, privateOld, + privateNew []byte, +) (e error) { + errChan := make(chan error, 1) + w.changePassphrases <- changePassphrasesRequest{ + publicOld: publicOld, + publicNew: publicNew, + privateOld: privateOld, + privateNew: privateNew, + err: errChan, + } + return <-errChan +} + +// // accountUsed returns whether there are any recorded transactions spending to +// // a given account. It returns true if atleast one address in the account was +// // used and false if no address in the account was used. +// func (w *Wallet) accountUsed(addrmgrNs walletdb.ReadWriteBucket, account uint32) (bool, error) { +// var used bool +// e := w.Manager.ForEachAccountAddress(addrmgrNs, account, +// func(maddr waddrmgr.ManagedAddress) (e error) { +// used = maddr.Used(addrmgrNs) +// if used { +// return waddrmgr.Break +// } +// return nil +// }) +// if e == waddrmgr.Break { +// e = nil +// } +// return used, err +// } + +// AccountAddresses returns the addresses for every created address for an +// account. +func (w *Wallet) AccountAddresses(account uint32) ( + addrs []btcaddr.Address, e error, +) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.ForEachAccountAddress( + addrmgrNs, account, func(maddr waddrmgr.ManagedAddress) (e error) { + addrs = append(addrs, maddr.Address()) + return nil + }, + ) + }, + ) + return +} + +// CalculateBalance sums the amounts of all unspent transaction outputs to addresses of a wallet and returns the +// balance. +// +// If confirmations is 0, all UTXOs, even those not present in a block (height -1), will be used to get the balance. +// Otherwise, a UTXO must be in a block. If confirmations is 1 or greater, the balance will be calculated based on how +// many how many blocks include a UTXO. +func (w *Wallet) CalculateBalance(confirms int32) ( + balance amt.Amount, e error, +) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + blk := w.Manager.SyncedTo() + balance, e = w.TxStore.Balance(txmgrNs, confirms, blk.Height) + return e + }, + ) + return balance, e +} + +// Balances records total, spendable (by policy), and immature coinbase reward balance amounts. +type Balances struct { + Total amt.Amount + Spendable amt.Amount + ImmatureReward amt.Amount +} + +// CalculateAccountBalances sums the amounts of all unspent transaction outputs to the given account of a wallet and +// returns the balance. +// +// This function is much slower than it needs to be since transactions outputs are not indexed by the accounts they +// credit to, and all unspent transaction outputs must be iterated. +func (w *Wallet) CalculateAccountBalances( + account uint32, confirms int32, +) (bals Balances, e error) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + // Get current block. The block height used for calculating + // the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + var unspent []wtxmgr.Credit + unspent, e = w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return e + } + for i := range unspent { + output := &unspent[i] + var outputAcct uint32 + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams, + ) + if e == nil && len(addrs) > 0 { + _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if e != nil || outputAcct != account { + continue + } + bals.Total += output.Amount + if output.FromCoinBase && !confirmed( + int32(w.chainParams.CoinbaseMaturity), + output.Height, syncBlock.Height, + ) { + bals.ImmatureReward += output.Amount + } else if confirmed(confirms, output.Height, syncBlock.Height) { + bals.Spendable += output.Amount + } + } + return nil + }, + ) + return bals, e +} + +// CurrentAddress gets the most recently requested Bitcoin payment address from a wallet for a particular key-chain +// scope. If the address has already been used (there is at least one transaction spending to it in the blockchain or +// pod mempool), the next chained address is returned. +func (w *Wallet) CurrentAddress( + account uint32, scope waddrmgr.KeyScope, +) (btcaddr.Address, error) { + chainClient, e := w.requireChainClient() + if e != nil { + return nil, e + } + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, e + } + var ( + addr btcaddr.Address + props *waddrmgr.AccountProperties + ) + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + maddr, e := manager.LastExternalAddress(addrmgrNs, account) + if e != nil { + // If no address exists yet, create the first external address. + if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { + addr, props, e = w.newAddress( + addrmgrNs, account, scope, + ) + } + return e + } + // Get next chained address if the last one has already been used. + if maddr.Used(addrmgrNs) { + addr, props, e = w.newAddress( + addrmgrNs, account, scope, + ) + return e + } + addr = maddr.Address() + return nil + }, + ) + if e != nil { + return nil, e + } + // If the props have been initially, then we had to create a new address to satisfy the query. Notify the rpc server + // about the new address. + if props != nil { + e = chainClient.NotifyReceived([]btcaddr.Address{addr}) + if e != nil { + return nil, e + } + w.NtfnServer.notifyAccountProperties(props) + } + return addr, nil +} + +// PubKeyForAddress looks up the associated public key for a P2PKH address. +func (w *Wallet) PubKeyForAddress(a btcaddr.Address) ( + pubKey *ec.PublicKey, e error, +) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, e := w.Manager.Address(addrmgrNs, a) + if e != nil { + return e + } + managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return errors.New("address does not have an associated public key") + } + pubKey = managedPubKeyAddr.PubKey() + return nil + }, + ) + return +} + +// PrivKeyForAddress looks up the associated private key for a P2PKH or P2PK address. +func (w *Wallet) PrivKeyForAddress(a btcaddr.Address) ( + privKey *ec.PrivateKey, e error, +) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, e := w.Manager.Address(addrmgrNs, a) + if e != nil { + return e + } + managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return errors.New("address does not have an associated private key") + } + privKey, e = managedPubKeyAddr.PrivKey() + return e + }, + ) + return privKey, e +} + +// HaveAddress returns whether the wallet is the owner of the address a. +func (w *Wallet) HaveAddress(a btcaddr.Address) (b bool, e error) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + _, e = w.Manager.Address(addrmgrNs, a) + return e + }, + ) + if e == nil { + return true, nil + } + if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { + return false, nil + } + return false, e +} + +// AccountOfAddress finds the account that an address is associated with. +func (w *Wallet) AccountOfAddress(a btcaddr.Address) (account uint32, e error) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + _, account, e = w.Manager.AddrAccount(addrmgrNs, a) + return e + }, + ) + return account, e +} + +// AddressInfo returns detailed information regarding a wallet address. +func (w *Wallet) AddressInfo(a btcaddr.Address) ( + waddrmgr.ManagedAddress, error, +) { + var managedAddress waddrmgr.ManagedAddress + e := walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddress, e = w.Manager.Address(addrmgrNs, a) + return e + }, + ) + return managedAddress, e +} + +// AccountNumber returns the account number for an account name under a particular key scope. +func (w *Wallet) AccountNumber( + scope waddrmgr.KeyScope, accountName string, +) (account uint32, e error) { + var manager *waddrmgr.ScopedKeyManager + manager, e = w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return 0, e + } + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + account, e = manager.LookupAccount(addrmgrNs, accountName) + return e + }, + ) + return account, e +} + +// AccountName returns the name of an account. +func (w *Wallet) AccountName( + scope waddrmgr.KeyScope, accountNumber uint32, +) (string, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return "", e + } + var accountName string + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + accountName, e = manager.AccountName(addrmgrNs, accountNumber) + return e + }, + ) + return accountName, e +} + +// AccountProperties returns the properties of an account, including address indexes and name. It first fetches the +// desynced information from the address manager, then updates the indexes based on the address pools. +func (w *Wallet) AccountProperties( + scope waddrmgr.KeyScope, acct uint32, +) (*waddrmgr.AccountProperties, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, e + } + var props *waddrmgr.AccountProperties + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + props, e = manager.AccountProperties(waddrmgrNs, acct) + return e + }, + ) + return props, e +} + +// RenameAccount sets the name for an account number to newName. +func (w *Wallet) RenameAccount( + scope waddrmgr.KeyScope, account uint32, newName string, +) (e error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return e + } + var props *waddrmgr.AccountProperties + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + e = manager.RenameAccount(addrmgrNs, account, newName) + if e != nil { + return e + } + props, e = manager.AccountProperties(addrmgrNs, account) + return e + }, + ) + if e == nil { + w.NtfnServer.notifyAccountProperties(props) + } + return e +} + +// const maxEmptyAccounts = 100 + +// NextAccount creates the next account and returns its account number. The name must be unique to the account. In order +// to support automatic seed restoring, new accounts may not be created when all of the previous 100 accounts have no +// transaction history (this is a deviation from the BIP0044 spec, which allows no unused account gaps). +func (w *Wallet) NextAccount(scope waddrmgr.KeyScope, name string) ( + uint32, error, +) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return 0, e + } + var ( + account uint32 + props *waddrmgr.AccountProperties + ) + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + account, e = manager.NewAccount(addrmgrNs, name) + if e != nil { + return e + } + props, e = manager.AccountProperties(addrmgrNs, account) + return e + }, + ) + if e != nil { + E.Ln( + "cannot fetch new account properties for notification after"+ + " account creation:", e, + ) + } + w.NtfnServer.notifyAccountProperties(props) + return account, e +} + +// CreditCategory describes the type of wallet transaction output. The category of "sent transactions" (debits) is +// always "send", and is not expressed by this type. +// +// TODO: This is a requirement of the RPC server and should be moved. +type CreditCategory byte + +// These constants define the possible credit categories. +const ( + CreditReceive CreditCategory = iota + CreditGenerate + CreditImmature +) + +// String returns the category as a string. This string may be used as the JSON string for categories as part of +// listtransactions and gettransaction RPC responses. +func (c CreditCategory) String() string { + switch c { + case CreditReceive: + return "receive" + case CreditGenerate: + return "generate" + case CreditImmature: + return "immature" + default: + return "unknown" + } +} + +// RecvCategory returns the category of received credit outputs from a transaction record. The passed block chain height +// is used to distinguish immature from mature coinbase outputs. +// +// TODO: This is intended for use by the RPC server and should be moved out of this package at a later time. +func RecvCategory( + details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Params, +) CreditCategory { + if blockchain.IsCoinBaseTx(&details.MsgTx) { + if confirmed( + int32(net.CoinbaseMaturity), details.Block.Height, + syncHeight, + ) { + return CreditGenerate + } + return CreditImmature + } + return CreditReceive +} + +// listTransactions creates a object that may be marshalled to a response result for a listtransactions RPC. +// +// TODO: This should be moved to the legacyrpc package. +func listTransactions( + tx walletdb.ReadTx, details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager, + syncHeight int32, net *chaincfg.Params, +) []btcjson.ListTransactionsResult { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + var ( + blockHashStr string + blockTime int64 + confirmations int64 + ) + if details.Block.Height != -1 { + blockHashStr = details.Block.Hash.String() + blockTime = details.Block.Time.Unix() + confirmations = int64(confirms(details.Block.Height, syncHeight)) + } + results := []btcjson.ListTransactionsResult{} + txHashStr := details.Hash.String() + received := details.Received.Unix() + generated := blockchain.IsCoinBaseTx(&details.MsgTx) + recvCat := RecvCategory(details, syncHeight, net).String() + send := len(details.Debits) != 0 + // Fee can only be determined if every input is a debit. + var feeF64 float64 + if len(details.Debits) == len(details.MsgTx.TxIn) { + var debitTotal amt.Amount + for _, deb := range details.Debits { + debitTotal += deb.Amount + } + var outputTotal amt.Amount + for _, output := range details.MsgTx.TxOut { + outputTotal += amt.Amount(output.Value) + } + // Note: The actual fee is debitTotal - outputTotal. However, this RPC reports negative numbers for fees, so the + // inverse is calculated. + feeF64 = (outputTotal - debitTotal).ToDUO() + } +outputs: + for i, output := range details.MsgTx.TxOut { + // Determine if this output is a credit, and if so, determine its spentness. + var isCredit bool + var spentCredit bool + for _, cred := range details.Credits { + if cred.Index == uint32(i) { + // Change outputs are ignored. + if cred.Change { + continue outputs + } + isCredit = true + spentCredit = cred.Spent + break + } + } + var address string + var accountName string + _, addrs, _, _ := txscript.ExtractPkScriptAddrs(output.PkScript, net) + if len(addrs) == 1 { + addr := addrs[0] + address = addr.EncodeAddress() + mgr, account, e := addrMgr.AddrAccount(addrmgrNs, addrs[0]) + if e == nil { + accountName, e = mgr.AccountName(addrmgrNs, account) + if e != nil { + accountName = "" + } + } + } + amountF64 := amt.Amount(output.Value).ToDUO() + blockIndex := int64(details.Block.Height) + result := btcjson.ListTransactionsResult{ + // Fields left zeroed: + // InvolvesWatchOnly + // BlockIndex + // + // Fields set below: + // Account (only for non-"send" categories) + // Category + // Amount + // Fee + Address: address, + Vout: uint32(i), + Confirmations: confirmations, + Generated: generated, + BlockHash: blockHashStr, + BlockIndex: blockIndex, + BlockTime: blockTime, + TxID: txHashStr, + WalletConflicts: []string{}, + Time: received, + TimeReceived: received, + } + // Add a received/generated/immature result if this is a credit. If the output was spent, create a second result + // under the send category with the inverse of the output amount. It is therefore possible that a single output + // may be included in the results set zero, one, or two times. + // + // Since credits are not saved for outputs that are not controlled by this wallet, all non-credits from + // transactions with debits are grouped under the send category. + if send || spentCredit { + result.Category = "send" + result.Amount = -amountF64 + result.Fee = feeF64 + results = append(results, result) + } + if isCredit { + result.Account = accountName + result.Category = recvCat + result.Amount = amountF64 + result.Fee = 0 + results = append(results, result) + } + } + return results +} + +// ListSinceBlock returns a slice of objects with details about transactions since the given block. If the block is -1 +// then all transactions are included. This is intended to be used for listsinceblock RPC replies. +func (w *Wallet) ListSinceBlock(start, end, syncHeight int32) ( + txList []btcjson.ListTransactionsResult, e error, +) { + txList = []btcjson.ListTransactionsResult{} + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for _, detail := range details { + jsonResults := listTransactions( + tx, &detail, + w.Manager, syncHeight, w.chainParams, + ) + txList = append(txList, jsonResults...) + } + return false, nil + } + return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) + }, + ) + return +} + +// ListTransactions returns a slice of objects with details about a recorded +// transaction. This is intended to be used for listtransactions RPC replies. +func (w *Wallet) ListTransactions(from, count int) ( + txList []btcjson.ListTransactionsResult, e error, +) { + // txList := []btcjson.ListTransactionsResult{} + // T.Ln("ListTransactions", from, count) + if e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + // Get current block. The block height used for calculating the number of tx + // confirmations. + syncBlock := w.Manager.SyncedTo() + D.Ln("synced to", syncBlock) + // Need to skip the first from transactions, and after those, only include the + // next count transactions. + skipped := 0 + n := 0 + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. This does nothing + // for unmined transactions, which are unsorted, but it will process mined + // transactions in the reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + if from > skipped { + skipped++ + continue + } + n++ + if n > count { + return true, nil + } + jsonResults := listTransactions( + tx, &details[i], + w.Manager, syncBlock.Height, w.chainParams, + ) + txList = append(txList, jsonResults...) + if len(jsonResults) > 0 { + n++ + } + } + return false, nil + } + // Return newer results first by starting at mempool height and working down to + // the genesis block. + return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) + }, + ); E.Chk(e) { + } + return +} + +// ListAddressTransactions returns a slice of objects with details about recorded transactions to or from any address +// belonging to a set. This is intended to be used for listaddresstransactions RPC replies. +func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( + txList []btcjson.ListTransactionsResult, + e error, +) { + txList = []btcjson.ListTransactionsResult{} + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + // Get current block. The block height used for calculating the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + loopDetails: + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + pkScript, w.chainParams, + ) + if e != nil || len(addrs) != 1 { + continue + } + apkh, ok := addrs[0].(*btcaddr.PubKeyHash) + if !ok { + continue + } + _, ok = pkHashes[string(apkh.ScriptAddress())] + if !ok { + continue + } + jsonResults := listTransactions( + tx, detail, + w.Manager, syncBlock.Height, w.chainParams, + ) + // if e != nil { + // return false, err + // } + txList = append(txList, jsonResults...) + continue loopDetails + } + } + return false, nil + } + return w.TxStore.RangeTransactions(txmgrNs, 0, -1, rangeFn) + }, + ) + return txList, e +} + +// ListAllTransactions returns a slice of objects with details about a recorded transaction. This is intended to be used +// for listalltransactions RPC replies. +func (w *Wallet) ListAllTransactions() ( + txList []btcjson.ListTransactionsResult, e error, +) { + txList = []btcjson.ListTransactionsResult{} + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + // Get current block. The block height used for calculating the number of tx confirmations. + syncBlock := w.Manager.SyncedTo() + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // Iterate over transactions at this height in reverse order. This does nothing for unmined transactions, + // which are unsorted, but it will process mined transactions in the reverse order they were marked mined. + for i := len(details) - 1; i >= 0; i-- { + jsonResults := listTransactions( + tx, &details[i], w.Manager, + syncBlock.Height, w.chainParams, + ) + txList = append(txList, jsonResults...) + } + return false, nil + } + // Return newer results first by starting at mempool height and working down to the genesis block. + return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn) + }, + ) + return txList, e +} + +// BlockIdentifier identifies a block by either a height or a hash. +type BlockIdentifier struct { + height int32 + hash *chainhash.Hash +} + +// NewBlockIdentifierFromHeight constructs a BlockIdentifier for a block height. +func NewBlockIdentifierFromHeight(height int32) *BlockIdentifier { + return &BlockIdentifier{height: height} +} + +// NewBlockIdentifierFromHash constructs a BlockIdentifier for a block hash. +func NewBlockIdentifierFromHash(hash *chainhash.Hash) *BlockIdentifier { + return &BlockIdentifier{hash: hash} +} + +// GetTransactionsResult is the result of the wallet's GetTransactions method. See GetTransactions for more details. +type GetTransactionsResult struct { + MinedTransactions []Block + UnminedTransactions []TransactionSummary +} + +// GetTransactions returns transaction results between a starting and ending block. BlockC in the block range may be +// specified by either a height or a hash. +// +// Because this is a possibly lengthy operation, a cancel channel is provided to cancel the task. If this channel +// unblocks, the results created thus far will be returned. +// +// Transaction results are organized by blocks in ascending order and unmined transactions in an unspecified order. +// Mined transactions are saved in a Block structure which records properties about the block. +func (w *Wallet) GetTransactions( + startBlock, endBlock *BlockIdentifier, cancel qu.C, +) ( + res *GetTransactionsResult, + e error, +) { + var start, end int32 = 0, -1 + w.chainClientLock.Lock() + chainClient := w.chainClient + w.chainClientLock.Unlock() + // TODO: Fetching block heights by their hashes is inherently racy because not all block headers are saved but when + // they are for SPV the db can be queried directly without this. + var startResp, endResp rpcclient.FutureGetBlockVerboseResult + if startBlock != nil { + if startBlock.hash == nil { + start = startBlock.height + } else { + if chainClient == nil { + return nil, errors.New("no chain server client") + } + switch client := chainClient.(type) { + case *chainclient.RPCClient: + startResp = client.GetBlockVerboseTxAsync(startBlock.hash) + case *chainclient.BitcoindClient: + start, e = client.GetBlockHeight(startBlock.hash) + if e != nil { + return nil, e + } + // case *chainclient.NeutrinoClient: + // start, e = client.GetBlockHeight(startBlock.hash) + // if e != nil { + // return nil, e + // } + } + } + } + if endBlock != nil { + if endBlock.hash == nil { + end = endBlock.height + } else { + if chainClient == nil { + return nil, errors.New("no chain server client") + } + switch client := chainClient.(type) { + case *chainclient.RPCClient: + endResp = client.GetBlockVerboseTxAsync(endBlock.hash) + // case *chainclient.NeutrinoClient: + // end, e = client.GetBlockHeight(endBlock.hash) + // if e != nil { + // return nil, e + // } + } + } + } + var resp *btcjson.GetBlockVerboseResult + if startResp != nil { + resp, e = startResp.Receive() + if e != nil { + return nil, e + } + start = int32(resp.Height) + } + if endResp != nil { + resp, e = endResp.Receive() + if e != nil { + return nil, e + } + end = int32(resp.Height) + } + res = &GetTransactionsResult{} + e = walletdb.View( + w.db, func(dbtx walletdb.ReadTx) (e error) { + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + // TODO: probably should make RangeTransactions not reuse the + // details backing array memory. + dets := make([]wtxmgr.TxDetails, len(details)) + copy(dets, details) + details = dets + txs := make([]TransactionSummary, 0, len(details)) + for i := range details { + txs = append(txs, makeTxSummary(dbtx, w, &details[i])) + } + if details[0].Block.Height != -1 { + blockHash := details[0].Block.Hash + res.MinedTransactions = append( + res.MinedTransactions, Block{ + Hash: &blockHash, + Height: details[0].Block.Height, + Timestamp: details[0].Block.Time.Unix(), + Transactions: txs, + }, + ) + } else { + res.UnminedTransactions = txs + } + select { + case <-cancel.Wait(): + return true, nil + default: + return false, nil + } + } + return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn) + }, + ) + return res, e +} + +// AccountResult is a single account result for the AccountsResult type. +type AccountResult struct { + waddrmgr.AccountProperties + TotalBalance amt.Amount +} + +// AccountsResult is the result of the wallet's Accounts method. See that method for more details. +type AccountsResult struct { + Accounts []AccountResult + CurrentBlockHash *chainhash.Hash + CurrentBlockHeight int32 +} + +// Accounts returns the current names, numbers, and total balances of all accounts in the wallet restricted to a +// particular key scope. The current chain tip is included in the result for atomicity reasons. +// +// TODO(jrick): Is the chain tip really needed, since only the total balances are included? +func (w *Wallet) Accounts(scope waddrmgr.KeyScope) (*AccountsResult, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, e + } + var ( + accounts []AccountResult + syncBlockHash *chainhash.Hash + syncBlockHeight int32 + ) + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + syncBlock := w.Manager.SyncedTo() + syncBlockHash = &syncBlock.Hash + syncBlockHeight = syncBlock.Height + unspent, e := w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return e + } + e = manager.ForEachAccount( + addrmgrNs, func(acct uint32) (e error) { + props, e := manager.AccountProperties(addrmgrNs, acct) + if e != nil { + return e + } + accounts = append( + accounts, AccountResult{ + AccountProperties: *props, + // TotalBalance set below + }, + ) + return nil + }, + ) + if e != nil { + return e + } + m := make(map[uint32]*amt.Amount) + for i := range accounts { + a := &accounts[i] + m[a.AccountNumber] = &a.TotalBalance + } + for i := range unspent { + output := unspent[i] + var outputAcct uint32 + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + if e == nil && len(addrs) > 0 { + _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if e == nil { + amt, ok := m[outputAcct] + if ok { + *amt += output.Amount + } + } + } + return nil + }, + ) + return &AccountsResult{ + Accounts: accounts, + CurrentBlockHash: syncBlockHash, + CurrentBlockHeight: syncBlockHeight, + }, + e +} + +// AccountBalanceResult is a single result for the Wallet.AccountBalances method. +type AccountBalanceResult struct { + AccountNumber uint32 + AccountName string + AccountBalance amt.Amount +} + +// AccountBalances returns all accounts in the wallet and their balances. Balances are determined by excluding +// transactions that have not met requiredConfs confirmations. +func (w *Wallet) AccountBalances( + scope waddrmgr.KeyScope, + requiredConfs int32, +) ([]AccountBalanceResult, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, e + } + var results []AccountBalanceResult + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + syncBlock := w.Manager.SyncedTo() + // Fill out all account info except for the balances. + lastAcct, e := manager.LastAccount(addrmgrNs) + if e != nil { + return e + } + results = make([]AccountBalanceResult, lastAcct+2) + for i := range results[:len(results)-1] { + var accountName string + accountName, e = manager.AccountName(addrmgrNs, uint32(i)) + if e != nil { + return e + } + results[i].AccountNumber = uint32(i) + results[i].AccountName = accountName + } + results[len(results)-1].AccountNumber = waddrmgr.ImportedAddrAccount + results[len(results)-1].AccountName = waddrmgr.ImportedAddrAccountName + // Fetch all unspent outputs, and iterate over them tallying each account's balance where the output script pays + // to an account address and the required number of confirmations is met. + unspentOutputs, e := w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return e + } + for i := range unspentOutputs { + output := &unspentOutputs[i] + if !confirmed(requiredConfs, output.Height, syncBlock.Height) { + continue + } + if output.FromCoinBase && !confirmed( + int32(w.ChainParams().CoinbaseMaturity), + output.Height, syncBlock.Height, + ) { + continue + } + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + if e != nil || len(addrs) == 0 { + continue + } + outputAcct, e := manager.AddrAccount(addrmgrNs, addrs[0]) + if e != nil { + continue + } + switch { + case outputAcct == waddrmgr.ImportedAddrAccount: + results[len(results)-1].AccountBalance += output.Amount + case outputAcct > lastAcct: + return errors.New( + "waddrmgr.Manager.AddrAccount returned account " + + "beyond recorded last account", + ) + default: + results[outputAcct].AccountBalance += output.Amount + } + } + return nil + }, + ) + return results, e +} + +// creditSlice satisfies the sort.Interface interface to provide sorting transaction credits from oldest to newest. +// Credits with the same receive time and mined in the same block are not guaranteed to be sorted by the order they +// appear in the block. Credits from the same transaction are sorted by output index. +type creditSlice []wtxmgr.Credit + +func (s creditSlice) Len() int { + return len(s) +} +func (s creditSlice) Less(i, j int) bool { + switch { + // If both credits are from the same tx, txsort by output index. + case s[i].OutPoint.Hash == s[j].OutPoint.Hash: + return s[i].OutPoint.Index < s[j].OutPoint.Index + // If both transactions are unmined, txsort by their received date. + case s[i].Height == -1 && s[j].Height == -1: + return s[i].Received.Before(s[j].Received) + // Unmined (newer) txs always come last. + case s[i].Height == -1: + return false + case s[j].Height == -1: + return true + // If both txs are mined in different blocks, txsort by block height. + default: + return s[i].Height < s[j].Height + } +} +func (s creditSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// ListUnspent returns a slice of objects representing the unspent wallet transactions fitting the given criteria. The +// confirmations will be more than minconf, less than maxconf and if addresses is populated only the addresses contained +// within it will be considered. If we know nothing about a transaction an empty array will be returned. +func (w *Wallet) ListUnspent( + minconf, maxconf int32, + addresses map[string]struct{}, +) (results []*btcjson.ListUnspentResult, e error) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + syncBlock := w.Manager.SyncedTo() + filter := len(addresses) != 0 + unspent, e := w.TxStore.UnspentOutputs(txmgrNs) + if e != nil { + return e + } + sort.Sort(sort.Reverse(creditSlice(unspent))) + defaultAccountName := "default" + results = make([]*btcjson.ListUnspentResult, 0, len(unspent)) + for i := range unspent { + output := unspent[i] + // Outputs with fewer confirmations than the minimum or more confs than the maximum are excluded. + confs := confirms(output.Height, syncBlock.Height) + if confs < minconf || confs > maxconf { + continue + } + // Only mature coinbase outputs are included. + if output.FromCoinBase { + target := int32(w.ChainParams().CoinbaseMaturity) + if !confirmed(target, output.Height, syncBlock.Height) { + continue + } + } + // Exclude locked outputs from the result set. + if w.LockedOutpoint(output.OutPoint) { + continue + } + // Lookup the associated account for the output. Use the default account name in case there is no associated + // account for some reason, although this should never happen. + // + // This will be unnecessary once transactions and outputs are grouped under the associated account in the + // db. + acctName := defaultAccountName + var sc txscript.ScriptClass + var addrs []btcaddr.Address + sc, addrs, _, e = txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams, + ) + if e != nil { + continue + } + if len(addrs) > 0 { + smgr, acct, e := w.Manager.AddrAccount(addrmgrNs, addrs[0]) + if e == nil { + s, e := smgr.AccountName(addrmgrNs, acct) + if e == nil { + acctName = s + } + } + } + if filter { + for _, addr := range addrs { + _, ok := addresses[addr.EncodeAddress()] + if ok { + goto include + } + } + continue + } + include: + // At the moment watch-only addresses are not supported, so all recorded outputs that are not multisig are + // "spendable". Multisig outputs are only "spendable" if all keys are controlled by this wallet. + // + // TODO: Each case will need updates when watch-only addrs is added. For P2PK, P2PKH, and P2SH, the address + // must be looked up and not be watching-only. For multisig, all pubkeys must belong to the manager with the + // associated private key (currently it only checks whether the pubkey exists, since the private key is + // required at the moment). + var spendable bool + scSwitch: + switch sc { + case txscript.PubKeyHashTy: + spendable = true + case txscript.PubKeyTy: + spendable = true + // case txscript.WitnessV0ScriptHashTy: + // spendable = true + // case txscript.WitnessV0PubKeyHashTy: + // spendable = true + case txscript.MultiSigTy: + for _, a := range addrs { + _, e := w.Manager.Address(addrmgrNs, a) + if e == nil { + continue + } + if waddrmgr.IsError(e, waddrmgr.ErrAddressNotFound) { + break scSwitch + } + return e + } + spendable = true + } + result := &btcjson.ListUnspentResult{ + TxID: output.OutPoint.Hash.String(), + Vout: output.OutPoint.Index, + Account: acctName, + ScriptPubKey: hex.EncodeToString(output.PkScript), + Amount: output.Amount.ToDUO(), + Confirmations: int64(confs), + Spendable: spendable, + } + // BUG: this should be a JSON array so that all addresses can be included, or removed (and the caller + // extracts addresses from the pkScript). + if len(addrs) > 0 { + result.Address = addrs[0].EncodeAddress() + } + results = append(results, result) + } + return nil + }, + ) + return results, e +} + +// DumpPrivKeys returns the WIF-encoded private keys for all addresses with private keys in a wallet. +func (w *Wallet) DumpPrivKeys() (privkeys []string, e error) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Iterate over each active address, appending the private key to privkeys. + return w.Manager.ForEachActiveAddress( + addrmgrNs, func(addr btcaddr.Address) (e error) { + var ma waddrmgr.ManagedAddress + ma, e = w.Manager.Address(addrmgrNs, addr) + if e != nil { + return e + } + // Only those addresses with keys needed. + pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil + } + var wif *util.WIF + wif, e = pka.ExportPrivKey() + if e != nil { + // It would be nice to zero out the array here. However, since strings in go are immutable, and we have + // no control over the caller I don't think we can. :( + return e + } + privkeys = append(privkeys, wif.String()) + return nil + }, + ) + }, + ) + return privkeys, e +} + +// DumpWIFPrivateKey returns the WIF encoded private key for a single wallet address. +func (w *Wallet) DumpWIFPrivateKey(addr btcaddr.Address) ( + address string, e error, +) { + var maddr waddrmgr.ManagedAddress + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + // Get private key from wallet if it exists. + maddr, e = w.Manager.Address(waddrmgrNs, addr) + return e + }, + ) + if e != nil { + return "", e + } + pka, ok := maddr.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return "", fmt.Errorf("address %s is not a key type", addr) + } + wif, e := pka.ExportPrivKey() + if e != nil { + return "", e + } + return wif.String(), nil +} + +// ImportPrivateKey imports a private key to the wallet and writes the new wallet to disk. +func (w *Wallet) ImportPrivateKey( + scope waddrmgr.KeyScope, wif *util.WIF, + bs *waddrmgr.BlockStamp, rescan bool, +) (string, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return "", e + } + // The starting block for the key is the genesis block unless otherwise specified. + var newBirthday time.Time + if bs == nil { + bs = &waddrmgr.BlockStamp{ + Hash: *w.chainParams.GenesisHash, + Height: 0, + } + } else { + // Only update the new birthday time from default value if we actually have timestamp info in the header. + var header *wire.BlockHeader + header, e = w.chainClient.GetBlockHeader(&bs.Hash) + if e == nil { + newBirthday = header.Timestamp + } + } + // Attempt to import private key into wallet. + var addr btcaddr.Address + var props *waddrmgr.AccountProperties + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + maddr, e := manager.ImportPrivateKey(addrmgrNs, wif, bs) + if e != nil { + return e + } + addr = maddr.Address() + props, e = manager.AccountProperties( + addrmgrNs, waddrmgr.ImportedAddrAccount, + ) + if e != nil { + return e + } + return w.Manager.SetBirthday(addrmgrNs, newBirthday) + }, + ) + if e != nil { + return "", e + } + // Rescan blockchain for transactions with txout scripts paying to the imported address. + if rescan { + job := &RescanJob{ + Addrs: []btcaddr.Address{addr}, + OutPoints: nil, + BlockStamp: *bs, + } + // Submit rescan job and log when the import has completed. Do not block on finishing the rescan. The rescan + // success or failure is logged elsewhere, and the channel is not required to be read, so discard the return + // value. + _ = w.SubmitRescan(job) + } else { + e := w.chainClient.NotifyReceived([]btcaddr.Address{addr}) + if e != nil { + return "", fmt.Errorf( + "failed to subscribe for address ntfns for "+ + "address %s: %s", addr.EncodeAddress(), e, + ) + } + } + addrStr := addr.EncodeAddress() + I.Ln("imported payment address", addrStr) + w.NtfnServer.notifyAccountProperties(props) + // Return the payment address string of the imported private key. + return addrStr, nil +} + +// LockedOutpoint returns whether an outpoint has been marked as locked and should not be used as an input for created +// transactions. +func (w *Wallet) LockedOutpoint(op wire.OutPoint) bool { + _, locked := w.lockedOutpoints[op] + return locked +} + +// LockOutpoint marks an outpoint as locked, that is, it should not be used as an input for newly created transactions. +func (w *Wallet) LockOutpoint(op wire.OutPoint) { + w.lockedOutpoints[op] = struct{}{} +} + +// UnlockOutpoint marks an outpoint as unlocked, that is, it may be used as an input for newly created transactions. +func (w *Wallet) UnlockOutpoint(op wire.OutPoint) { + delete(w.lockedOutpoints, op) +} + +// ResetLockedOutpoints resets the set of locked outpoints so all may be used as inputs for new transactions. +func (w *Wallet) ResetLockedOutpoints() { + w.lockedOutpoints = map[wire.OutPoint]struct{}{} +} + +// LockedOutpoints returns a slice of currently locked outpoints. This is intended to be used by marshaling the result +// as a JSON array for listlockunspent RPC results. +func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { + locked := make([]btcjson.TransactionInput, len(w.lockedOutpoints)) + i := 0 + for op := range w.lockedOutpoints { + locked[i] = btcjson.TransactionInput{ + Txid: op.Hash.String(), + Vout: op.Index, + } + i++ + } + return locked +} + +// resendUnminedTxs iterates through all transactions that spend from wallet credits that are not known to have been +// mined into a block, and attempts to send each to the chain server for relay. +func (w *Wallet) resendUnminedTxs() { + chainClient, e := w.requireChainClient() + if e != nil { + E.Ln("no chain server available to resend unmined transactions", e) + return + } + var txs []*wire.MsgTx + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + txs, e = w.TxStore.UnminedTxs(txmgrNs) + return e + }, + ) + if e != nil { + E.Ln("cannot load unmined transactions for resending:", e) + return + } + for _, tx := range txs { + resp, e := chainClient.SendRawTransaction(tx, false) + if e != nil { + D.F( + "could not resend transaction %v: %v %s", + tx.TxHash(), e, + ) + // We'll only stop broadcasting transactions if we detect that the output has already been fully spent, is + // an orphan, or is conflicting with another transaction. + // + // TODO(roasbeef): SendRawTransaction needs to return concrete error types, no need for string matching + switch { + // The following are errors returned from pod's mempool. + case strings.Contains(e.Error(), "spent"): + case strings.Contains(e.Error(), "orphan"): + case strings.Contains(e.Error(), "conflict"): + case strings.Contains(e.Error(), "already exists"): + case strings.Contains(e.Error(), "negative"): + // The following errors are returned from bitcoind's + // mempool. + case strings.Contains(e.Error(), "Missing inputs"): + case strings.Contains(e.Error(), "already in block chain"): + case strings.Contains(e.Error(), "fee not met"): + default: + continue + } + // As the transaction was rejected, we'll attempt to remove the unmined transaction all together. Otherwise, + // we'll keep attempting to rebroadcast this, and we may be computing our balance incorrectly if this tx + // credits or debits to us. + tt := tx + e := walletdb.Update( + w.db, func(dbTx walletdb.ReadWriteTx) (e error) { + txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) + txRec, e := wtxmgr.NewTxRecordFromMsgTx( + tt, time.Now(), + ) + if e != nil { + return e + } + return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) + }, + ) + if e != nil { + W.F( + "unable to remove conflicting tx %v: %v %s", tt.TxHash(), + e, + ) + continue + } + I.C( + func() string { + return "removed conflicting tx:" + spew.Sdump(tt) + " " + }, + ) + continue + } + D.Ln("resent unmined transaction", resp) + } +} + +// SortedActivePaymentAddresses returns a slice of all active payment addresses in a wallet. +func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { + var addrStrs []string + e := walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + return w.Manager.ForEachActiveAddress( + addrmgrNs, func(addr btcaddr.Address) (e error) { + addrStrs = append(addrStrs, addr.EncodeAddress()) + return nil + }, + ) + }, + ) + if e != nil { + return nil, e + } + sort.Strings(addrStrs) + return addrStrs, nil +} + +// NewAddress returns the next external chained address for a wallet. +func (w *Wallet) NewAddress( + account uint32, scope waddrmgr.KeyScope, + nochain bool, +) (addr btcaddr.Address, e error) { + var ( + chainClient chainclient.Interface + props *waddrmgr.AccountProperties + ) + if !nochain { + chainClient, e = w.requireChainClient() + if e != nil { + return nil, e + } + } + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + addr, props, e = w.newAddress(addrmgrNs, account, scope) + return e + }, + ) + if e != nil { + return nil, e + } + if !nochain { + // Notify the rpc server about the newly created address. + e = chainClient.NotifyReceived([]btcaddr.Address{addr}) + if e != nil { + return nil, e + } + w.NtfnServer.notifyAccountProperties(props) + } + return addr, nil +} +func (w *Wallet) newAddress( + addrmgrNs walletdb.ReadWriteBucket, account uint32, + scope waddrmgr.KeyScope, +) (btcaddr.Address, *waddrmgr.AccountProperties, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, nil, e + } + // Get next address from wallet. + var addrs []waddrmgr.ManagedAddress + if addrs, e = manager.NextExternalAddresses(addrmgrNs, account, 1); E.Chk(e) { + return nil, nil, e + } + var props *waddrmgr.AccountProperties + if props, e = manager.AccountProperties(addrmgrNs, account); E.Chk(e) { + E.Ln( + "cannot fetch account properties for notification after deriving next external address:", + e, + ) + return nil, nil, e + } + return addrs[0].Address(), props, nil +} + +// NewChangeAddress returns a new change address for a wallet. +func (w *Wallet) NewChangeAddress( + account uint32, + scope waddrmgr.KeyScope, +) (btcaddr.Address, error) { + chainClient, e := w.requireChainClient() + if e != nil { + return nil, e + } + var addr btcaddr.Address + e = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + addr, e = w.newChangeAddress(addrmgrNs, account) + return e + }, + ) + if e != nil { + return nil, e + } + // Notify the rpc server about the newly created address. + e = chainClient.NotifyReceived([]btcaddr.Address{addr}) + if e != nil { + return nil, e + } + return addr, nil +} +func (w *Wallet) newChangeAddress( + addrmgrNs walletdb.ReadWriteBucket, + account uint32, +) (btcaddr.Address, error) { + // As we're making a change address, we'll fetch the type of manager that is able to make p2wkh output as they're + // the most efficient. + scopes := w.Manager.ScopesForExternalAddrType( + waddrmgr.PubKeyHash, + ) + manager, e := w.Manager.FetchScopedKeyManager(scopes[0]) + if e != nil { + return nil, e + } + // Get next chained change address from wallet for account. + addrs, e := manager.NextInternalAddresses(addrmgrNs, account, 1) + if e != nil { + return nil, e + } + return addrs[0].Address(), nil +} + +// confirmed checks whether a transaction at height txHeight has met minconf confirmations for a blockchain at height +// curHeight. +func confirmed(minconf, txHeight, curHeight int32) bool { + return confirms(txHeight, curHeight) >= minconf +} + +// confirms returns the number of confirmations for a transaction in a block at height txHeight (or -1 for an +// unconfirmed tx) given the chain height curHeight. +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +// AccountTotalReceivedResult is a single result for the Wallet.TotalReceivedForAccounts method. +type AccountTotalReceivedResult struct { + AccountNumber uint32 + AccountName string + TotalReceived amt.Amount + LastConfirmation int32 +} + +// TotalReceivedForAccounts iterates through a wallet's transaction history, returning the total amount of Bitcoin +// received for all accounts. +func (w *Wallet) TotalReceivedForAccounts( + scope waddrmgr.KeyScope, + minConf int32, +) ([]AccountTotalReceivedResult, error) { + manager, e := w.Manager.FetchScopedKeyManager(scope) + if e != nil { + return nil, e + } + var results []AccountTotalReceivedResult + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + syncBlock := w.Manager.SyncedTo() + e = manager.ForEachAccount( + addrmgrNs, func(account uint32) (e error) { + accountName, e := manager.AccountName(addrmgrNs, account) + if e != nil { + return e + } + results = append( + results, AccountTotalReceivedResult{ + AccountNumber: account, + AccountName: accountName, + }, + ) + return nil + }, + ) + if e != nil { + return e + } + var stopHeight int32 + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + var outputAcct uint32 + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs(pkScript, w.chainParams) + if e == nil && len(addrs) > 0 { + _, outputAcct, e = w.Manager.AddrAccount(addrmgrNs, addrs[0]) + } + if e == nil { + acctIndex := int(outputAcct) + if outputAcct == waddrmgr.ImportedAddrAccount { + acctIndex = len(results) - 1 + } + res := &results[acctIndex] + res.TotalReceived += cred.Amount + res.LastConfirmation = confirms( + detail.Block.Height, syncBlock.Height, + ) + } + } + } + return false, nil + } + return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) + }, + ) + return results, e +} + +// TotalReceivedForAddr iterates through a wallet's transaction history, returning the total amount of bitcoins received +// for a single wallet address. +func (w *Wallet) TotalReceivedForAddr( + addr btcaddr.Address, minConf int32, +) (amount amt.Amount, e error) { + e = walletdb.View( + w.db, func(tx walletdb.ReadTx) (e error) { + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + syncBlock := w.Manager.SyncedTo() + var ( + addrStr = addr.EncodeAddress() + stopHeight int32 + ) + if minConf > 0 { + stopHeight = syncBlock.Height - minConf + 1 + } else { + stopHeight = -1 + } + rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { + for i := range details { + detail := &details[i] + for _, cred := range detail.Credits { + pkScript := detail.MsgTx.TxOut[cred.Index].PkScript + var addrs []btcaddr.Address + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + pkScript, + w.chainParams, + ) + // An error creating addresses from the output script only indicates a non-standard script, so + // ignore this credit. + if e != nil { + continue + } + for _, a := range addrs { + if addrStr == a.EncodeAddress() { + amount += cred.Amount + break + } + } + } + } + return false, nil + } + return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) + }, + ) + return amount, e +} + +// SendOutputs creates and sends payment transactions. It returns the transaction hash upon success. +func (w *Wallet) SendOutputs( + outputs []*wire.TxOut, account uint32, + minconf int32, satPerKb amt.Amount, +) (*chainhash.Hash, error) { + // Ensure the outputs to be created adhere to the network's consensus rules. + for _, output := range outputs { + if e := txrules.CheckOutput(output, satPerKb); E.Chk(e) { + return nil, e + } + } + // Create the transaction and broadcast it to the network. The transaction will be added to the database in order to + // ensure that we continue to re-broadcast the transaction upon restarts until it has been confirmed. + createdTx, e := w.CreateSimpleTx(account, outputs, minconf, satPerKb) + if e != nil { + return nil, e + } + D.S(createdTx) + return w.publishTransaction(createdTx.Tx) +} + +// SignatureError records the underlying error when validating a transaction input signature. +type SignatureError struct { + InputIndex uint32 + Error error +} + +// SignTransaction uses secrets of the wallet, as well as additional secrets +// passed in by the caller, to create and add input signatures to a transaction. +// +// Transaction input script validation is used to confirm that all signatures +// are valid. For any invalid input, a SignatureError is added to the returns. +// The final error return is reserved for unexpected or fatal errors, such as +// being unable to determine a previous output script to redeem. +// +// The transaction pointed to by tx is modified by this function. +func (w *Wallet) SignTransaction( + tx *wire.MsgTx, hashType txscript.SigHashType, + additionalPrevScripts map[wire.OutPoint][]byte, + additionalKeysByAddress map[string]*util.WIF, + p2shRedeemScriptsByAddress map[string][]byte, +) (signErrors []SignatureError, e error) { + I.Ln("signing transaction") + I.S(tx) + e = walletdb.View( + w.db, func(dbtx walletdb.ReadTx) (e error) { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + for i, txIn := range tx.TxIn { + prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] + if !ok { + prevHash := &txIn.PreviousOutPoint.Hash + prevIndex := txIn.PreviousOutPoint.Index + var txDetails *wtxmgr.TxDetails + txDetails, e = w.TxStore.TxDetails(txmgrNs, prevHash) + if e != nil { + return fmt.Errorf( + "cannot query previous transaction details for %v: %v", + txIn.PreviousOutPoint, e, + ) + } + if txDetails == nil { + return fmt.Errorf( + "%v not found", + txIn.PreviousOutPoint, + ) + } + prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript + } + // Set up our callbacks that we pass to txscript so it can look up the + // appropriate keys and scripts by address. + getKey := txscript.KeyClosure( + func(addr btcaddr.Address) (*ec.PrivateKey, bool, error) { + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + var wif *util.WIF + wif, ok = additionalKeysByAddress[addrStr] + if !ok { + return nil, false, + errors.New("no key for address") + } + return wif.PrivKey, wif.CompressPubKey, nil + } + var address waddrmgr.ManagedAddress + address, e = w.Manager.Address(addrmgrNs, addr) + if e != nil { + return nil, false, e + } + var pka waddrmgr.ManagedPubKeyAddress + pka, ok = address.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil, false, fmt.Errorf( + "address %v is not "+ + "a pubkey address", address.Address().EncodeAddress(), + ) + } + var key *ec.PrivateKey + key, e = pka.PrivKey() + if e != nil { + return nil, false, e + } + return key, pka.Compressed(), nil + }, + ) + getScript := txscript.ScriptClosure( + func(addr btcaddr.Address) ([]byte, error) { + // If keys were provided then we can only use the redeem scripts provided with + // our inputs, too. + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + script, ok := p2shRedeemScriptsByAddress[addrStr] + if !ok { + return nil, errors.New("no script for address") + } + return script, nil + } + address, e := w.Manager.Address(addrmgrNs, addr) + if e != nil { + return nil, e + } + sa, ok := address.(waddrmgr.ManagedScriptAddress) + if !ok { + return nil, errors.New( + "address is not a script" + + " address", + ) + } + return sa.Script() + }, + ) + // SigHashSingle inputs can only be signed if there's a corresponding output. However this could be already + // signed, so we always verify the output. + if (hashType&txscript.SigHashSingle) != + txscript.SigHashSingle || i < len(tx.TxOut) { + script, e := txscript.SignTxOutput( + w.ChainParams(), + tx, i, prevOutScript, hashType, getKey, + getScript, txIn.SignatureScript, + ) + // Failure to sign isn't an error, it just means that the tx isn't complete. + if e != nil { + signErrors = append( + signErrors, SignatureError{ + InputIndex: uint32(i), + Error: e, + }, + ) + continue + } + txIn.SignatureScript = script + } + // Either it was already signed or we just signed it. Find out if it is completely satisfied or still needs + // more. + vm, e := txscript.NewEngine( + prevOutScript, tx, i, + txscript.StandardVerifyFlags, nil, nil, 0, + ) + if e == nil { + e = vm.Execute() + } + if e != nil { + signErrors = append( + signErrors, SignatureError{ + InputIndex: uint32(i), + Error: e, + }, + ) + } + } + return nil + }, + ) + return signErrors, e +} + +// PublishTransaction sends the transaction to the consensus RPC server so it can be propagated to other nodes and +// eventually mined. +// +// This function is unstable and will be removed once syncing code is moved out of the wallet. +func (w *Wallet) PublishTransaction(tx *wire.MsgTx) (e error) { + _, e = w.publishTransaction(tx) + return e +} + +// publishTransaction is the private version of PublishTransaction which contains the primary logic required for +// publishing a transaction, updating the relevant database state, and finally possible removing the transaction from +// the database (along with cleaning up all inputs used, and outputs created) if the transaction is rejected by the back +// end. +func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { + server, e := w.requireChainClient() + if e != nil { + return nil, e + } + // As we aim for this to be general reliable transaction broadcast API, we'll write this tx to disk as an + // unconfirmed transaction. This way, upon restarts, we'll always rebroadcast it, and also add it to our set of + // records. + txRec, e := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) + if e != nil { + return nil, e + } + e = walletdb.Update( + w.db, func(dbTx walletdb.ReadWriteTx) (e error) { + return w.addRelevantTx(dbTx, txRec, nil) + }, + ) + if e != nil { + return nil, e + } + txid, e := server.SendRawTransaction(tx, false) + switch { + case e == nil: + return txid, nil + // The following are errors returned from pod's mempool. + case strings.Contains(e.Error(), "spent"): + fallthrough + case strings.Contains(e.Error(), "orphan"): + fallthrough + case strings.Contains(e.Error(), "conflict"): + fallthrough + // The following errors are returned from bitcoind's mempool. + case strings.Contains(e.Error(), "fee not met"): + fallthrough + case strings.Contains(e.Error(), "Missing inputs"): + fallthrough + case strings.Contains(e.Error(), "already in block chain"): + // If the transaction was rejected, then we'll remove it from the txstore, as otherwise, we'll attempt to + // continually re-broadcast it, and the utxo state of the wallet won't be accurate. + dbErr := walletdb.Update( + w.db, func(dbTx walletdb.ReadWriteTx) (e error) { + txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) + return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) + }, + ) + if dbErr != nil { + return nil, fmt.Errorf( + "unable to broadcast tx: %v, "+ + "unable to remove invalid tx: %v", e, dbErr, + ) + } + return nil, e + default: + return nil, e + } +} + +// ChainParams returns the network parameters for the blockchain the wallet belongs to. +func (w *Wallet) ChainParams() *chaincfg.Params { + return w.chainParams +} + +// Database returns the underlying walletdb database. This method is provided in order to allow applications wrapping +// btcwallet to store node-specific data with the wallet's database. +func (w *Wallet) Database() walletdb.DB { + return w.db +} + +// Create creates an new wallet, writing it to an empty database. If the passed seed is non-nil, it is used. Otherwise, +// a secure random seed of the recommended length is generated. +func Create( + db walletdb.DB, pubPass, privPass, seed []byte, params *chaincfg.Params, + birthday time.Time, +) (e error) { + // If a seed was provided, ensure that it is of valid length. Otherwise, we generate a random seed for the wallet + // with the recommended seed length. + if seed == nil { + hdSeed, e := hdkeychain.GenerateSeed( + hdkeychain.RecommendedSeedLen, + ) + if e != nil { + return e + } + seed = hdSeed + } + if len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + return hdkeychain.ErrInvalidSeedLen + } + return walletdb.Update( + db, func(tx walletdb.ReadWriteTx) (e error) { + addrmgrNs, e := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if e != nil { + return e + } + txmgrNs, e := tx.CreateTopLevelBucket(wtxmgrNamespaceKey) + if e != nil { + return e + } + e = waddrmgr.Create( + addrmgrNs, seed, pubPass, privPass, params, nil, + birthday, + ) + if e != nil { + return e + } + return wtxmgr.Create(txmgrNs) + }, + ) +} + +// Open loads an already-created wallet from the passed database and namespaces. +func Open( + db walletdb.DB, + pubPass []byte, + cbs *waddrmgr.OpenCallbacks, + params *chaincfg.Params, + recoveryWindow uint32, + podConfig *config.Config, + quit qu.C, +) (*Wallet, error) { + // debug.PrintStack() + W.Ln("opening wallet") // , string(pubPass)) + e := walletdb.View( + db, func(tx walletdb.ReadTx) (e error) { + waddrmgrBucket := tx.ReadBucket(waddrmgrNamespaceKey) + if waddrmgrBucket == nil { + return errors.New("missing address manager namespace") + } + wtxmgrBucket := tx.ReadBucket(wtxmgrNamespaceKey) + if wtxmgrBucket == nil { + return errors.New("missing transaction manager namespace") + } + return nil + }, + ) + if e != nil { + return nil, e + } + T.Ln("opened wallet") + // Perform upgrades as necessary. Each upgrade is done under its own transaction, which is managed by each package + // itself, so the entire DB is passed instead of passing already opened write transaction. + // + // This will need to change later when upgrades in one package depend on data in another (such as removing chain + // synchronization from address manager). + T.Ln("doing address manager upgrades") + e = waddrmgr.DoUpgrades(db, waddrmgrNamespaceKey, pubPass, params, cbs) + if e != nil { + return nil, e + } + T.Ln("doing txmanager upgrades") + e = wtxmgr.DoUpgrades(db, wtxmgrNamespaceKey) + if e != nil { + return nil, e + } + // Open database abstraction instances + var ( + addrMgr *waddrmgr.Manager + txMgr *wtxmgr.Store + ) + T.Ln("opening wallet database abstraction instances") + e = walletdb.View( + db, func(tx walletdb.ReadTx) (e error) { + T.Ln("reading address bucket") + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + T.Ln("reading tx bucket") + txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) + T.Ln("opening address manager") + addrMgr, e = waddrmgr.Open(addrmgrNs, pubPass, params) + if e != nil { + E.Ln(e, "'"+string(pubPass)+"'") + return e + } + T.Ln("opening transaction manager") + txMgr, e = wtxmgr.Open(txmgrNs, params) + T.Ln("wallet database abstraction instances opened") + return e + }, + ) + if e != nil { + return nil, e + } + T.Ln("creating wallet state") // TODO: log balance? last sync height? + w := &Wallet{ + publicPassphrase: pubPass, + db: db, + Manager: addrMgr, + TxStore: txMgr, + lockedOutpoints: map[wire.OutPoint]struct{}{}, + recoveryWindow: recoveryWindow, + rescanAddJob: make(chan *RescanJob), + rescanBatch: make(chan *rescanBatch), + rescanNotifications: make(chan interface{}), + rescanProgress: make(chan *RescanProgressMsg), + rescanFinished: make(chan *RescanFinishedMsg), + createTxRequests: make(chan createTxRequest), + unlockRequests: make(chan unlockRequest), + lockRequests: qu.T(), + holdUnlockRequests: make(chan chan heldUnlock), + lockState: make(chan bool), + changePassphrase: make(chan changePassphraseRequest), + changePassphrases: make(chan changePassphrasesRequest), + chainParams: params, + PodConfig: podConfig, + quit: quit, + } + w.NtfnServer = newNotificationServer(w) + w.TxStore.NotifyUnspent = func(hash *chainhash.Hash, index uint32) { + w.NtfnServer.notifyUnspentOutput(0, hash, index) + } + T.Ln("wallet state created") + return w, nil +} diff --git a/cmd/wallet/walletsetup.go b/cmd/wallet/walletsetup.go new file mode 100644 index 0000000..e59e42c --- /dev/null +++ b/cmd/wallet/walletsetup.go @@ -0,0 +1,254 @@ +package wallet + +import ( + "bufio" + "os" + "path/filepath" + "time" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/constant" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/util/legacy/keystore" + "github.com/p9c/p9/pkg/util/prompt" + "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/pkg/walletdb" + "github.com/p9c/p9/pkg/wire" + "github.com/p9c/p9/pod/config" + // This initializes the bdb driver + _ "github.com/p9c/p9/pkg/walletdb/bdb" +) + +// CreateSimulationWallet is intended to be called from the rpcclient and used +// to create a wallet for actors involved in simulations. +func CreateSimulationWallet(activenet *chaincfg.Params, cfg *config.Config) (e error) { + // Simulation wallet password is 'password'. + privPass := []byte("password") + // Public passphrase is the default. + pubPass := []byte(InsecurePubPassphrase) + netDir := NetworkDir(cfg.DataDir.V(), activenet) + // Create the wallet. + dbPath := filepath.Join(netDir, constant.WalletDbName) + I.Ln("Creating the wallet...") + // Create the wallet database backed by bolt db. + db, e := walletdb.Create("bdb", dbPath) + if e != nil { + return e + } + defer func() { + if e = db.Close(); E.Chk(e) { + } + }() + // Create the wallet. + e = Create(db, pubPass, privPass, nil, activenet, time.Now()) + if e != nil { + return e + } + I.Ln("The wallet has been created successfully.") + return nil +} + +// CreateWallet prompts the user for information needed to generate a new wallet and generates the wallet accordingly. +// The new wallet will reside at the provided path. +func CreateWallet(activenet *chaincfg.Params, config *config.Config) (e error) { + dbDir := *config.WalletFile + loader := NewLoader(activenet, dbDir.V(), 250) + D.Ln("WalletPage", loader.ChainParams.Name) + // When there is a legacy keystore, open it now to ensure any errors don't end up exiting the process after the user + // has spent time entering a bunch of information. + netDir := NetworkDir(config.DataDir.V(), activenet) + keystorePath := filepath.Join(netDir, keystore.Filename) + var legacyKeyStore *keystore.Store + _, e = os.Stat(keystorePath) + if e != nil && !os.IsNotExist(e) { + // A stat error not due to a non-existant file should be returned to the caller. + return e + } else if e == nil { + // Keystore file exists. + legacyKeyStore, e = keystore.OpenDir(netDir) + if e != nil { + return e + } + } + // Start by prompting for the private passphrase. When there is an existing keystore, the user will be promped for + // that passphrase, otherwise they will be prompted for a new one. + reader := bufio.NewReader(os.Stdin) + privPass, e := prompt.PrivatePass(reader, legacyKeyStore) + if e != nil { + D.Ln(e) + time.Sleep(time.Second * 3) + return e + } + // When there exists a legacy keystore, unlock it now and set up a callback to import all keystore keys into the new + // walletdb wallet + if legacyKeyStore != nil { + e = legacyKeyStore.Unlock(privPass) + if e != nil { + return e + } + // Import the addresses in the legacy keystore to the new wallet if any exist, locking each wallet again when + // finished. + loader.RunAfterLoad( + func(w *Wallet) { + defer func() { + e = legacyKeyStore.Lock() + if e != nil { + D.Ln(e) + } + }() + I.Ln("Importing addresses from existing wallet...") + lockChan := make(chan time.Time, 1) + defer func() { + lockChan <- time.Time{} + }() + e = w.Unlock(privPass, lockChan) + if e != nil { + E.F( + "ERR: Failed to unlock new wallet "+ + "during old wallet key import: %v", e, + ) + return + } + e = convertLegacyKeystore(legacyKeyStore, w) + if e != nil { + E.F( + "ERR: Failed to import keys from old "+ + "wallet format: %v %s", e, + ) + return + } + // Remove the legacy key store. + e = os.Remove(keystorePath) + if e != nil { + E.Ln( + "WARN: Failed to remove legacy wallet "+ + "from'%s'\n", keystorePath, + ) + } + }, + ) + } + // Ascertain the public passphrase. This will either be a value specified by the user or the default hard-coded + // public passphrase if the user does not want the additional public data encryption. + var pubPass []byte + if pubPass, e = prompt.PublicPass(reader, privPass, []byte(""), + config.WalletPass.Bytes());E.Chk(e){ + time.Sleep(time.Second * 5) + return e + } + // Ascertain the wallet generation seed. This will either be an automatically generated value the user has already + // confirmed or a value the user has entered which has already been validated. + seed, e := prompt.Seed(reader) + if e != nil { + D.Ln(e) + time.Sleep(time.Second * 5) + return e + } + D.Ln("Creating the wallet") + w, e := loader.CreateNewWallet(pubPass, privPass, seed, time.Now(), false, config, nil) + if e != nil { + D.Ln(e) + time.Sleep(time.Second * 5) + return e + } + w.Manager.Close() + D.Ln("The wallet has been created successfully.") + return nil +} + +// NetworkDir returns the directory name of a network directory to hold wallet files. +func NetworkDir(dataDir string, chainParams *chaincfg.Params) string { + netname := chainParams.Name + // For now, we must always name the testnet data directory as "testnet" and not "testnet3" or any other version, as + // the chaincfg testnet3 paramaters will likely be switched to being named "testnet3" in the future. This is done to + // future proof that change, and an upgrade plan to move the testnet3 data directory can be worked out later. + if chainParams.Net == wire.TestNet3 { + netname = "testnet" + } + return filepath.Join(dataDir, netname) +} + +// // checkCreateDir checks that the path exists and is a directory. +// // If path does not exist, it is created. +// func checkCreateDir(// path string) (e error) { +// if fi, e := os.Stat(path); E.Chk(e) { +// if os.IsNotExist(e) { +// // Attempt data directory creation +// if e = os.MkdirAll(path, 0700); E.Chk(e) { +// return fmt.Errorf("cannot create directory: %s", e) +// } +// } else { +// return fmt.Errorf("error checking directory: %s", e) +// } +// } else { +// if !fi.IsDir() { +// return fmt.Errorf("path '%s' is not a directory", path) +// } +// } +// return nil +// } + +// convertLegacyKeystore converts all of the addresses in the passed legacy key store to the new waddrmgr.Manager +// format. Both the legacy keystore and the new manager must be unlocked. +func convertLegacyKeystore(legacyKeyStore *keystore.Store, w *Wallet) (e error) { + netParams := legacyKeyStore.Net() + blockStamp := waddrmgr.BlockStamp{ + Height: 0, + Hash: *netParams.GenesisHash, + } + for _, walletAddr := range legacyKeyStore.ActiveAddresses() { + switch addr := walletAddr.(type) { + case keystore.PubKeyAddress: + privKey, e := addr.PrivKey() + if e != nil { + W.F( + "Failed to obtain private key "+ + "for address %v: %v", addr.Address(), + e, + ) + continue + } + wif, e := util.NewWIF( + privKey, + netParams, addr.Compressed(), + ) + if e != nil { + E.Ln( + "Failed to create wallet "+ + "import format for address %v: %v", + addr.Address(), e, + ) + continue + } + _, e = w.ImportPrivateKey( + waddrmgr.KeyScopeBIP0044, + wif, &blockStamp, false, + ) + if e != nil { + W.F( + "WARN: Failed to import private "+ + "key for address %v: %v", + addr.Address(), e, + ) + continue + } + case keystore.ScriptAddress: + _, e := w.ImportP2SHRedeemScript(addr.Script()) + if e != nil { + W.F( + "WARN: Failed to import "+ + "pay-to-script-hash script for "+ + "address %v: %v\n", addr.Address(), e, + ) + continue + } + default: + W.F( + "WARN: Skipping unrecognized legacy "+ + "keystore type: %T\n", addr, + ) + continue + } + } + return nil +} diff --git a/docker-source.sh b/docker-source.sh new file mode 100755 index 0000000..060442d --- /dev/null +++ b/docker-source.sh @@ -0,0 +1,33 @@ +#!/bin/bash +#!/bin/bash [ HOW TO RUN: `source docker-source.sh` ] #NOPRINT +export NAME="docker-pod" #NOPRINT +export DATADIR="`pwd`" #NOPRINT +# source $DATADIR/config #NOPRINT +echo "Loading command aliases... Type 'halp' to see available commands" #NOPRINT +### HALP! How to control your $NAME docker container +alias dkr="sudo docker" + ### [ shortcut to run docker with sudo ] +alias .where="echo $DATADIR" + ### [ show where the current instance activated by init.sh lives ] +alias .cd="cd $DATADIR" + ### [ change working directory to instance folder ] +alias .run="sudo docker run --network=\"host\" -v $DATADIR/data:/root/.parallelcoin -d=true -p 11047:11047 -p 11048:11048 -p 21047:21047 -p 21048:21048 --device /dev/fuse --cap-add SYS_ADMIN --security-opt apparmor:unconfined --name $NAME $NAME" + ### [ start up the container (after building, to restart. for a'.stop'ed container, use '.start') ] +alias .start="sudo docker start $NAME" + ### [ start the container that was previously '.stop'ed ] +alias .stop="sudo docker stop $NAME" + ### [ stop the container, start it again with '.start' ] +alias .enter="sudo docker exec -it $NAME bash" + ### [ open a shell inside the container ] +alias .log="sudo tail -f $DATADIR/data/debug.log" + ### [ show the current output from the primary process in the container ] +alias .build="sudo docker build -t $NAME $DATADIR" + ### [ build the container from the Dockerfile ] +alias .rm="sudo docker rm $NAME" + ### [ remove the current container (for rebuilding) ] +alias .editdkr="nano $DATADIR/Dockerfile" + ### [ edit the Dockerfile ] +alias .editsh="nano $DATADIR/init.sh;source $DATADIR/init.sh" + ### [ edit init.sh with nano then reload ] +alias halp="sed 's/\$NAME/$NAME/g' $DATADIR/init.sh|sed 's#\$DATADIR#$DATADIR#g'|grep -v NOPRINT|sed 's/alias //g'|sed 's/=\"/ \"/g'|sed 's/#/>/g'" +######### hit the 'q' key to exit help viewer <<<<<<<<< \ No newline at end of file diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/docs/binancedexproposal.md b/docs/binancedexproposal.md new file mode 100644 index 0000000..fedcf5e --- /dev/null +++ b/docs/binancedexproposal.md @@ -0,0 +1,290 @@ +# ParallelCoin Binance Dex Listing Proposal +**Project Summary** + +###### Project Name + +ParallelCoin + +###### Your name and position + +David Vennik, Lead Developer and Architect + +###### A one-sentence pitch about your Project + +Open, Programmable, Distributed Business Administration Systems for All + +###### What are you planning to achieve with your project? + +- To build an application development and deployment system for distributed +applications that you can access to whatever level of skill, from simple +scripting and calculation/spreadsheets up to full blown heteregenous, sharded +virtual shared environments. + +- To replace the mess of software repository services and development +environments with one simple system based on Golang, RPC API and CLI +scripting, using the Plan 9 from Bell Labs inspired server-oriented, and +build a set standard libraries to ease the process of learning to develop on the +platform. On ParallelCoin's network, everyone can build tools and easily share +them and fork other people's tools without limitation. + +###### What is the team size now? Please list each core member of the Project team and outline their role and responsibilities. If any project member is involved in other projects, please clearly state their commitment and how they avoid conflict of interest. + +### ParallelCoin Team + +#### Djordje Marcetin +Founder, Webmaster, systems administrator and web application developer with 20 +years of experience, intermediate level Go programmer and Systems +Administrator running our web systems. + +#### David Vennik + +Blockchain and GUI developer with 5 years experience, specialising in protocols, +architecture and ergonomics. 15 years experience as sole-trader entrepreneur in +IT support and training, and life-long passion for computer science. David +is the main programmer working on the current Plan 9 Hard Fork for ParallelCoin + +#### Frank Neumann + +IT Security Consultant with over 20 years of experience, tasked with +business liason and compliance, to now mostly negotiating with +cryptocurrency exchanges. + +#### Dane Zivanovic + +Entrepreneur in construction industry with 12 years experience, Dane is the +cat-herder of our development team. + +#### Cedomir Vukobrat + +Civil Engineer and Draftsman with 20 years experience in project planning, +delivery and legal compliance. Cedomir's industry experience gives us a +concrete, market-based problem to solve - improving inventory, logistics, +market availability and price discovery, facilitating compliance with local +government regulations and presenting the portfolios of construction +industry project management companies. + +#### Project Website + +[https://parallelcoin.info](https://parallelcoin.info) + +###### Share a link to your project’s whitepaper + +[https://github.com/p9c/matrjoska/blob/main/docs/whitepaper.md](https://github.com/p9c/matrjoska/blob/main/docs/whitepaper.md) + +###### Share a link or image of your roadmap + +Insert Roadmap Here + +###### Where are you right now in your roadmap? + +Late Alpha for Plan 9 Hard fork, estimated to be finished beta by July + +###### What is the business model of the Project? + +To facilitate the development of a community of specialists and promote the +adoption of our platform as a means to participate in the decentralised economy. + +ParallelCoin's planned application development environment is not just for +on-line services but includes web but focusing on + +###### What is the potential user base and market opportunity? + +We are aiming to gain adoption especially by small and medium businesses, +assisting them to integrate crypto payments and administration systems into +their existing setup. + +### Token Information & Economics + +###### Token Name and Symbol + +ParallelCoin - DUO + +###### What is the primary use case of your token? + +On-chain service fees for Oracle state update transactions, payments for +users of chain-connected market and exchange services and payments in general. + +###### Where else can your token be used? + +###### What is the total supply of your token? + +Currently at around 220,000, with at least 170,000 known to be out of +circulation either permanently or for some time to come (from exchange hacks). + +The upcoming Plan 9 Hard Fork will recalibrate the chain to emit a total of +1,000,000 tokens and a minor fork planned later upgrades to 256 bit token +denomination precision to account for the small supply. + +###### What is the circulating supply of your token? + +Approximately 240,000 DUO + +###### Please provide a breakdown of current token allocation. + +Tokens have never been allocated on this chain, only mined. An issue of +funds to remunerate the developers and team members and fund the team is +planned to take effect upon the Hard Fork + +###### Please provide a list of team wallets that hold your token. + +Currently only the legacy wallet [https://github.com/p9c/p9/releases/tag/1. +2.0](https://github.com/p9c/p9/releases/tag/1.2.0) + +###### Please provide all lockups in place for any holders of the token. + +We are currently in the process of learning about options for building the +bridge for the token, it is pretty much standard bitcoin JSONRPC2 and so it +should not be complicated to do it, as is the script engine still identical +to bitcoin. + +###### Are you compliant in all jurisdictions that you service and operate in? + +We are not engaging in any fiat gateway services and have currently no +formal investment vehicle, and there is no securitization in our protocol +and none is planned, though such may be part of future projects building on +the development environment we are planning for after the fork. + +###### Is the token currently running on any network other than Binance Chain? + +none at all + +###### Please provide explorer links to each version of the token. + +[https://oldex.parallelcoin.info/](https://oldex.parallelcoin.info/) + +###### Please provide a detailed token migration plan. What mechanism will you be using? + +We intend to have a bridge server that a user can load with their stock of +DUO tokens, add further by deposit, and at the will of the owners of these +tokens, lock them using scripts to enable issuance on the Dex, like how the +Bitcoin Binance Bridge works, using the SDK and based on an existing, proven +bridge system. + +###### What is the exact timeline of opening the swap/bridge to the public? Will you be able to complete this prior to the end of the voting period? If yes, please provide evidence that it the swap/bridge is ready or close to being ready. + +At this point we don't even know where to find the starting point to +building a bridge as the Binance Bridge has no source code released and +other than ethereum bridges we have not yet found anything that can do this. +The binance documentation is unhelpful in this also. + +###### ~~If your tokens currently still sit in only 1-5 addresses, please provide the timing of when they will be fully allocated to users.~~~ + +N/A: tokens are only mined, none to date issued under special consensus +conditions. + +###### Is the token currently listed on any other exchange? What are they? + +[https://dex-trade.com](https://dex-trade.com) - however this is an +untrusted exchange with dubious credentials that we are warning our users to +not use. + +### Funds + +###### Did you conduct an ICO or any type of fundraising? When was it? + +Never, never will, though there will be an issuance upon hard fork +activation to remunerate developers and provide a base fund for promoting +the project. + +~~How much in total did you raise?~~ +~~How much have you spent in total thus far? What have you spent these funds +on?~~ + +###### How much runway does your project require to ship the final product? + +The hard fork has already over 2.5 years of work wrapped up in it and should +be less than 3 months to release. See the timeline above for information +about the development platform project time estimates + +###### What currency/coin/token do you plan to hold your funds in? + +Primarily Bitcoin and other major well known tokens, we don't have a formal +structure, each team member is managing their own capitalisation at this point. + +###### What’s your coin storage/conversion policy? Do you only convert to fiat when you spend? + +We are HODLers... fiat is only for running expenses because the supermarket +hasn't adopted our not-yet-existing business administration system. + +###### How do you plan to publish your spending each month? + +The bridge will have automatic and IPFS-mirrored storage of the ledger of +locks, unlocks, cross-chain migrations. If this question relates to the +spending of investment funds, we have no public investment so there is +nothing to report. + +###### Where are your funds stored? Please provide the addresses for public community tracking. + +We have no formalised structure yet and are waiting on clues how to make our +Bridge and that will be where the funds are managed and + +###### ~~~If you have plans to conduct an IDO, please provide a detailed plan including IDO format, methodology, timeline, fundraising goal and amount of tokens, and sale price.~~~ + +### Development + +###### What are the technological innovations of the project, if there are any? + +We are building a distributed application development environment targeted +at all levels of skill, from rudimentary scripts like in spreadsheets, to +simple logic like shell scripting and seamlessly merged with pure Go code, +upon which we will be building systems to implement and distribute and +secure the data for various Oracles and other databases like for managing +inventory in production lines, distribution and logistics. All businesses of +over about 10 employees has someone who can write simple logic to make +custom tools for a business. We want to put tools in their hands that make +decentralised networks as easy as Excel, Word and Access. + +###### Where are your code repositories located? + +[https://github.com/p9c](https://github.com/p9c) + +The two most important repositories are [matrjoska](https://github.com/p9c/matrjoska) +and [pod](https://github.com/p9c/p9). The former is where current work is +going on while there is a lot of changes in API and restructure/refactoring, +the latter is where the full release will live. + +###### Do you have any products or UI demos can you share publicly? Are any built by your community, as opposed to being in-house demos? + +We have one initial prototype of the planned construction industry +administration system, a calculator that allows a user to inventory all of +the materials and services required for a construction project, and +generating all of the necessary documentation required to submit to planning +authorities. Our team member Cedomir will be managing the curation of the +Oracles that we will build with his knowledge in the field in the region we +intend to deploy first (Serbia). + +##### Do you plan to release weekly progress updates in this forum? + +We are in late stages of finishing a complete suite of web sites amongst +which will be reports and blogs from the team, if it is simple to link the +API we could push the relevant ones to this forum so we don't have +complicated manual work required to propagate the updates. + +##### Do you plan to host monthly or bi-monthly video AMAs? + +Once we have dealt with current ongoing issues with securing sufficient +working capital and the hard fork is completed we will need to promote both +what we are doing and helping users with building stuff on our platform. + +### Competitors + +###### Who do you view as your current competitors/peers? + +As we see it, right now, nobody is building a distributed business +administration system framework. There is centralised services, and there is +specialised, narrow niches such as smart contract development, but we want +to build the tools for everything from the GUI through middleware, deploying +new sidechains and oracles, chat and messaging systems including +collaborative work spaces (like MindMeister and Google Docs). + +So, in short, we don't have competitors right now, in the crypto space, and +we are bringing ideas that have proven themselves from the centralised +software development companies like Google and Microsoft, and putting them +on decentralised systems so they are more secure and resilient. + +###### What is your project’s competitive advantage over existing or potential future solutions? + +The main advantage is that the users can change our systems as much as they +like, to exactly suit their own business, which also creates an opportunity +for people that develop knowledge and skills in using it to become +contractors providing such services. \ No newline at end of file diff --git a/docs/discord.svg b/docs/discord.svg new file mode 100644 index 0000000..3938d42 --- /dev/null +++ b/docs/discord.svg @@ -0,0 +1,71 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/docs/fb.svg b/docs/fb.svg new file mode 100644 index 0000000..6f3cb14 --- /dev/null +++ b/docs/fb.svg @@ -0,0 +1,108 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7eb9e4f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,83 @@ +[![](https://raw.githubusercontent.com/p9c/pod/master/pkg/gui/logo/logo_small.svg)](https://p9c.github.io/pod/) +# ParallelCoin pod + +### all-in-one-everything for parallelcoin + +###### ParallelCoin's Omnibus Depositorie + +###### [Official Website](https://parallelcoin.info) + +wiki: [![](docs/wiki.svg)](https://github.com/p9c/p9/wiki) social: [![](docs/telegram.svg)](https://t.me/ParallelCoinPlan9) , [![](docs/discord.svg)](https://discord.gg/yB9sYmm3cZ) , [![](docs/fb.svg)](https://www.facebook.com/parallelcoin) & [![](docs/twitter.svg)](https://twitter.com/parallelcoinduo) + +# Coming Soon + +[Plan 9 Spore Protocol](https://github.com/p9c/p9/wiki/The-Spore-Protocol) + +# ParallelCoin Legacy + +### ParallelCoin Legacy Specifications + +- Multi-algo proof of work crypto currency. +- Fair release with no premine, no IPO and no ICO. + +|---|---| +| **Algorithms** | SHA256D & SCRYPT | +| **Abbreviation** | DUO | +| **Total Coins** | 1,000,000 | +| **Block Reward** | Blocks 1-998 = 0.02 DUO (Fair Release) / Blocks > 998 = 2 DUO | +| **Block Interval** | 5 minutes | +| **Halving Interval** | 250,000 blocks | +| **P2P Port** | 11047 (21047 Testnet) | +| **RPC Port** | 11048 (21048 Testnet) | + + +### Get Parallelcoin Legacy + +[![github](https://raw.githubusercontent.com/thecreation/brand-icons/master/src/svg/github.svg)](https://github.com/p9c/p9/tree/master/legacy) +[![Windows GUI Binary](https://raw.githubusercontent.com/thecreation/brand-icons/master/src/svg/windows.svg)](https://download.parallelcoin.io/Parallelcoin-qt-v1.2.0.0-Win.zip) +[![Linux GUI](https://raw.githubusercontent.com/thecreation/brand-icons/master/src/svg/linux.svg)](https://github.com/p9c/p9/releases/download/v1.2.0/parallelcoin-qt-x86_64.AppImage) +[![Linux CLI](https://raw.githubusercontent.com/cmedinam/scripts/033106979fc7e58a6d363efe52236ef07a55de08/linux/custom/focal-fossa/usr/share/icons/HighContrast/scalable/places/network-server.svg)](https://github.com/p9c/p9/releases/download/1.2.0/parallelcoind-x86_64.AppImage) + +# Plan 9 from Crypto Space Hard Fork + +The hard fork includes the following new features: + +- **New Proof of Work hash function** - uses very large integer long + division to avert the centralisation of hash power by using + the slowest and most expensive integer mathematical operation + already being most optimally implemented in CPUs - long division. + GPU dividers are slower and no ASIC could do it faster. + +- **9 way parallel prime based block intervals** - The blocks come + in randomly anyway so why not make the schedule semi-random? + This block product scheduler (difficulty adjustment) runs 9 + parallel block schedules using the new hash function where each + different block has a different but regular block time with a + different difficulty target and proportional block reward. + + This allows a broader scale dynamic between small and larger + miners, who have different needs for payment regularity. + The average block time is 36 seconds, which is sufficient + for many retail operation types. + +- **Simple zero configuration multicast mining cluster control + system** - Using a simple pre-shared key it is easy to add nodes + and mining worker machines to a cluster. Simple redundancy by + the use of multicast one can just run one or more controller + nodes and the workers just listen to whoever is also using the + same key. + + A customised live linux USB will be available for + both functions with an easy configuration file readable by + windows for setting the password and payment addresses. + +- **Multi-platform touch-friendly wallet GUI** - A responsive and + simple user interface for making and viewing transactions + as well as an inbuilt block explorer, configuration and mining + controls, available on Windows, Mac, Linux, Android and iOS. + +## Where can I learn more? + +Best place to go is the wiki: [https://github.com/p9c/p9/wiki](https://github.com/p9c/p9/wiki) + +wiki: [![](docs/wiki.svg)](https://github.com/p9c/p9/wiki) social: [![](docs/telegram.svg)](https://t.me/ParallelCoinPlan9) , [![](docs/discord.svg)](https://discord.gg/yB9sYmm3cZ) , [![](docs/fb.svg)](https://www.facebook.com/parallelcoin) & [![](docs/twitter.svg)](https://twitter.com/parallelcoinduo) diff --git a/docs/manjaro.sh b/docs/manjaro.sh new file mode 100644 index 0000000..fac788a --- /dev/null +++ b/docs/manjaro.sh @@ -0,0 +1,20 @@ +#!/bin/bash +cd ~ +yay -Syy wget git base-devel +wget -c https://golang.org/dl/go1.14.13.linux-amd64.tar.gz +sudo rm -rf ~/go +tar zxvf go1.14.13.linux-amd64.tar.gz +cat <> ~/.bashrc +export GOBIN=$HOME/.local/bin +export GOPATH=$HOME +export GOROOT=$HOME/go +export PATH=$HOME/go/bin:$PATH +EOF +mkdir -p src/github.com/p9c +cd src/github.com/p9c +git clone https://github.com/p9c/p9.git +cd pod +source ~/.bashrc +go mod tidy +make -B stroy +stroy guass diff --git a/docs/ontology.png b/docs/ontology.png new file mode 100644 index 0000000..7f9dec5 Binary files /dev/null and b/docs/ontology.png differ diff --git a/docs/ontology.svg b/docs/ontology.svg new file mode 100644 index 0000000..84e6f2b --- /dev/null +++ b/docs/ontology.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/plan9hardfork.md b/docs/plan9hardfork.md new file mode 100644 index 0000000..a1d3b35 --- /dev/null +++ b/docs/plan9hardfork.md @@ -0,0 +1,13 @@ +# ParallelCoin + +## Plan 9 Hard Fork Specifications + +### 1. Hash Function + +A key goal of the Plan 9 Hard Fork is to make a chain that has a very large peer to peer network composed of mostly medium and small miners, in order to reduce the loss of security from mining being under the control of a small number who make up 80% or more of blocks. + +The reason why this aggregation is bad for security is that in any distributed system, the more centralised a section becomes, the more vulnerable the network becomes to extensive cascading failures, or a single lever for an attacker either upon the chain itself or against some set of users. There is an obvious economic issue as well - dominant miners on a network usually also have a lot of reserve tokens and can create the conditions for higher volatility. + +The hash function consists of slicing up the block header in two pieces, shuffling and rearranging the bytes composing the data, squaring the jumbled halves and then multiplying them together, and the most important step, dividing the result with a sliced up version of the original block. + +The process is done twice and then the final data... diff --git a/docs/telegram.svg b/docs/telegram.svg new file mode 100644 index 0000000..dd816bb --- /dev/null +++ b/docs/telegram.svg @@ -0,0 +1,92 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/docs/twitter.svg b/docs/twitter.svg new file mode 100644 index 0000000..630d1c7 --- /dev/null +++ b/docs/twitter.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/docs/whitepaper/whitepaper.md b/docs/whitepaper/whitepaper.md new file mode 100644 index 0000000..210da66 --- /dev/null +++ b/docs/whitepaper/whitepaper.md @@ -0,0 +1,583 @@ +![](https://raw.githubusercontent.com/p9c/logo/main/logo_nocircle128x128.svg) + +# The Parallelcoin Pod White Paper + +###### by + +### David Vennik + +#### April 2021, Novi Sad, Serbia + +--- + +## 0. Introduction + +### Open, Programmable, Distributed Business Administration Systems for All + +The greatest challenge for cryptocurrency and related open and public +distributed network systems is integration, but this is not unique to it, it is +the central criteria that has determined the success of software development +companies since the 1970s. + +In any company with more than a handful of employees, there is likely at least +one user who can at least construct a multi-page spreadsheet with basic sums, +averages and so forth, to generate financial reports and evaluate +projections. This type of capability slides gently towards programming, and +can go quite deep in the direction of allowing the creating algorithms. + +The number one software company in the world for the majority of the last 50 +years is Microsoft, whose key business strategy was in building business data +administration systems that were usable and *customizable* by minimally +trained and skilled users. + +The one thing that Microsoft understood best was that businesses need +interoperability of data and granular depth of access to programmability of the +applications, to fit the business's needs, its personnel, organisational +communication structure and protocols, and the business operators' philosophy. + +Cryptocurrency represents the first and most foundational element of a complete +business network administration system, a financial ledger, that can be trusted +without needing to trust any individual or group participating in the +network. In many projects since the first appearance of Bitcoin, many other +features and programmability other consensus mechanisms for preventing +centralization of control of the ledger. + +## 1. Evolution of Open, Distributed Business Administration Systems + +### 1.1 Bitcoin + +#### Permissionless, Accountless, Trustless, Reliable Distributed Financial Ledger + +Bitcoin was the first distributed database system that is reliable enough to be +trusted while distributing the risk of malicious network participants such that +the cost of bad behaviour is higher than the potential gain, to prevent the +arbitrary issuance of new tokens outside of the consensus network rules laid +down in the source code, which must apply the same protocol rules throughout +the network, or it partitions into multiple networks, damaging the value of +the token and destroying trust in the network. + +### It has confused pundits and experts alike for some time what it really is about: + +- Some focus on its use of artificial scarcity; + +- Some focus on the inherently conservative governance created by a system +with purely staked participation in its development direction, due to their +ability to embargo updates to the protocol, causing a fork and damage to the +interests of all with such stakes at risk; + +- While still others focus on its survivability compared to more brittle +centralised systems, that often have a single point at which failure +destroys the functionality of the entire system. + +#### The one thing all businesses have in common is money + +A business does not necessarily need to participate in these higher +abstractions over top of the distributed ledger; beyond this, into such +things as lending, securitization, and hypothecation, + +Money originally started with the use of compact, portable, and scarce +physical tokens, or divisible and universally marketable materials such as +salt, tobacco, alcohol, and so on, that by its nature prevents a false +record developing, and facilitates the collective creation of a record of +who has benefited who the most, since the amount of tokens reflects the +relative valuation against the product or service it monetizes. + +It is the expense of producing a valid token that gives trust in its ability +to hold and convey the notion of value across an economy. + +In various ways, such as debasement, and outright theft, robbery or +usurious lending i.e.: unilaterally renegotiating interest rates, interest rate +controls or printing claims (the origin of paper money) for the physical +monetary asset that cannot be converted back to the backing asset; such as +what led to the endless series of bank runs in the 19th century. + +Gradually, over the last 1000 years, this has shifted towards some form of +distributed ledger, as well as printed money, invented originally by the +Chinese, which acts as a substitute for coins by embodying the scarcity in a +difficult to reproduce, and recognisable proof of authenticity, however, +generally nearly infinitely cheaper than the long established use of +monetary metals, gold, silver and copper, and like coating ingots of +tungsten with a thin layer of gold to inflate the price it can fetch in +exchange for goods, services, and other currencies. + +The modern monetary system is entirely based on electronic storage of +ledgers that are from central banks, and then further, within the internal +records of private banking organisations. + +The use of the word 'money' in such a context becomes increasingly +inappropriate, as it is more like an invisible version of the divisible +universally marketable materials like salt, alcohol, tobacco, and other +similarly widely used materials. Bitcoin has for this, the Satoshi, 1/100, +000,000 units per 'one' coin, acting like the smallest denomination of a +metallic token. + +#### Really, this has long been the age of the distributed ledger, well before 2009. + +The greatest source of power within a distributed ledger is that of authorship. +Who has the authority to cause a particular record to become canonical and thus +immutable, and provide the service thereby of a stable base on which to do +economic calculations when deciding how to allocate working capital in a +business... because that common data affects every decision even if only in a +subtle way? + +At the centre of the proposal for the protocol proposed by the pseudonymous +Satoshi Nakamoto was the idea of creating a distributed lottery for the right to +be the author of a batch of records in this financial ledger, thus distributing +the risk of false records being made canonical, and using probability limit +supply and to progressively raise the cost of having an altered record +become trusted. + +### 1.2 Ethereum + +#### Programmable Distributed Database Application Layer + +The programmability tends to be the attention grabbing part of the description +of this system, but it is not this that makes it both an innovation, and the +basis for further refinements and expansions in the scope of what kinds of data +with monetary value can be stored. + +But what is more important is also more subtle - it is about allowing the +creation of arbitrary new recompositions of user input and existing data stored +in the financial database and other databases. The first such other database +is the Gas token, that evaluates code by its processing costs and data +weight to ensure that blocks are produced within a limited span, and the +network cannot be gridlocked by spammy processing, the first and most +important of the many little databases, *other than a financial ledger*, +that an ethereum node stores. + +The emerging downside of the technology is that the encoding of application code +into immutable blockchains is quite expensive in space, and further taxes the +serial processing nature of the Nakamoto Consensus upon which it has been +unable to completely separate itself (due to the unarguably extremely strong +safety of transactions beyond a certain age). The project has been +attempting to switch to stake-based systems but the users, whose systems run +the protocol, have resisted this because of its centralization potential. + +### 1.3 Tendermint and Cosmos + +#### Separating Consensus and Execution + +The Tendermint protocol is a derivative of Practical Byzantine Fault Tolerance, +a distributed database protocol first proposed in the ACM +[Practical Byzantine fault tolerance](https://dl.acm.org/doi/10.5555/296806.296824) +paper by Miguel Castro and Barbara Liskov. + +The refinement Tendermint adds is the ability to alter the database of certified +proposers, since the original design assumes that there is no malicious nodes, +which is a dangerous assumption in an open, or as the correct way to describe +it, fluid-membership consortium with both automated and human-operated methods +of preventing the altering of the data, and the finalization of the records +immediately after publication and before the next batch is processed, called ' +Immediate Finality'. + +Like all distributed network systems, it is impossible to solve every problem +perfectly, there is always one or more element that has a limitation that +provides the ability to give strong guarantees about the other, mutually +exclusive properties in the protocol. + +It has been proving to be an innovation that cannot stand by itself, and the +reason why staked systems have become more common is only because of their +exchangeabilty with tokens generated through the use of energy to perform the +mining of new tokens with Proof of Work. + +However, its utility especially for consortiums of independent, usually +competing network node operators, is now allowing it to become an accepted +and trusted mechanism for distribution of the new tokens minted to both +provide security and incentives for innovation in the space. + +A key element of the Tendermint design is that it separates the validation from +the replication. This creates an agnostic base level to the protocol that can be +then used to carry any kind of data in a batched, cryptographically chained +block format, storing data beyond simple monetary, into market intelligence +and coordination data. + +This greatly simplified the process of building a new protocol - it was not +necessary to build a scripting engine into the server, but instead to be able to +write it in a language that compiles down to fast binary code, and the time cost +of taking a database application and turning it into a distributed database +application has been greatly reduced, and rolling out a new network is often +just a matter of a some configuration and a small amount of extra +application layer code, leading to a proliferation of specialised networks, +and further, the same basic principles have led to Polkadot, which mainly is +distinguished by its different primary implementation language, but equally +implementation agnostic and implementable in other languages as suits the +project's access to the programming labor pool. + +#### Finality + +At the very head of a blockchain whose head is chosen by the highest cumulative +total of work along its path, there is the possibility and both malicious and +accidental, network- and machine- failure caused corruption of blocks that can +be mined on prior blocks in parallel, and it is only the high probability of the +majority of miners being honest enough to maintain the value of the network. + +The necessary cost of a delay before strong confirmation of a transaction +when using a probabalistic security model as used in the Nakamoto Consensus +makes the operation of applications with a lower latency requirement +impossible. + +The most extreme form is realtime massive multiplayer games, which +further add the existence of multiple parallel 'spaces'. Limiting a whole +ecosystem to the capacity of one, however high spec, server machine, greatly +limits the possible applications where updates need to be more timely. + +By making transactions final shortly after their initial publication, the +data can be used more quickly and settlement is effectively instantaneous, +within the timespan required to produce the consensus that is considered to +be finality by network participants. + +Further, the easy deployment of new chains gets around the serial nature of +a network consensus by dividing the consensus by chains, which has proven +far more viable than attempts to shard the ledger into arbitrarily related +elements that can then run in parallel. As such, the number of chains based +on this and similar types of consensus protocol has exploded rapidly since +it was brought to market. + +#### Transportability of Ledgers + +Where Ethereum and other chains have considered sharding, in order to +parallelise the processing of transactions, the Cosmos project launched the +concept of instead making a chain for the application, and allowing a third +layer to the distributed system, where the ledgers are able to exchange between +multiple networks transparently, tokens are locked on one and proof of the +lock is used to issue new, identical tokens on the other chain. + +Parallelcoin is building a unified application environment that allows +programmability at all levels of the system, from protocol to interface, +without the cost of serial processing, adding the ability to move more and +more of the chain data to more transient stores, reducing blockchain data +weight and providing a greater variety of ways to participate in the network. + +## 2. The Parallelcoin Pod + +The central concept behind the Pod can be expressed in well known eastern +european Matryoshka dolls. Everything in the system has a container type that +can carry a message intended for a different processing system, with the only +common limitation of requiring a public key or address, and the signature on it +to prove authenticity and/or privilege to do so. + +This is essentially the same as the Tendermint model, in that transactions are +generalised, and the validation is separate though the validator governs the +re-distribution of a received message or its processed product. + +Where Pod goes further is not in only the base layer, but rather in a holistic +view of the application starting from the user input and the GUI down to the +network protocol. + +Most blockchain wallets have some kind of RPC console built into them, and in +this console, with varying degrees of interface sophistication, one can query +the blockchain's databases and get back results. + +Hard-core, early adopters originally accessed the chains only this way to begin +with, but the sophistication of these interfaces has been pretty much halted at +a level that is far below that of even basic CLI interfaces like the `sh` +command interpreter. + +###### Even young children can construct a basic script + +It could even be argued that in fact, narrative, the serialisation of a series +of expressions into a coherent message that conveys some kind of information is +the primary skill and activity of the human brain, in the processing of +language. + +In order to perform any task, a person must first learn the program of the task. +What things come first, which things can occur in parallel or have non-competing +delays, and the productivity of a worker exactly relates to their ability to +learn to perform the task in the way that straightens out the nebulous cloud of +requirements into a directed acyclic graph showing the various steps in the +process and includes parts where parallelization is visible, such as waiting for +dough to rise, a mopped floor to dry, or a solution to settle out solids. + +###### From small, open things, great and complex things grow + +Not every need of a person wanting to keep business records or conduct +transactions of business records with another business is complicated. The web +has exploded in the main due to the fact that it exactly allows aggregation of +data. The number one business being search, since any data set's backbone is the +index hash that all records bear, without it, you simply cannot find it. +Someone has to build that index and it is a utility that is universal to all +business. + +By making scripting, and eventually programming, from network protocol to GUI, +accessible to the user only limited by their technical skill, and making the +blockchain a first class citizen in the programming environment, the Pod will do +for cryptocurrency what Microsoft did for word processing, spreadsheets, +email, calendars and databases. + +### 2.1 Phase 1: The Parallelcoin Plan 9 Hard Fork + +The first step in the process is capitalization. With the strong resistance +from the de facto monopoly cartel of banking institutions to cryptocurrency, +the use of securitization to fund development has been stifled. However, in +almost all cases, the core value proposition of all cryptocurrencies comes +from the innovations of the development teams who build them. + +Thus, in effect, cryptocurrencies have been functioning as substitutes for +equity; many different approaches have been tried, such as Initial Coin +Offerings and other methods of initial distribution of part of the supply of +a token in exchange for funding of the development of the network, leading +to the capital required to fund development of further improvements and the +marketing of the network for its utilities. + +The features that are coming in the Plan 9 from Crypto Space Hard Fork include: + +- #### CPU oriented proof of work + that depends on very large integer long division as a bottleneck step, thus + proportionally disadvantaging generic, programmable and custom silicon + production that is not specialised for this expensive, iterative mathematical + operation, and more scarce and thus narrowing the decentralisation, and + thus security, caused by this concentration, as seen in the established + ASIC based PoW coins. + +- #### 9 Parallel Block Intervals for more precise supply control and lower latency + + The block schedule is composed of 9 parallel schedules based on powers of 2, + which each provide a reward that is in proportion with their consensus target + average, with the base interval at 18 seconds, producing a resultant + average of 9 seconds between blocks. + + This provides a more accurate census of the active miner population, + needed for accurate difficulty adjustment, than a single interval, as each + new block on any of the intervals also triggers the recomputing of the + targets of the other intervals, without further complicating the data in + the on-chain consensus as in Real Time Targets, it is derived through + averaging of the history only, over several common and interval-specific + spans extending from 18 seconds up 2304 seconds or 38.4 minutes - + producing a multi-factor averaging that spans a total + timespan of around 96 days. + + ##### Orphan Minimization + + Because the block time is short, many orphans are mined. To combat this, + in addition to the standard cumulative work total, (the lowest sum of all + block hashes in the chain path), the value of the block is a criteria that + modulates the fork evaluation. + + In addition, with varying block rewards, it is natural that hash power + attacks on the difficulty adjustment will target the longest intervals, + and thus be more likely to be the blocks of miners performing hashpower + attacks. + + When these lower reward blocks are added to the head of the chain, the + difficulty of the higher reward blocks is recomputed, ensuring that a + spike in numbers of these blocks is reined in quickly, providing the + benefit of more frequent adjustments with an optimised cost for the + network value (dilution of currency). + + Thus, rather than only determining the best block by work, it is the work + of the most frequent blocks that get priority position to join + the chain, in proportion with their target and historical frequency, by + expanding the work value formula to add an equal percentage to the work + inverse of the relative times of the blocks. + + So all else being exactly equal, the lowest value block is chosen by a factor + the percentage out of 256, the maximum block interval's differential, up + to 100%. This thins out the number of orphans that can form a fork in an + economic way for the holders of the coins, specifically, and favours also + a fairer distribution amongst small miners of the most frequently occuring + block versions. + +- #### Zero configuration multicast local area network + + Adoption is the key goal of Plan 9 Crypto, and participating in the mining + of the base token is something we want to see as wide as possible. More + blocks means more wide distribution of tokens as well as improving the + liveness of the network (time between updates). + + To facilitate this, configuring a standard mining farm is as easy as + deploying a wireless network, a pre-shared key acts as both signal + security and channel separation for the miners, and jobs are streamed + regularly in advertisments for miners to come online and start work on blocks. + +- #### Multiplatform GUI wallet built in pure Go + + Go is a programming language generally associated with scalable + distributed networked database systems, and has had quite wide adoption in + the cryptocurrency space. It has a key advantage over the other languages used + in that it has a cooperative, work stealing scheduler that coordinates + lightweight threads communicating over atomic FIFO queues called 'channels'. + + This programming model, known as Concurrent Sequential Processes, allows + extremely low latency responses to data processing with widely varying and + unpredictable volume and frequency. This makes it great for dividing up + fluctuating loads of processing in parallel, but also makes it easy to + build user interfaces that represent many concurrent processes. + + The Pod is a single binary but contains run modes for all functions + it performs, that run as independent processes, sharing data with other + components over sockets and pipes. + + The GUI interface is designed to be usable and efficient when used on any + device, whether it be a mobile phone, tablet, laptop, or desktop computer. + Given the necessary limitations of size, all functions are accessible on + all platforms. + +## 2.2 Phase 2: You will be assimilated! + +### The Pod Console + +The key technology that is planned to be developed once the new protocol is +in place, and the hard fork activated is an open, accessible and intuitive +system for creating scripts, and eventually full programming logic, that +treats command line style invocations as equal to Go statements and +expressions, and facilitating grouping these lists into functions, +automating the specification of variables, parameters and return values. + +This starts out as a simple RPC console, as will be found in the Hard Fork +GUI wallet, and stepwise adds features to it that allow users to build +applications and supporting libraries and servers. Between an RPC API call +and a Go function invocation there is a 1:1 correspondence between the +variables involved and input values and their types, and so, recognising +this, the Pod Console will allow anyone to easily create simple queries +combining multiple data sources and produce a report that can be updated +from data connected to the chain. + +#### Build anything + +Rather than separating end user and server and peer side programming as is +convention in the industry, with, for example with ethereum, the server +written in C++, Go or whatever other language, while the scripts are written +in Solidity, the Pod Console allows you to use the same language to define +everything from the user interface to the format of protocol packets in a +peer to peer data aggregation application that can be deployed with a simple +command line invocation or automated through a GUI interface. + +This programming environment natively deals in a log format consisting of +the actions that can be immediately executed or deferred, grouped into +functions, functions grouped into a package and either an application or +supporting library created to facilitate the building of more applications. +Not just smart contracts but smart user interfaces as well. + +The source code that is generated out of the log of commands input by the +user and structured into functions, types, and executable programs is stored +in its original Directed Acyclic Graph form, which can then be played back +like a script to produce the state of the application source code at a given +point, like a commit in a Git repository. + +These logs are bundled into updates on IPFS stores and by URLs including +version specifications, once an application is developed and tested, it can +be then immediately upgraded or installed by other nodes on the network. + +#### Ad-hoc integration with external data sources + +Since any code that can be integrated with Go can be used in this +environment, it then becomes possible to create interfacing layers that +encapsulate foreign server applications whether in Go, in other languages or +only by their binaries. + +From this basis, applications can require access to these services, which +can then be automatically installed and once synchronised and ready for +queries, new services can be created that depend on multiple data sources. + +#### Converting data sources into an Oracle + +Unlike Tendermint/Cosmos, these data sources do not necessarily have to be +other, deterministic types of sources, ie blockchains, by building a +consensus around the external data source, and thus producing a trustworthy +ledger of third party information that can be used as a data source in place +of the third party's own outlets. + +Once the data is certified by the oracle, it becomes deterministic, shared +data for applications to connect this data feed to blockchain based +applications. + +### Bootstrapping + +The first phase of developing the Pod Console aims to first quickly make the +console itself stored in the format used by the console and editable. +Similar to the bootstrap step in compiler construction, this step enables +the platform to quickly advance to being changed to suit how it suits being +changed. + +The first milestone is to have created a native Console Log that builds the +Console. + +Once the system understands itself, it can be rapidly extended in any +direction whatsoever. It will have a scheme for converting between API/CLI +call format, and the native Go function syntax that is then compiled to be +executed, and the user can chain lists of commands in any of the implemented +parameter syntax formats, symbols given names, functions and types grouped +together automatically and turned into Go packages that can then be shared +immediately over IPFS. + +From this point onwards, Plan 9 Crypto will pivot to focus on developing the +platform with examples, integrated documentation, peer to peer hosting of +source code and assets and one or several proof of concept projects. + +### Proof of Concept 1: Inventory, Logistics and Compliance for Construction Industry + +An application part way into development with a basic demonstration allows +the creation of public records relating to materials, sources of materials, +market current offered inventory, and automated generation of documentation +required for compliance with government regulations to streamline compliance +and reduce compliance administration costs to the minimum possible, leaving +more capital aside to apply to other business purposes. + +Through the application of the principle of designing protocols that +encapsulate, composit and present data from one or many sources, the detail +level of the data can eventually reach a point where it becomes effectively +an authoritative resource on the information about everything related to +governance of the industry and the facilitation of trade and discovery of +profit opportunities and cost-savings opportunities that better connections +can provide. + +#### Implementation Overview + +In order to implement this system, there is the creation of notary services, +which certify and host the certified content on IPFS content addresses, +built on several extensions to the ParallelCoin ledger for creating a market +in notary services by providing a strongly immutable record of these +notarisations. + +The system contains three sections: + +1. Realtime market data, offers for goods and services, offered available + inventory, prices and payment methods + +2. Storage of public data required for legal compliance related to projects + including all of the technical specifications each different regulation + requires, with automated feeding of the data from inputs into the market + system and the settlements of these activities providing transparency and + compliance to users of the system operating in the industry. + +3. A platform for the certifiable record of the complete projects from + inception, to the parties involved, the physical locations, the materials + used, the plans and engineering specifications, and to whatever degree + desired, additional media such as video, 3d models, photographs, text and + composite documents containing all of the above. + +The Plan 9 Hard Fork protocol will be extended with several important +features required for this service: + +- Schnorr multisignature Public Key Infrastructure + + In one step using Schnorr signatures instead of Elliptic Curve signatures, + the data weight of multisignature transactions is reduced to a uniform 32 + bytes, and facilitates the creation of a masternodes system that creates + block finalisation transactions from the given consensus masternode set. + +- Mineable, exchangeable masternode tokens with a regular emission rate that + can be traded and deployed by masternode operators to sign the + certifications that allow the network to 100% finalise transactions, + within an average of 9 seconds of their publication. + +- Integration with multiple naming services to facilitate easier access to + resources for humans while solving the sybil attack problem through + enforced scarcity, a consensus database of names and user-configurable + metadata used to access the named resources. + +- Development of simple form GUI interface systems that combined with wallet + keychain management enable the creation of data entry for updating + inventory, service availability, and other elements of the 3 tier + architecture of the system, such as browsing market offers, compiling + historical data into charts, confidential communications between + businesses and secure mutually accessible related data storage and backup, + and so on. + +## Proof of Concept Zero: Codename Jorm + +In the preliminary work required before the development of Proof of Concept 1, as a testbed and initial implementation, the same protocol upgrades will be first deployed to create an oracle that aggregates many sources of data relating to cryptocurrencies. + +This system will then become a service that can be accessed from any other application to retrieve data that can be trusted to be identical to the original source, and thus as a historical ledger of information updates, an Oracle that can be used for all distributed applications needing access to this data. + +The second step is creating interfaces that bridge between the Oracle and the write-side of these data sources, subsuming the entire space into a portal that can be come a one-stop-shop for all information and eventually building interfaces that allow the arbitrary merging of services and data sources into a new application composition. diff --git a/docs/wiki.svg b/docs/wiki.svg new file mode 100644 index 0000000..288ad39 --- /dev/null +++ b/docs/wiki.svg @@ -0,0 +1,102 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..312b786 --- /dev/null +++ b/go.mod @@ -0,0 +1,45 @@ +module github.com/p9c/p9 + +go 1.16 + +require ( + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc + github.com/VividCortex/ewma v1.2.0 + github.com/aead/siphash v1.0.1 + github.com/akavel/rsrc v0.10.2 + github.com/atotto/clipboard v0.1.4 + github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd + github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8 + github.com/btcsuite/goleveldb v1.0.0 + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 + github.com/chromedp/cdproto v0.0.0-20210429002609-5ec2b0624aec + github.com/chromedp/chromedp v0.7.1 + github.com/conformal/fastsha256 v0.0.0-20160815193821-637e65642941 + github.com/davecgh/go-spew v1.1.1 + github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 + github.com/gookit/color v1.4.2 + github.com/hpcloud/tail v1.0.0 // indirect + github.com/jackpal/gateway v1.0.7 + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 + github.com/kkdai/bstream v1.0.0 + github.com/marusama/semaphore v0.0.0-20190110074507-6952cef993b2 + github.com/niubaoshu/gotiny v0.0.3 + github.com/programmer10110/gostreebog v0.0.0-20170704145444-a3e1d28291b2 + github.com/tstranex/gozmq v0.0.0-20160831212417-0daa84a596ba + github.com/tyler-smith/go-bip39 v1.1.0 + github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 + go.etcd.io/bbolt v1.3.5 + go.uber.org/atomic v1.7.0 + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b + golang.org/x/exp v0.0.0-20210503015746-b3083d562e1d + golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb + golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sys v0.0.0-20210503073744-b6777538623b + golang.org/x/text v0.3.6 + golang.org/x/tools v0.1.0 + gopkg.in/fsnotify.v1 v1.4.7 // indirect + gopkg.in/src-d/go-git.v4 v4.13.1 + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + lukechampine.com/blake3 v1.1.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3a4f380 --- /dev/null +++ b/go.sum @@ -0,0 +1,245 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8 h1:nOsAWScwueMVk/VLm/dvQQD7DuanyvAUb6B3P3eT274= +github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= +github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJGQE= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/chromedp/cdproto v0.0.0-20210429002609-5ec2b0624aec h1:dbdRUO+gH+jrbL7Q2ZFhhBe4I5JWXOXk5bBcGdWi2ts= +github.com/chromedp/cdproto v0.0.0-20210429002609-5ec2b0624aec/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= +github.com/chromedp/chromedp v0.7.1 h1:OWS/1a82SDXccBBnKXtrud/adgQvaQYArCEwEs5l6Ws= +github.com/chromedp/chromedp v0.7.1/go.mod h1:OOJJ9XkdOAphY+9ptamjez84TxBLBkAMVp9mZ/mkOfk= +github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/conformal/fastsha256 v0.0.0-20160815193821-637e65642941 h1:rOVcN552l7af5e6si8Wdd574TTEaBP6xqHiF7T1ZWsU= +github.com/conformal/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:L/DvjsI5Fhg+SLf++bxzYa06pZd1fwtOEm7CSFSmtjo= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 h1:D22EM5TeYZJp43hGDx6dUng8mvtyYbB9BnE3+BmJR1Q= +github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0-rc.5 h1:QOAag7FoBaBYYHRqzqkhhd8fq5RTubvI4v3Ft/gDVVQ= +github.com/gobwas/ws v1.1.0-rc.5/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackpal/gateway v1.0.7 h1:7tIFeCGmpyrMx9qvT0EgYUi7cxVW48a0mMvnIL17bPM= +github.com/jackpal/gateway v1.0.7/go.mod h1:aRcO0UFKt+MgIZmRmvOmnejdDT4Y1DNiNOsSd1AcIbA= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= +github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/marusama/semaphore v0.0.0-20190110074507-6952cef993b2 h1:sq+a5mb8zHbmHhrIH06oqIMGsanjpbxNgxEgZVfgpvQ= +github.com/marusama/semaphore v0.0.0-20190110074507-6952cef993b2/go.mod h1:TmeOqAKoDinfPfSohs14CO3VcEf7o+Bem6JiNe05yrQ= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/niubaoshu/gotiny v0.0.3 h1:aUt+fvr8nQmitT6XqwuBH8JUQz7QyS4A+KyCNXSesGc= +github.com/niubaoshu/gotiny v0.0.3/go.mod h1:QdEauSzqdF5tbLIVtGYO6sqOhUKVPSZGd5x7xK5oeS4= +github.com/niubaoshu/goutils v0.0.0-20180828035119-e8e576f66c2b h1:T7vmCmpGIvqlOOp5SXatALP+HYc/40ZHbxmgy+p+sN0= +github.com/niubaoshu/goutils v0.0.0-20180828035119-e8e576f66c2b/go.mod h1:aDwH4aWrEBXw/uvtSvwNwxdtnsx++aP8c8ad4AmlRCg= +github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/programmer10110/gostreebog v0.0.0-20170704145444-a3e1d28291b2 h1:gb6u48DzkRwDpNtqaQ+SQYNJ8G3epwf9uJHxtKXKHec= +github.com/programmer10110/gostreebog v0.0.0-20170704145444-a3e1d28291b2/go.mod h1:zSCZczSNxET3dzUjgsrViwmMCj8MRUw0bpEL+k7+IPE= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tstranex/gozmq v0.0.0-20160831212417-0daa84a596ba h1:Xy5Ak7HEazhrOLJo3iQa8hG5REvye7hUkVvKyMASSZA= +github.com/tstranex/gozmq v0.0.0-20160831212417-0daa84a596ba/go.mod h1:6E8YOlY+lYyclnr3Kpc47BR2txQO68xhuECzUj/jSao= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k= +github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20210503015746-b3083d562e1d h1:GVdk3fETn7OIrVsO9oikNOn5qWu8EcgkTtcDD9IGyQ4= +golang.org/x/exp v0.0.0-20210503015746-b3083d562e1d/go.mod h1:MSdmUWF4ZWBPSUbgUX/gaau5kvnbkSs9pgtY6B9JXDE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk= +golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210503073744-b6777538623b h1:JWC69xVWHab8bjJYC2FN1ueBMws7//XKzN4wy7XQoso= +golang.org/x/sys v0.0.0-20210503073744-b6777538623b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/blake3 v1.1.5 h1:hsACfxWvLdGmjYbWGrumQIphOvO+ZruZehWtgd2fxoM= +lukechampine.com/blake3 v1.1.5/go.mod h1:hE8RpzdO8ttZ7446CXEwDP1eu2V4z7stv0Urj1El20g= diff --git a/pkg/addrmgr/addrmanager.go b/pkg/addrmgr/addrmanager.go new file mode 100644 index 0000000..fa6a1b9 --- /dev/null +++ b/pkg/addrmgr/addrmanager.go @@ -0,0 +1,997 @@ +package addrmgr + +import ( + "container/list" + crand "crypto/rand" // for seeding + "encoding/base32" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "math/rand" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// AddrManager provides a concurrency safe address manager for caching potential peers on the bitcoin network. +type AddrManager struct { + mtx sync.Mutex + PeersFile string + lookupFunc func(string) ([]net.IP, error) + rand *rand.Rand + key [32]byte + addrIndex map[string]*KnownAddress // address key to ka for all addrs. + addrNew [newBucketCount]map[string]*KnownAddress + addrTried [triedBucketCount]*list.List + started int32 + shutdown int32 + wg sync.WaitGroup + quit qu.C + nTried int + nNew int + lamtx sync.Mutex + localAddresses map[string]*localAddress +} +type serializedKnownAddress struct { + Addr string + Src string + Attempts int + TimeStamp int64 + LastAttempt int64 + LastSuccess int64 + // no refcount or tried, that is available from context. +} +type serializedAddrManager struct { + Version int + Key [32]byte + Addresses []*serializedKnownAddress + NewBuckets [newBucketCount][]string // string is NetAddressKey + TriedBuckets [triedBucketCount][]string +} +type localAddress struct { + na *wire.NetAddress + score AddressPriority +} + +// AddressPriority type is used to describe the hierarchy of local address routeable methods. +type AddressPriority int + +const ( + // InterfacePrio signifies the address is on a local interface + InterfacePrio AddressPriority = iota + // BoundPrio signifies the address has been explicitly bounded to. + BoundPrio + // UpnpPrio signifies the address was obtained from UPnP. + UpnpPrio + // HTTPPrio signifies the address was obtained from an external HTTP service. + HTTPPrio + // ManualPrio signifies the address was provided by --externalip. + ManualPrio +) +const ( + // needAddressThreshold is the number of addresses under which the address manager will claim to need more + // addresses. + needAddressThreshold = 1000 + // dumpAddressInterval is the interval used to dump the address cache to + // disk for future use. + dumpAddressInterval = time.Minute * 5 + // triedBucketSize is the maximum number of addresses in each tried address bucket. + triedBucketSize = 256 + // triedBucketCount is the number of buckets we split tried addresses over. + triedBucketCount = 64 + // newBucketSize is the maximum number of addresses in each new address bucket. + newBucketSize = 64 + // newBucketCount is the number of buckets that we spread new addresses over. + newBucketCount = 1024 + // triedBucketsPerGroup is the number of tried buckets over which an address group will be spread. + triedBucketsPerGroup = 8 + // newBucketsPerGroup is the number of new buckets over which an source address group will be spread. + newBucketsPerGroup = 64 + // newBucketsPerAddress is the number of buckets a frequently seen new address may end up in. + newBucketsPerAddress = 8 + // numMissingDays is the number of days before which we assume an address has vanished if we have not seen it + // announced in that long. + numMissingDays = 30 + // numRetries is the number of tried without a single success before we assume an address is bad. + numRetries = 3 + // maxFailures is the maximum number of failures we will accept without a success before considering an address bad. + maxFailures = 10 + // minBadDays is the number of days since the last success before we will consider evicting an address. + minBadDays = 7 + // getAddrMax is the most addresses that we will send in response to a getAddr (in practise the most addresses we + // will return from a call to AddressCache()). + getAddrMax = 2500 + // getAddrPercent is the percentage of total addresses known that we will share with a call to AddressCache. + getAddrPercent = 23 + // serialisationVersion is the current version of the on-disk format. + serialisationVersion = 1 +) + +// updateAddress is a helper function to either update an address already known to the address manager, or to add the +// address if not already known. +func (a *AddrManager) updateAddress(netAddr, srcAddr *wire.NetAddress) { + // Filter out non-routable addresses. Note that non-routable also includes invalid and local addresses. + if !IsRoutable(netAddr) { + return + } + addr := NetAddressKey(netAddr) + ka := a.find(netAddr) + if ka != nil { + // TODO: only update addresses periodically. + // + // Update the last seen time and services. note that to prevent causing excess garbage on getaddr messages the + // netaddresses in addrmanager are *immutable*, if we need to change them then we replace the pointer with a new + // copy so that we don't have to copy every na for getaddr. + if netAddr.Timestamp.After(ka.na.Timestamp) || + (ka.na.Services&netAddr.Services) != + netAddr.Services { + naCopy := *ka.na + naCopy.Timestamp = netAddr.Timestamp + naCopy.AddService(netAddr.Services) + ka.na = &naCopy + } + // If already in tried, we have nothing to do here. + if ka.tried { + return + } + // Already at our max? + if ka.refs == newBucketsPerAddress { + return + } + // The more entries we have, the less likely we are to add more. likelihood is 2N. + factor := int32(2 * ka.refs) + if a.rand.Int31n(factor) != 0 { + return + } + } else { + // Make a copy of the net address to avoid races since it is updated elsewhere in the addrmanager code and would + // otherwise change the actual netaddress on the peer. + netAddrCopy := *netAddr + ka = &KnownAddress{na: &netAddrCopy, srcAddr: srcAddr} + a.addrIndex[addr] = ka + a.nNew++ + // XXX time penalty? + } + bucket := a.getNewBucket(netAddr, srcAddr) + // Already exists? + if _, ok := a.addrNew[bucket][addr]; ok { + return + } + // Enforce max addresses. + if len(a.addrNew[bucket]) > newBucketSize { + a.expireNew(bucket) + } + // Add to new bucket. + ka.refs++ + a.addrNew[bucket][addr] = ka + // T.F("added new address %s for a total of %d addresses", addr, + // a.nTried+a.nNew, + // ) +} + +// expireNew makes space in the new buckets by expiring the really bad entries. If no bad entries are available we look +// at a few and remove the oldest. +func (a *AddrManager) expireNew(bucket int) { + // First see if there are any entries that are so bad we can just throw them away. otherwise we throw away the + // oldest entry in the cache. Bitcoind here chooses four random and just throws the oldest of those away, but we + // keep track of oldest in the initial traversal and use that information instead. + var oldest *KnownAddress + for k, v := range a.addrNew[bucket] { + if v.isBad() { + T.F("expiring bad address %v", k) + delete(a.addrNew[bucket], k) + v.refs-- + if v.refs == 0 { + a.nNew-- + delete(a.addrIndex, k) + } + continue + } + if oldest == nil { + oldest = v + } else if !v.na.Timestamp.After(oldest.na.Timestamp) { + oldest = v + } + } + if oldest != nil { + key := NetAddressKey(oldest.na) + T.F("expiring oldest address %v", key) + delete(a.addrNew[bucket], key) + oldest.refs-- + if oldest.refs == 0 { + a.nNew-- + delete(a.addrIndex, key) + } + } +} + +// pickTried selects an address from the tried bucket to be evicted. We just choose the eldest. +// +// Bitcoind selects 4 random entries and throws away the older of them. +func (a *AddrManager) pickTried(bucket int) *list.Element { + var oldest *KnownAddress + var oldestElem *list.Element + for e := a.addrTried[bucket].Front(); e != nil; e = e.Next() { + ka := e.Value.(*KnownAddress) + if oldest == nil || oldest.na.Timestamp.After(ka.na.Timestamp) { + oldestElem = e + oldest = ka + } + } + return oldestElem +} +func (a *AddrManager) getNewBucket(netAddr, srcAddr *wire.NetAddress) int { + // bitcoind: + // doublesha256(key + sourcegroup + int64(doublesha256(key + group + + // sourcegroup))%bucket_per_source_group) % num_new_buckets + data1 := []byte{} + data1 = append(data1, a.key[:]...) + data1 = append(data1, []byte(GroupKey(netAddr))...) + data1 = append(data1, []byte(GroupKey(srcAddr))...) + hash1 := chainhash.DoubleHashB(data1) + hash64 := binary.LittleEndian.Uint64(hash1) + hash64 %= newBucketsPerGroup + var hashbuf [8]byte + binary.LittleEndian.PutUint64(hashbuf[:], hash64) + data2 := []byte{} + data2 = append(data2, a.key[:]...) + data2 = append(data2, GroupKey(srcAddr)...) + data2 = append(data2, hashbuf[:]...) + hash2 := chainhash.DoubleHashB(data2) + return int(binary.LittleEndian.Uint64(hash2) % newBucketCount) +} +func (a *AddrManager) getTriedBucket(netAddr *wire.NetAddress) int { + // bitcoind hashes this as: + // doublesha256(key + group + truncate_to_64bits(doublesha256(key)) % + // buckets_per_group) % num_buckets + data1 := []byte{} + data1 = append(data1, a.key[:]...) + data1 = append(data1, []byte(NetAddressKey(netAddr))...) + hash1 := chainhash.DoubleHashB(data1) + hash64 := binary.LittleEndian.Uint64(hash1) + hash64 %= triedBucketsPerGroup + var hashbuf [8]byte + binary.LittleEndian.PutUint64(hashbuf[:], hash64) + data2 := []byte{} + data2 = append(data2, a.key[:]...) + data2 = append(data2, GroupKey(netAddr)...) + data2 = append(data2, hashbuf[:]...) + hash2 := chainhash.DoubleHashB(data2) + return int(binary.LittleEndian.Uint64(hash2) % triedBucketCount) +} + +// addressHandler is the main handler for the address manager. +// +// It must be run as a goroutine. +func (a *AddrManager) addressHandler() { + T.Ln("starting address handler") + dumpAddressTicker := time.NewTicker(dumpAddressInterval) + defer dumpAddressTicker.Stop() +out: + for { + select { + case <-dumpAddressTicker.C: + T.Ln("saving peers data") + a.savePeers() + case <-a.quit.Wait(): + break out + } + } + a.savePeers() + a.wg.Done() + T.Ln("address handler done") +} + +// savePeers saves all the known addresses to a file so they can be read back in at next run. +func (a *AddrManager) savePeers() { + a.mtx.Lock() + defer a.mtx.Unlock() + // First we make a serialisable datastructure so we can encode it to json. + sam := new(serializedAddrManager) + sam.Version = serialisationVersion + copy(sam.Key[:], a.key[:]) + sam.Addresses = make([]*serializedKnownAddress, len(a.addrIndex)) + i := 0 + for k, v := range a.addrIndex { + ska := new(serializedKnownAddress) + ska.Addr = k + ska.TimeStamp = v.na.Timestamp.Unix() + ska.Src = NetAddressKey(v.srcAddr) + ska.Attempts = v.attempts + ska.LastAttempt = v.lastattempt.Unix() + ska.LastSuccess = v.lastsuccess.Unix() + // Tried and refs are implicit in the rest of the structure and will be worked out from context on + // deserialisation. + sam.Addresses[i] = ska + i++ + } + for i := range a.addrNew { + sam.NewBuckets[i] = make([]string, len(a.addrNew[i])) + j := 0 + for k := range a.addrNew[i] { + sam.NewBuckets[i][j] = k + j++ + } + } + for i := range a.addrTried { + sam.TriedBuckets[i] = make([]string, a.addrTried[i].Len()) + j := 0 + for e := a.addrTried[i].Front(); e != nil; e = e.Next() { + ka := e.Value.(*KnownAddress) + sam.TriedBuckets[i][j] = NetAddressKey(ka.na) + j++ + } + } + w, e := os.Create(a.PeersFile) + if e != nil { + E.F("error opening file %s: %v", a.PeersFile, e) + return + } + enc := json.NewEncoder(w) + defer func() { + if e := w.Close(); E.Chk(e) { + } + }() + if e := enc.Encode(&sam); E.Chk(e) { + E.F("failed to encode file %s: %v", a.PeersFile, e) + return + } +} + +// loadPeers loads the known address from the saved file. If empty, missing, or malformed file, just don't load anything +// and start fresh +func (a *AddrManager) loadPeers() { + T.Ln("loading peers") + + a.mtx.Lock() + defer a.mtx.Unlock() + e := a.deserializePeers(a.PeersFile) + if e != nil { + E.F("failed to parse file %s: %v", a.PeersFile, e) + // if it is invalid we nuke the old one unconditionally. + e = os.Remove(a.PeersFile) + if e != nil { + W.F("failed to remove corrupt peers file %s: %v", a.PeersFile, e) + } + a.reset() + return + } + // Tracec(func() string { + // return fmt.Sprintf( + // "loaded %d addresses from file '%s'", + // a.numAddresses(), a.PeersFile, + // ) + // }) +} +func (a *AddrManager) deserializePeers(filePath string) (e error) { + _, e = os.Stat(filePath) + if os.IsNotExist(e) { + return nil + } + r, e := os.Open(filePath) + if e != nil { + E.Ln(e) + return fmt.Errorf("%s error opening file: %v", filePath, e) + } + defer func() { + if e = r.Close(); E.Chk(e) { + } + }() + var sam serializedAddrManager + dec := json.NewDecoder(r) + e = dec.Decode(&sam) + if e != nil { + E.Ln(e) + return fmt.Errorf("error reading %s: %v", filePath, e) + } + if sam.Version != serialisationVersion { + return fmt.Errorf( + "unknown version %v in serialized addrmanager", + sam.Version, + ) + } + copy(a.key[:], sam.Key[:]) + for _, v := range sam.Addresses { + ka := new(KnownAddress) + ka.na, e = a.DeserializeNetAddress(v.Addr) + if e != nil { + E.Ln(e) + return fmt.Errorf("failed to deserialize netaddress "+ + "%s: %v", v.Addr, e, + ) + } + ka.srcAddr, e = a.DeserializeNetAddress(v.Src) + if e != nil { + E.Ln(e) + return fmt.Errorf("failed to deserialize netaddress "+ + "%s: %v", v.Src, e, + ) + } + ka.attempts = v.Attempts + ka.lastattempt = time.Unix(v.LastAttempt, 0) + ka.lastsuccess = time.Unix(v.LastSuccess, 0) + a.addrIndex[NetAddressKey(ka.na)] = ka + } + for i := range sam.NewBuckets { + for _, val := range sam.NewBuckets[i] { + ka, ok := a.addrIndex[val] + if !ok { + return fmt.Errorf("newbucket contains %s but "+ + "none in address list", val, + ) + } + if ka.refs == 0 { + a.nNew++ + } + ka.refs++ + a.addrNew[i][val] = ka + } + } + for i := range sam.TriedBuckets { + for _, val := range sam.TriedBuckets[i] { + ka, ok := a.addrIndex[val] + if !ok { + return fmt.Errorf( + "newbucket contains %s but none in address list", + val, + ) + } + ka.tried = true + a.nTried++ + a.addrTried[i].PushBack(ka) + } + } + // Sanity checking. + for k, v := range a.addrIndex { + if v.refs == 0 && !v.tried { + return fmt.Errorf("address %s after serialisationwith no references", k) + } + if v.refs > 0 && v.tried { + return fmt.Errorf("address %s after serialisation which is both new and tried", k) + } + } + return nil +} + +// DeserializeNetAddress converts a given address string to a *wire.NetAddress +func (a *AddrManager) DeserializeNetAddress(addr string) (*wire.NetAddress, error) { + host, portStr, e := net.SplitHostPort(addr) + if e != nil { + E.Ln(e) + return nil, e + } + port, e := strconv.ParseUint(portStr, 10, 16) + if e != nil { + E.Ln(e) + return nil, e + } + return a.HostToNetAddress(host, uint16(port), wire.SFNodeNetwork) +} + +// Start begins the core address handler which manages a pool of known +// addresses, timeouts, and interval based writes. +func (a *AddrManager) Start() { + // Already started? + if atomic.AddInt32(&a.started, 1) != 1 { + return + } + // Load peers we already know about from file. + T.Ln("loading peers data") + a.loadPeers() + // Start the address ticker to save addresses periodically. + a.wg.Add(1) + go a.addressHandler() +} + +// Stop gracefully shuts down the address manager by stopping the main handler. +func (a *AddrManager) Stop() (e error) { + if atomic.AddInt32(&a.shutdown, 1) != 1 { + D.Ln("address manager is already in the process of shutting down") + return nil + } + // D.Ln("address manager shutting down"} + a.quit.Q() + a.wg.Wait() + return nil +} + +// AddAddresses adds new addresses to the address manager. +// +// It enforces a max number of addresses and silently ignores duplicate addresses. +// +// It is safe for concurrent access. +func (a *AddrManager) AddAddresses(addrs []*wire.NetAddress, srcAddr *wire.NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + for _, na := range addrs { + a.updateAddress(na, srcAddr) + } +} + +// AddAddress adds a new address to the address manager. +// +// It enforces a max number of addresses and silently ignores duplicate addresses. +// +// It is safe for concurrent access. +func (a *AddrManager) AddAddress(addr, srcAddr *wire.NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + a.updateAddress(addr, srcAddr) +} + +// AddAddressByIP adds an address where we are given an ip:port and not a wire.NetAddress. +func (a *AddrManager) AddAddressByIP(addrIP string) (e error) { + // Split IP and port + addr, portStr, e := net.SplitHostPort(addrIP) + if e != nil { + E.Ln(e) + return e + } + // Put it in wire.Netaddress + ip := net.ParseIP(addr) + if ip == nil { + return fmt.Errorf("invalid ip address %s", addr) + } + port, e := strconv.ParseUint(portStr, 10, 0) + if e != nil { + E.Ln(e) + return fmt.Errorf("invalid port %s: %v", portStr, e) + } + na := wire.NewNetAddressIPPort(ip, uint16(port), 0) + a.AddAddress(na, na) // XXX use correct src address + return nil +} + +// NumAddresses returns the number of addresses known to the address manager. +func (a *AddrManager) numAddresses() int { + return a.nTried + a.nNew +} + +// NumAddresses returns the number of addresses known to the address manager. +func (a *AddrManager) NumAddresses() int { + a.mtx.Lock() + defer a.mtx.Unlock() + return a.numAddresses() +} + +// NeedMoreAddresses returns whether or not the address manager needs more addresses. +func (a *AddrManager) NeedMoreAddresses() bool { + a.mtx.Lock() + defer a.mtx.Unlock() + return a.numAddresses() < needAddressThreshold +} + +// AddressCache returns the current address cache. It must be treated as read-only (but since it is a copy now, this is +// not as dangerous). +func (a *AddrManager) AddressCache() []*wire.NetAddress { + a.mtx.Lock() + defer a.mtx.Unlock() + addrIndexLen := len(a.addrIndex) + if addrIndexLen == 0 { + return nil + } + allAddr := make([]*wire.NetAddress, 0, addrIndexLen) + // Iteration order is undefined here, but we randomise it anyway. + for _, v := range a.addrIndex { + allAddr = append(allAddr, v.na) + } + numAddresses := addrIndexLen * getAddrPercent / 100 + if numAddresses > getAddrMax { + numAddresses = getAddrMax + } + // Fisher-Yates shuffle the array. We only need to do the first `numAddresses' since we are throwing the rest. + for i := 0; i < numAddresses; i++ { + // pick a number between current index and the end + j := rand.Intn(addrIndexLen-i) + i + allAddr[i], allAddr[j] = allAddr[j], allAddr[i] + } + // slice off the limit we are willing to share. + return allAddr[0:numAddresses] +} + +// reset resets the address manager by reinitialising the random source and allocating fresh empty bucket storage. +func (a *AddrManager) reset() { + a.addrIndex = make(map[string]*KnownAddress) + // fill key with bytes from a good random source. + _, e := io.ReadFull(crand.Reader, a.key[:]) + if e != nil { + E.Ln(e) + } + for i := range a.addrNew { + a.addrNew[i] = make(map[string]*KnownAddress) + } + for i := range a.addrTried { + a.addrTried[i] = list.New() + } +} + +// HostToNetAddress returns a netaddress given a host address. +// +// If the address is a Tor .onion address this will be taken care of. +// +// Else if the host is not an IP address it will be resolved ( via Tor if required). +func (a *AddrManager) HostToNetAddress(host string, port uint16, services wire.ServiceFlag) (*wire.NetAddress, error) { + // Tor address is 16 char base32 + ".onion" + var ip net.IP + if len(host) == 22 && host[16:] == ".onion" { + // go base32 encoding uses capitals (as does the rfc but Tor and bitcoind tend to user lowercase, so we switch + // case here. + data, e := base32.StdEncoding.DecodeString( + strings.ToUpper(host[:16]), + ) + if e != nil { + E.Ln(e) + return nil, e + } + prefix := []byte{0xfd, 0x87, 0xd8, 0x7e, 0xeb, 0x43} + ip = append(prefix, data...) + } else if ip = net.ParseIP(host); ip == nil { + ips, e := a.lookupFunc(host) + if e != nil { + E.Ln(e) + return nil, e + } + if len(ips) == 0 { + return nil, fmt.Errorf("no addresses found for %s", host) + } + ip = ips[0] + } + return wire.NewNetAddressIPPort(ip, port, services), nil +} + +// ipString returns a string for the ip from the provided NetAddress. If the ip is in the range used for Tor addresses +// then it will be transformed into the relevant .onion address. +func ipString(na *wire.NetAddress) string { + if IsOnionCatTor(na) { + // We know now that na.IP is long enough. + s := base32.StdEncoding.EncodeToString(na.IP[6:]) + return strings.ToLower(s) + ".onion" + } + return na.IP.String() +} + +// NetAddressKey returns a string key in the form of ip:port for IPv4 addresses or [ip]:port for IPv6 addresses. +func NetAddressKey(na *wire.NetAddress) string { + port := strconv.FormatUint(uint64(na.Port), 10) + return net.JoinHostPort(ipString(na), port) +} + +// GetAddress returns a single address that should be routable. It picks a random one from the possible addresses with +// preference given to ones that have not been used recently and should not pick 'close' addresses consecutively. +func (a *AddrManager) GetAddress() *KnownAddress { + // Protect concurrent access. + a.mtx.Lock() + defer a.mtx.Unlock() + if a.numAddresses() == 0 { + return nil + } + // Use a 50% chance for choosing between tried and new table entries. + if a.nTried > 0 && (a.nNew == 0 || a.rand.Intn(2) == 0) { + // Tried entry. + large := 1 << 30 + factor := 1.0 + for { + // pick a random bucket. + bucket := a.rand.Intn(len(a.addrTried)) + if a.addrTried[bucket].Len() == 0 { + continue + } + // Pick a random entry in the list + e := a.addrTried[bucket].Front() + for i := + a.rand.Int63n(int64(a.addrTried[bucket].Len())); i > 0; i-- { + e = e.Next() + } + ka := e.Value.(*KnownAddress) + randval := a.rand.Intn(large) + if float64(randval) < (factor * ka.chance() * float64(large)) { + T.C(func() string { + return fmt.Sprintf("selected %v from tried bucket", NetAddressKey(ka.na)) + }, + ) + return ka + } + factor *= 1.2 + } + } else { + // new node. + // TODO: use a closure/function to avoid repeating this. + large := 1 << 30 + factor := 1.0 + for { + // Pick a random bucket. + bucket := a.rand.Intn(len(a.addrNew)) + if len(a.addrNew[bucket]) == 0 { + continue + } + // Then, a random entry in it. + var ka *KnownAddress + nth := a.rand.Intn(len(a.addrNew[bucket])) + for _, value := range a.addrNew[bucket] { + if nth == 0 { + ka = value + } + nth-- + } + randval := a.rand.Intn(large) + if float64(randval) < (factor * ka.chance() * float64(large)) { + T.C(func() string { + return fmt.Sprintf("Selected %v from new bucket", + NetAddressKey(ka.na), + ) + }, + ) + return ka + } + factor *= 1.2 + } + } +} +func (a *AddrManager) find(addr *wire.NetAddress) *KnownAddress { + return a.addrIndex[NetAddressKey(addr)] +} + +// Attempt increases the given address' attempt counter and updates the last attempt time. +func (a *AddrManager) Attempt(addr *wire.NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + // find address. Surely address will be in tried by now? + ka := a.find(addr) + if ka == nil { + return + } + // set last tried time to now + ka.attempts++ + ka.lastattempt = time.Now() +} + +// Connected Marks the given address as currently connected and working at the current time. The address must already be +// known to AddrManager else it will be ignored. +func (a *AddrManager) Connected(addr *wire.NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + ka := a.find(addr) + if ka == nil { + return + } + // Update the time as long as it has been 20 minutes since last we did so. + now := time.Now() + if now.After(ka.na.Timestamp.Add(time.Minute * 20)) { + // ka.na is immutable, so replace it. + naCopy := *ka.na + naCopy.Timestamp = time.Now() + ka.na = &naCopy + } +} + +// Good marks the given address as good. To be called after a successful connection and version exchange. If the address +// is unknown to the address manager it will be ignored. +func (a *AddrManager) Good(addr *wire.NetAddress) { + a.mtx.Lock() + defer a.mtx.Unlock() + ka := a.find(addr) + if ka == nil { + return + } + // ka.Timestamp is not updated here to avoid leaking information about currently connected peers. + now := time.Now() + ka.lastsuccess = now + ka.lastattempt = now + ka.attempts = 0 + // move to tried set, optionally evicting other addresses if needed. + if ka.tried { + return + } + // ok, need to move it to tried. remove from all new buckets. record one of the buckets in question and call it the + // `first' + addrKey := NetAddressKey(addr) + oldBucket := -1 + for i := range a.addrNew { + // we check for existence so we can record the first one + if _, ok := a.addrNew[i][addrKey]; ok { + delete(a.addrNew[i], addrKey) + ka.refs-- + if oldBucket == -1 { + oldBucket = i + } + } + } + a.nNew-- + if oldBucket == -1 { + // What? wasn't in a bucket after all.... Panic? + return + } + bucket := a.getTriedBucket(ka.na) + // Room in this tried bucket? + if a.addrTried[bucket].Len() < triedBucketSize { + ka.tried = true + a.addrTried[bucket].PushBack(ka) + a.nTried++ + return + } + // No room, we have to evict something else. + entry := a.pickTried(bucket) + rmka := entry.Value.(*KnownAddress) + // First bucket it would have been put in. + newBucket := a.getNewBucket(rmka.na, rmka.srcAddr) + // If no room in the original bucket, we put it in a bucket we just freed up a space in. + if len(a.addrNew[newBucket]) >= newBucketSize { + newBucket = oldBucket + } + // replace with ka in list. + ka.tried = true + entry.Value = ka + rmka.tried = false + rmka.refs++ + // We don't touch a.nTried here since the number of tried stays the same but we decemented new above, raise it again + // since we're putting something back. + a.nNew++ + rmkey := NetAddressKey(rmka.na) + T.F("replacing %s with %s in tried", rmkey, addrKey) + + // We made sure there is space here just above. + a.addrNew[newBucket][rmkey] = rmka +} + +// SetServices sets the services for the giiven address to the provided value. +func (a *AddrManager) SetServices(addr *wire.NetAddress, services wire.ServiceFlag) { + a.mtx.Lock() + defer a.mtx.Unlock() + ka := a.find(addr) + if ka == nil { + return + } + // Update the services if needed. + if ka.na.Services != services { + // ka.na is immutable, so replace it. + naCopy := *ka.na + naCopy.Services = services + ka.na = &naCopy + } +} + +// AddLocalAddress adds na to the list of known local addresses to advertise with the given priority. +func (a *AddrManager) AddLocalAddress(na *wire.NetAddress, priority AddressPriority) (e error) { + if !IsRoutable(na) { + return fmt.Errorf("address %s is not routable", na.IP) + } + a.lamtx.Lock() + defer a.lamtx.Unlock() + key := NetAddressKey(na) + la, ok := a.localAddresses[key] + if !ok || la.score < priority { + if ok { + la.score = priority + 1 + } else { + a.localAddresses[key] = &localAddress{ + na: na, + score: priority, + } + } + } + return nil +} + +// getReachabilityFrom returns the relative reachability of the provided local address to the provided remote address. +func getReachabilityFrom(localAddr, remoteAddr *wire.NetAddress) int { + const ( + Unreachable = 0 + Default = iota + Teredo + Ipv6Weak + Ipv4 + Ipv6Strong + Private + ) + if !IsRoutable(remoteAddr) { + return Unreachable + } + if IsOnionCatTor(remoteAddr) { + if IsOnionCatTor(localAddr) { + return Private + } + if IsRoutable(localAddr) && IsIPv4(localAddr) { + return Ipv4 + } + return Default + } + if IsRFC4380(remoteAddr) { + if !IsRoutable(localAddr) { + return Default + } + if IsRFC4380(localAddr) { + return Teredo + } + if IsIPv4(localAddr) { + return Ipv4 + } + return Ipv6Weak + } + if IsIPv4(remoteAddr) { + if IsRoutable(localAddr) && IsIPv4(localAddr) { + return Ipv4 + } + return Unreachable + } + /* ipv6 */ + var tunnelled bool + // Is our v6 is tunnelled? + if IsRFC3964(localAddr) || IsRFC6052(localAddr) || IsRFC6145(localAddr) { + tunnelled = true + } + if !IsRoutable(localAddr) { + return Default + } + if IsRFC4380(localAddr) { + return Teredo + } + if IsIPv4(localAddr) { + return Ipv4 + } + if tunnelled { + // only prioritise ipv6 if we aren't tunnelling it. + return Ipv6Weak + } + return Ipv6Strong +} + +// GetBestLocalAddress returns the most appropriate local address to use for the given remote address. +func (a *AddrManager) GetBestLocalAddress(remoteAddr *wire.NetAddress) *wire.NetAddress { + a.lamtx.Lock() + defer a.lamtx.Unlock() + bestreach := 0 + var bestscore AddressPriority + var bestAddress *wire.NetAddress + for _, la := range a.localAddresses { + reach := getReachabilityFrom(la.na, remoteAddr) + if reach > bestreach || + (reach == bestreach && la.score > bestscore) { + bestreach = reach + bestscore = la.score + bestAddress = la.na + } + } + if bestAddress != nil { + T.F("suggesting address %s:%d for %s:%d", bestAddress.IP, + bestAddress.Port, remoteAddr.IP, remoteAddr.Port, + ) + } else { + T.F("no worthy address for %s:%d", remoteAddr.IP, + remoteAddr.Port, + ) + // Send something unroutable if nothing suitable. + var ip net.IP + if !IsIPv4(remoteAddr) && !IsOnionCatTor(remoteAddr) { + ip = net.IPv6zero + } else { + ip = net.IPv4zero + } + services := wire.SFNodeNetwork | /*wire.SFNodeWitness |*/ wire.SFNodeBloom + bestAddress = wire.NewNetAddressIPPort(ip, 0, services) + } + return bestAddress +} + +// New returns a new bitcoin address manager. Use Start to begin processing asynchronous address updates. +func New(dataDir string, lookupFunc func(string) ([]net.IP, error)) *AddrManager { + am := AddrManager{ + PeersFile: filepath.Join(dataDir, "peers.json"), + lookupFunc: lookupFunc, + rand: rand.New(rand.NewSource(time.Now().UnixNano())), + quit: qu.T(), + localAddresses: make(map[string]*localAddress), + } + am.reset() + return &am +} diff --git a/pkg/addrmgr/addrmanager_test.go b/pkg/addrmgr/addrmanager_test.go new file mode 100644 index 0000000..34e2c8b --- /dev/null +++ b/pkg/addrmgr/addrmanager_test.go @@ -0,0 +1,427 @@ +package addrmgr_test + +import ( + "errors" + "fmt" + "net" + "reflect" + "testing" + "time" + + "github.com/p9c/p9/pkg/addrmgr" + "github.com/p9c/p9/pkg/wire" +) + +// naTest is used to describe a test to be performed against the NetAddressKey method. +type naTest struct { + in wire.NetAddress + want string +} + +// naTests houses all of the tests to be performed against the NetAddressKey method. +var naTests = make([]naTest, 0) + +// Put some IP in here for convenience. Points to google. +var someIP = "173.194.115.66" + +// addNaTests +func addNaTests() { + // IPv4 + // Localhost + addNaTest("127.0.0.1", 11047, "127.0.0.1:11047") + addNaTest("127.0.0.1", 11048, "127.0.0.1:11048") + // Class A + addNaTest("1.0.0.1", 11047, "1.0.0.1:11047") + addNaTest("2.2.2.2", 11048, "2.2.2.2:11048") + addNaTest("27.253.252.251", 8335, "27.253.252.251:8335") + addNaTest("123.3.2.1", 8336, "123.3.2.1:8336") + // Private Class A + addNaTest("10.0.0.1", 11047, "10.0.0.1:11047") + addNaTest("10.1.1.1", 11048, "10.1.1.1:11048") + addNaTest("10.2.2.2", 8335, "10.2.2.2:8335") + addNaTest("10.10.10.10", 8336, "10.10.10.10:8336") + // Class B + addNaTest("128.0.0.1", 11047, "128.0.0.1:11047") + addNaTest("129.1.1.1", 11048, "129.1.1.1:11048") + addNaTest("180.2.2.2", 8335, "180.2.2.2:8335") + addNaTest("191.10.10.10", 8336, "191.10.10.10:8336") + // Private Class B + addNaTest("172.16.0.1", 11047, "172.16.0.1:11047") + addNaTest("172.16.1.1", 11048, "172.16.1.1:11048") + addNaTest("172.16.2.2", 8335, "172.16.2.2:8335") + addNaTest("172.16.172.172", 8336, "172.16.172.172:8336") + // Class C + addNaTest("193.0.0.1", 11047, "193.0.0.1:11047") + addNaTest("200.1.1.1", 11048, "200.1.1.1:11048") + addNaTest("205.2.2.2", 8335, "205.2.2.2:8335") + addNaTest("223.10.10.10", 8336, "223.10.10.10:8336") + // Private Class C + addNaTest("192.168.0.1", 11047, "192.168.0.1:11047") + addNaTest("192.168.1.1", 11048, "192.168.1.1:11048") + addNaTest("192.168.2.2", 8335, "192.168.2.2:8335") + addNaTest("192.168.192.192", 8336, "192.168.192.192:8336") + // IPv6 + // Localhost + addNaTest("::1", 11047, "[::1]:11047") + addNaTest("fe80::1", 11048, "[fe80::1]:11048") + // Link-local + addNaTest("fe80::1:1", 11047, "[fe80::1:1]:11047") + addNaTest("fe91::2:2", 11048, "[fe91::2:2]:11048") + addNaTest("fea2::3:3", 8335, "[fea2::3:3]:8335") + addNaTest("feb3::4:4", 8336, "[feb3::4:4]:8336") + // Site-local + addNaTest("fec0::1:1", 11047, "[fec0::1:1]:11047") + addNaTest("fed1::2:2", 11048, "[fed1::2:2]:11048") + addNaTest("fee2::3:3", 8335, "[fee2::3:3]:8335") + addNaTest("fef3::4:4", 8336, "[fef3::4:4]:8336") +} +func addNaTest(ip string, port uint16, want string) { + nip := net.ParseIP(ip) + na := *wire.NewNetAddressIPPort(nip, port, wire.SFNodeNetwork) + test := naTest{na, want} + naTests = append(naTests, test) +} +func lookupFunc(host string) ([]net.IP, error) { + return nil, errors.New("not implemented") +} +func TestStartStop(t *testing.T) { + n := addrmgr.New("teststartstop", lookupFunc) + n.Start() + e := n.Stop() + if e != nil { + t.Fatalf("Address Manager failed to stop: %v", e) + } +} +func TestAddAddressByIP(t *testing.T) { + fmtErr := fmt.Errorf("") + addrErr := &net.AddrError{} + var tests = []struct { + addrIP string + err error + }{ + { + someIP + ":11047", nil, + }, + { + someIP, + addrErr, + }, + { + someIP[:12] + ":11047", + fmtErr, + }, + { + someIP + ":abcd", + fmtErr, + }, + } + amgr := addrmgr.New("testaddressbyip", nil) + for i, test := range tests { + e := amgr.AddAddressByIP(test.addrIP) + if test.err != nil && e == nil { + t.Errorf("TestGood test %d failed expected an error and got none", i) + continue + } + if test.err == nil && e != nil { + t.Errorf("TestGood test %d failed expected no error and got one", i) + continue + } + if reflect.TypeOf(e) != reflect.TypeOf(test.err) { + t.Errorf("TestGood test %d failed got %v, want %v", i, + reflect.TypeOf(e), reflect.TypeOf(test.err), + ) + continue + } + } +} +func TestAddLocalAddress(t *testing.T) { + var tests = []struct { + address wire.NetAddress + priority addrmgr.AddressPriority + valid bool + }{ + { + wire.NetAddress{IP: net.ParseIP("192.168.0.100")}, + addrmgr.InterfacePrio, + false, + }, + { + wire.NetAddress{IP: net.ParseIP("204.124.1.1")}, + addrmgr.InterfacePrio, + true, + }, + { + wire.NetAddress{IP: net.ParseIP("204.124.1.1")}, + addrmgr.BoundPrio, + true, + }, + { + wire.NetAddress{IP: net.ParseIP("::1")}, + addrmgr.InterfacePrio, + false, + }, + { + wire.NetAddress{IP: net.ParseIP("fe80::1")}, + addrmgr.InterfacePrio, + false, + }, + { + wire.NetAddress{IP: net.ParseIP("2620:100::1")}, + addrmgr.InterfacePrio, + true, + }, + } + amgr := addrmgr.New("testaddlocaladdress", nil) + for x, test := range tests { + result := amgr.AddLocalAddress(&test.address, test.priority) + if result == nil && !test.valid { + t.Errorf("TestAddLocalAddress test #%d failed: %s should have been accepted", x, test.address.IP) + continue + } + if result != nil && test.valid { + t.Errorf("TestAddLocalAddress test #%d failed: %s should not have been accepted", x, test.address.IP) + continue + } + } +} +func TestAttempt(t *testing.T) { + n := addrmgr.New("testattempt", lookupFunc) + // Add a new address and get it + e := n.AddAddressByIP(someIP + ":11047") + if e != nil { + t.Fatalf("Adding address failed: %v", e) + } + ka := n.GetAddress() + if !ka.LastAttempt().IsZero() { + t.Errorf("Address should not have attempts, but does") + } + na := ka.NetAddress() + n.Attempt(na) + if ka.LastAttempt().IsZero() { + t.Errorf("Address should have an attempt, but does not") + } +} +func TestConnected(t *testing.T) { + n := addrmgr.New("testconnected", lookupFunc) + // Add a new address and get it + e := n.AddAddressByIP(someIP + ":11047") + if e != nil { + t.Fatalf("Adding address failed: %v", e) + } + ka := n.GetAddress() + na := ka.NetAddress() + // make it an hour ago + na.Timestamp = time.Unix(time.Now().Add(time.Hour*-1).Unix(), 0) + n.Connected(na) + if !ka.NetAddress().Timestamp.After(na.Timestamp) { + t.Errorf("Address should have a new timestamp, but does not") + } +} +func TestNeedMoreAddresses(t *testing.T) { + n := addrmgr.New("testneedmoreaddresses", lookupFunc) + addrsToAdd := 1500 + b := n.NeedMoreAddresses() + if !b { + t.Errorf("Expected that we need more addresses") + } + addrs := make([]*wire.NetAddress, addrsToAdd) + var e error + for i := 0; i < addrsToAdd; i++ { + s := fmt.Sprintf("%d.%d.173.147:11047", i/128+60, i%128+60) + addrs[i], e = n.DeserializeNetAddress(s) + if e != nil { + t.Errorf("Failed to turn %s into an address: %v", s, e) + } + } + srcAddr := wire.NewNetAddressIPPort(net.IPv4(173, 144, 173, 111), 11047, 0) + n.AddAddresses(addrs, srcAddr) + numAddrs := n.NumAddresses() + if numAddrs > addrsToAdd { + t.Errorf("Number of addresses is too many %d vs %d", numAddrs, addrsToAdd) + } + b = n.NeedMoreAddresses() + if b { + t.Errorf("Expected that we don't need more addresses") + } +} +func TestGood(t *testing.T) { + n := addrmgr.New("testgood", lookupFunc) + addrsToAdd := 64 * 64 + addrs := make([]*wire.NetAddress, addrsToAdd) + var e error + for i := 0; i < addrsToAdd; i++ { + s := fmt.Sprintf("%d.173.147.%d:11047", i/64+60, i%64+60) + addrs[i], e = n.DeserializeNetAddress(s) + if e != nil { + t.Errorf("Failed to turn %s into an address: %v", s, e) + } + } + srcAddr := wire.NewNetAddressIPPort(net.IPv4(173, 144, 173, 111), 11047, 0) + n.AddAddresses(addrs, srcAddr) + for _, addr := range addrs { + n.Good(addr) + } + numAddrs := n.NumAddresses() + if numAddrs >= addrsToAdd { + t.Errorf("Number of addresses is too many: %d vs %d", numAddrs, addrsToAdd) + } + numCache := len(n.AddressCache()) + if numCache >= numAddrs/4 { + t.Errorf("Number of addresses in cache: got %d, want %d", numCache, numAddrs/4) + } +} +func TestGetAddress(t *testing.T) { + n := addrmgr.New("testgetaddress", lookupFunc) + // Get an address from an empty set (should error) + if rv := n.GetAddress(); rv != nil { + t.Errorf("GetAddress failed: got: %v want: %v\n", rv, nil) + } + // Add a new address and get it + e := n.AddAddressByIP(someIP + ":11047") + if e != nil { + t.Fatalf("Adding address failed: %v", e) + } + ka := n.GetAddress() + if ka == nil { + t.Fatalf("Did not get an address where there is one in the pool") + } + if ka.NetAddress().IP.String() != someIP { + t.Errorf("Wrong IP: got %v, want %v", ka.NetAddress().IP.String(), someIP) + } + // Mark this as a good address and get it + n.Good(ka.NetAddress()) + ka = n.GetAddress() + if ka == nil { + t.Fatalf("Did not get an address where there is one in the pool") + } + if ka.NetAddress().IP.String() != someIP { + t.Errorf("Wrong IP: got %v, want %v", ka.NetAddress().IP.String(), someIP) + } + numAddrs := n.NumAddresses() + if numAddrs != 1 { + t.Errorf("Wrong number of addresses: got %d, want %d", numAddrs, 1) + } +} +func TestGetBestLocalAddress(t *testing.T) { + localAddrs := []wire.NetAddress{ + {IP: net.ParseIP("192.168.0.100")}, + {IP: net.ParseIP("::1")}, + {IP: net.ParseIP("fe80::1")}, + {IP: net.ParseIP("2001:470::1")}, + } + var tests = []struct { + remoteAddr wire.NetAddress + want0 wire.NetAddress + want1 wire.NetAddress + want2 wire.NetAddress + want3 wire.NetAddress + }{ + { + // Remote connection from public IPv4 + wire.NetAddress{IP: net.ParseIP("204.124.8.1")}, + wire.NetAddress{IP: net.IPv4zero}, + wire.NetAddress{IP: net.IPv4zero}, + wire.NetAddress{IP: net.ParseIP("204.124.8.100")}, + wire.NetAddress{IP: net.ParseIP("fd87:d87e:eb43:25::1")}, + }, + { + // Remote connection from private IPv4 + wire.NetAddress{IP: net.ParseIP("172.16.0.254")}, + wire.NetAddress{IP: net.IPv4zero}, + wire.NetAddress{IP: net.IPv4zero}, + wire.NetAddress{IP: net.IPv4zero}, + wire.NetAddress{IP: net.IPv4zero}, + }, + { + // Remote connection from public IPv6 + wire.NetAddress{IP: net.ParseIP("2602:100:abcd::102")}, + wire.NetAddress{IP: net.IPv6zero}, + wire.NetAddress{IP: net.ParseIP("2001:470::1")}, + wire.NetAddress{IP: net.ParseIP("2001:470::1")}, + wire.NetAddress{IP: net.ParseIP("2001:470::1")}, + }, + /* XXX + { + // Remote connection from Tor + wire.NetAddress{IP: net.ParseIP("fd87:d87e:eb43::100")}, + wire.NetAddress{IP: net.IPv4zero}, + wire.NetAddress{IP: net.ParseIP("204.124.8.100")}, + wire.NetAddress{IP: net.ParseIP("fd87:d87e:eb43:25::1")}, + }, + */ + } + amgr := addrmgr.New("testgetbestlocaladdress", nil) + // Test against default when there's no address + for x, test := range tests { + got := amgr.GetBestLocalAddress(&test.remoteAddr) + if !test.want0.IP.Equal(got.IP) { + t.Errorf("TestGetBestLocalAddress test1 #%d failed for remote address %s: want %s got %s", + x, test.remoteAddr.IP, test.want1.IP, got.IP, + ) + continue + } + } + var e error + for _, localAddr := range localAddrs { + e = amgr.AddLocalAddress(&localAddr, addrmgr.InterfacePrio) + if e != nil { + t.Log(e) + } + } + // Test against want1 + for x, test := range tests { + got := amgr.GetBestLocalAddress(&test.remoteAddr) + if !test.want1.IP.Equal(got.IP) { + t.Errorf("TestGetBestLocalAddress test1 #%d failed for remote address %s: want %s got %s", + x, test.remoteAddr.IP, test.want1.IP, got.IP, + ) + continue + } + } + // Add a public IP to the list of local addresses. + localAddr := wire.NetAddress{IP: net.ParseIP("204.124.8.100")} + e = amgr.AddLocalAddress(&localAddr, addrmgr.InterfacePrio) + if e != nil { + t.Log(e) + } + // Test against want2 + for x, test := range tests { + got := amgr.GetBestLocalAddress(&test.remoteAddr) + if !test.want2.IP.Equal(got.IP) { + t.Errorf("TestGetBestLocalAddress test2 #%d failed for remote address %s: want %s got %s", + x, test.remoteAddr.IP, test.want2.IP, got.IP, + ) + continue + } + } + /* + // Add a Tor generated IP address + localAddr = wire.NetAddress{IP: net.ParseIP("fd87:d87e:eb43:25::1")} + amgr.AddLocalAddress(&localAddr, addrmgr.ManualPrio) + // Test against want3 + + for x, test := range tests { + + got := amgr.GetBestLocalAddress(&test.remoteAddr) + + if !test.want3.IP.Equal(got.IP) { + + + t.Errorf("TestGetBestLocalAddress test3 #%d failed for remote address %s: want %s got %s", + x, test.remoteAddr.IP, test.want3.IP, got.IP) + continue + } + } + */ +} +func TestNetAddressKey(t *testing.T) { + addNaTests() + t.Logf("Running %d tests", len(naTests)) + for i, test := range naTests { + key := addrmgr.NetAddressKey(&test.in) + if key != test.want { + t.Errorf("NetAddressKey #%d\n got: %s want: %s", i, key, test.want) + continue + } + } +} diff --git a/pkg/addrmgr/doc.go b/pkg/addrmgr/doc.go new file mode 100644 index 0000000..8a87890 --- /dev/null +++ b/pkg/addrmgr/doc.go @@ -0,0 +1,25 @@ +/*Package addrmgr implements concurrency safe Bitcoin address manager. + +Address Manager Overview + +In order maintain the peer-to-peer Bitcoin network, there needs to be a source of addresses to connect to as nodes come +and go. The Bitcoin protocol provides the getaddr and addr messages to allow peers to communicate known addresses with +each other. However, there needs to a mechanism to store those results and select peers from them. It is also important +to note that remote peers can't be trusted to send valid peers nor attempt to provide you with only peers they control +with malicious intent. + +With that in mind, this package provides a concurrency safe address manager for caching and selecting peers in a +non-deterministic manner. The general idea is the caller adds addresses to the address manager and notifies it when +addresses are connected, known good, and attempted. The caller also requests addresses as it needs them. + +The address manager internally segregates the addresses into groups and non-deterministically selects groups in a +cryptographically random manner. This reduce the chances multiple addresses from the same nets are selected which +generally helps provide greater peer diversity, and perhaps more importantly, drastically reduces the chances an +attacker is able to coerce your peer into only connecting to nodes they control. + +The address manager also understands routability and Tor addresses and tries hard to only return routable addresses. In +addition, it uses the information provided by the caller about connected, known good, and attempted addresses to +periodically purge peers which no longer appear to be good peers as well as bias the selection toward known good peers. +The general idea is to make a best effort at only providing usable addresses. +*/ +package addrmgr diff --git a/pkg/addrmgr/internal_test.go b/pkg/addrmgr/internal_test.go new file mode 100644 index 0000000..f0cfce0 --- /dev/null +++ b/pkg/addrmgr/internal_test.go @@ -0,0 +1,20 @@ +package addrmgr + +import ( + "time" + + "github.com/p9c/p9/pkg/wire" +) + +func TstKnownAddressIsBad(ka *KnownAddress) bool { + return ka.isBad() +} +func TstKnownAddressChance(ka *KnownAddress) float64 { + return ka.chance() +} +func TstNewKnownAddress(na *wire.NetAddress, attempts int, lastattempt, lastsuccess time.Time, tried bool, refs int, +) *KnownAddress { + return &KnownAddress{na: na, attempts: attempts, lastattempt: lastattempt, lastsuccess: lastsuccess, tried: tried, + refs: refs, + } +} diff --git a/pkg/addrmgr/knownaddress.go b/pkg/addrmgr/knownaddress.go new file mode 100644 index 0000000..0d7afab --- /dev/null +++ b/pkg/addrmgr/knownaddress.go @@ -0,0 +1,84 @@ +package addrmgr + +import ( + "time" + + "github.com/p9c/p9/pkg/wire" +) + +// KnownAddress tracks information about a known network address that is used to determine how viable an address is. +type KnownAddress struct { + na *wire.NetAddress + srcAddr *wire.NetAddress + attempts int + lastattempt time.Time + lastsuccess time.Time + tried bool + refs int // reference count of new buckets +} + +// NetAddress returns the underlying wire.NetAddress associated with the known address. +func (ka *KnownAddress) NetAddress() *wire.NetAddress { + return ka.na +} + +// LastAttempt returns the last time the known address was attempted. +func (ka *KnownAddress) LastAttempt() time.Time { + return ka.lastattempt +} + +// chance returns the selection probability for a known address. The priority depends upon how recently the address has +// been seen, how recently it was last attempted and how often attempts to connect to it have failed. +func (ka *KnownAddress) chance() float64 { + now := time.Now() + lastAttempt := now.Sub(ka.lastattempt) + if lastAttempt < 0 { + lastAttempt = 0 + } + c := 1.0 + // Very recent attempts are less likely to be retried. + if lastAttempt < 10*time.Minute { + c *= 0.01 + } + // Failed attempts deprioritise. + for i := ka.attempts; i > 0; i-- { + c /= 1.5 + } + return c +} + +// isBad returns true if the address in question has not been tried in the last minute and meets one of the following +// criteria: +// +// 1) It claims to be from the future +// +// 2) It hasn't been seen in over a month +// +// 3) It has failed at least three times and never succeeded +// +// 4) It has failed ten times in the last week +// +// All addresses that meet these criteria are assumed to be worthless and not worth keeping hold of. +func (ka *KnownAddress) isBad() bool { + if ka.lastattempt.After(time.Now().Add(-1 * time.Minute)) { + return false + } + // From the future? + if ka.na.Timestamp.After(time.Now().Add(10 * time.Minute)) { + return true + } + // Over a month old? + if ka.na.Timestamp.Before(time.Now().Add(-1 * numMissingDays * time.Hour * 24)) { + return true + } + // Never succeeded? + if ka.lastsuccess.IsZero() && ka.attempts >= numRetries { + return true + } + // Hasn't succeeded in too long? + if !ka.lastsuccess.After(time.Now().Add(-1*minBadDays*time.Hour*24)) && + ka.attempts >= maxFailures { + return true + } + return false +} diff --git a/pkg/addrmgr/knownaddress_test.go b/pkg/addrmgr/knownaddress_test.go new file mode 100644 index 0000000..4b5ad45 --- /dev/null +++ b/pkg/addrmgr/knownaddress_test.go @@ -0,0 +1,106 @@ +package addrmgr_test + +import ( + "math" + "testing" + "time" + + "github.com/p9c/p9/pkg/addrmgr" + "github.com/p9c/p9/pkg/wire" +) + +func TestChance(t *testing.T) { + now := time.Unix(time.Now().Unix(), 0) + var tests = []struct { + addr *addrmgr.KnownAddress + expected float64 + }{ + { + // Test normal case + addrmgr.TstNewKnownAddress(&wire.NetAddress{Timestamp: now.Add(-35 * time.Second)}, + 0, time.Now().Add(-30*time.Minute), time.Now(), false, 0, + ), + 1.0, + }, { + // Test case in which lastseen < 0 + addrmgr.TstNewKnownAddress(&wire.NetAddress{Timestamp: now.Add(20 * time.Second)}, + 0, time.Now().Add(-30*time.Minute), time.Now(), false, 0, + ), + 1.0, + }, { + // Test case in which lastattempt < 0 + addrmgr.TstNewKnownAddress(&wire.NetAddress{Timestamp: now.Add(-35 * time.Second)}, + 0, time.Now().Add(30*time.Minute), time.Now(), false, 0, + ), + 1.0 * .01, + }, { + // Test case in which lastattempt < ten minutes + addrmgr.TstNewKnownAddress(&wire.NetAddress{Timestamp: now.Add(-35 * time.Second)}, + 0, time.Now().Add(-5*time.Minute), time.Now(), false, 0, + ), + 1.0 * .01, + }, { + // Test case with several failed attempts. + addrmgr.TstNewKnownAddress(&wire.NetAddress{Timestamp: now.Add(-35 * time.Second)}, + 2, time.Now().Add(-30*time.Minute), time.Now(), false, 0, + ), + 1 / 1.5 / 1.5, + }, + } + e := .0001 + for i, test := range tests { + chance := addrmgr.TstKnownAddressChance(test.addr) + if math.Abs(test.expected-chance) >= e { + t.Errorf("case %d: got %f, expected %f", i, chance, test.expected) + } + } +} +func TestIsBad(t *testing.T) { + now := time.Unix(time.Now().Unix(), 0) + future := now.Add(35 * time.Minute) + monthOld := now.Add(-43 * time.Hour * 24) + secondsOld := now.Add(-2 * time.Second) + minutesOld := now.Add(-27 * time.Minute) + hoursOld := now.Add(-5 * time.Hour) + zeroTime := time.Time{} + futureNa := &wire.NetAddress{Timestamp: future} + minutesOldNa := &wire.NetAddress{Timestamp: minutesOld} + monthOldNa := &wire.NetAddress{Timestamp: monthOld} + currentNa := &wire.NetAddress{Timestamp: secondsOld} + // Test addresses that have been tried in the last minute. + if addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(futureNa, 3, secondsOld, zeroTime, false, 0)) { + t.Errorf("test case 1: addresses that have been tried in the last minute are not bad.") + } + if addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(monthOldNa, 3, secondsOld, zeroTime, false, 0)) { + t.Errorf("test case 2: addresses that have been tried in the last minute are not bad.") + } + if addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(currentNa, 3, secondsOld, zeroTime, false, 0)) { + t.Errorf("test case 3: addresses that have been tried in the last minute are not bad.") + } + if addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(currentNa, 3, secondsOld, monthOld, true, 0)) { + t.Errorf("test case 4: addresses that have been tried in the last minute are not bad.") + } + if addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(currentNa, 2, secondsOld, secondsOld, true, 0)) { + t.Errorf("test case 5: addresses that have been tried in the last minute are not bad.") + } + // Test address that claims to be from the future. + if !addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(futureNa, 0, minutesOld, hoursOld, true, 0)) { + t.Errorf("test case 6: addresses that claim to be from the future are bad.") + } + // Test address that has not been seen in over a month. + if !addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(monthOldNa, 0, minutesOld, hoursOld, true, 0)) { + t.Errorf("test case 7: addresses more than a month old are bad.") + } + // It has failed at least three times and never succeeded. + if !addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(minutesOldNa, 3, minutesOld, zeroTime, true, 0)) { + t.Errorf("test case 8: addresses that have never succeeded are bad.") + } + // It has failed ten times in the last week + if !addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(minutesOldNa, 10, minutesOld, monthOld, true, 0)) { + t.Errorf("test case 9: addresses that have not succeeded in too long are bad.") + } + // Test an address that should work. + if addrmgr.TstKnownAddressIsBad(addrmgr.TstNewKnownAddress(minutesOldNa, 2, minutesOld, hoursOld, true, 0)) { + t.Errorf("test case 10: This should be a valid address.") + } +} diff --git a/pkg/addrmgr/log.go b/pkg/addrmgr/log.go new file mode 100644 index 0000000..c4945ca --- /dev/null +++ b/pkg/addrmgr/log.go @@ -0,0 +1,43 @@ +package addrmgr + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/addrmgr/network.go b/pkg/addrmgr/network.go new file mode 100644 index 0000000..ab582dc --- /dev/null +++ b/pkg/addrmgr/network.go @@ -0,0 +1,230 @@ +package addrmgr + +import ( + "fmt" + "net" + + "github.com/p9c/p9/pkg/wire" +) + +var ( + // rfc1918Nets specifies the IPv4 private address blocks as defined by by RFC1918 (10.0.0.0/8, 172.16.0.0/12, and + // 192.168.0.0/16). + rfc1918Nets = []net.IPNet{ + ipNet("10.0.0.0", 8, 32), + ipNet("172.16.0.0", 12, 32), + ipNet("192.168.0.0", 16, 32), + } + // rfc2544Net specifies the the IPv4 block as defined by RFC2544 (198.18.0.0/15) + rfc2544Net = ipNet("198.18.0.0", 15, 32) + // rfc3849Net specifies the IPv6 documentation address block as defined by RFC3849 (2001:DB8::/32). + rfc3849Net = ipNet("2001:DB8::", 32, 128) + // rfc3927Net specifies the IPv4 auto configuration address block as defined by RFC3927 (169.254.0.0/16). + rfc3927Net = ipNet("169.254.0.0", 16, 32) + // rfc3964Net specifies the IPv6 to IPv4 encapsulation address block as defined by RFC3964 (2002::/16). + rfc3964Net = ipNet("2002::", 16, 128) + // rfc4193Net specifies the IPv6 unique local address block as defined by RFC4193 (FC00::/7). + rfc4193Net = ipNet("FC00::", 7, 128) + // rfc4380Net specifies the IPv6 teredo tunneling over UDP address block as defined by RFC4380 (2001::/32). + rfc4380Net = ipNet("2001::", 32, 128) + // rfc4843Net specifies the IPv6 ORCHID address block as defined by RFC4843 (2001:10::/28). + rfc4843Net = ipNet("2001:10::", 28, 128) + // rfc4862Net specifies the IPv6 stateless address autoconfiguration address block as defined by RFC4862 + // (FE80::/64). + rfc4862Net = ipNet("FE80::", 64, 128) + // rfc5737Net specifies the IPv4 documentation address blocks as defined by RFC5737 (192.0.2.0/24, 198.51.100.0/24, + // 203.0.113.0/24) + rfc5737Net = []net.IPNet{ + ipNet("192.0.2.0", 24, 32), + ipNet("198.51.100.0", 24, 32), + ipNet("203.0.113.0", 24, 32), + } + // rfc6052Net specifies the IPv6 well-known prefix address block as defined by RFC6052 (64:FF9B::/96). + rfc6052Net = ipNet("64:FF9B::", 96, 128) + // rfc6145Net specifies the IPv6 to IPv4 translated address range as defined by RFC6145 (::FFFF:0:0:0/96). + rfc6145Net = ipNet("::FFFF:0:0:0", 96, 128) + // rfc6598Net specifies the IPv4 block as defined by RFC6598 (100.64.0.0/10) + rfc6598Net = ipNet("100.64.0.0", 10, 32) + // onionCatNet defines the IPv6 address block used to support Tor. bitcoind encodes a .onion address as a 16 byte + // number by decoding the address prior to the .onion (i.e. the key hash) base32 into a ten/ byte number. It then + // stores the first 6 bytes of the address as 0xfd, 0x87, 0xd8, 0x7e, 0xeb, 0x43. This is the same range used by + // OnionCat, which is part part of the RFC4193 unique local IPv6 range. In summary the format is: { magic 6 bytes, + // 10 bytes base32 decode of key hash } + onionCatNet = ipNet("fd87:d87e:eb43::", 48, 128) + // zero4Net defines the IPv4 address block for address staring with 0 (0.0.0.0/8). + zero4Net = ipNet("0.0.0.0", 8, 32) + // heNet defines the Hurricane Electric IPv6 address block. + heNet = ipNet("2001:470::", 32, 128) +) + +// ipNet returns a net.IPNet struct given the passed IP address string, number of one bits to include at the start of +// the mask, and the total number of bits for the mask. +func ipNet(ip string, ones, bits int) net.IPNet { + return net.IPNet{IP: net.ParseIP(ip), Mask: net.CIDRMask(ones, bits)} +} + +// IsIPv4 returns whether or not the given address is an IPv4 address. +func IsIPv4(na *wire.NetAddress) bool { + return na.IP.To4() != nil +} + +// IsLocal returns whether or not the given address is a local address. +func IsLocal(na *wire.NetAddress) bool { + return na.IP.IsLoopback() || zero4Net.Contains(na.IP) +} + +// IsOnionCatTor returns whether or not the passed address is in the IPv6 range used by bitcoin to support Tor +// (fd87:d87e:eb43::/48). Note that this range is the same range used by OnionCat, which is part of the RFC4193 unique +// local IPv6 range. +func IsOnionCatTor(na *wire.NetAddress) bool { + return onionCatNet.Contains(na.IP) +} + +// IsRFC1918 returns whether or not the passed address is part of the IPv4 private network address space as defined by +// RFC1918 (10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16). +func IsRFC1918(na *wire.NetAddress) bool { + for _, rfc := range rfc1918Nets { + if rfc.Contains(na.IP) { + return true + } + } + return false +} + +// IsRFC2544 returns whether or not the passed address is part of the IPv4 address space as defined by RFC2544 +// (198.18.0.0/15) +func IsRFC2544(na *wire.NetAddress) bool { + return rfc2544Net.Contains(na.IP) +} + +// IsRFC3849 returns whether or not the passed address is part of the IPv6 documentation range as defined by RFC3849 +// (2001:DB8::/32). +func IsRFC3849(na *wire.NetAddress) bool { + return rfc3849Net.Contains(na.IP) +} + +// IsRFC3927 returns whether or not the passed address is part of the IPv4 autoconfiguration range as defined by RFC3927 +// (169.254.0.0/16). +func IsRFC3927(na *wire.NetAddress) bool { + return rfc3927Net.Contains(na.IP) +} + +// IsRFC3964 returns whether or not the passed address is part of the IPv6 to IPv4 encapsulation range as defined by +// RFC3964 (2002::/16). +func IsRFC3964(na *wire.NetAddress) bool { + return rfc3964Net.Contains(na.IP) +} + +// IsRFC4193 returns whether or not the passed address is part of the IPv6 unique local range as defined by RFC4193 +// (FC00::/7). +func IsRFC4193(na *wire.NetAddress) bool { + return rfc4193Net.Contains(na.IP) +} + +// IsRFC4380 returns whether or not the passed address is part of the IPv6 teredo tunneling over UDP range as defined by +// RFC4380 (2001::/32). +func IsRFC4380(na *wire.NetAddress) bool { + return rfc4380Net.Contains(na.IP) +} + +// IsRFC4843 returns whether or not the passed address is part of the IPv6 ORCHID range as defined by RFC4843 +// (2001:10::/28). +func IsRFC4843(na *wire.NetAddress) bool { + return rfc4843Net.Contains(na.IP) +} + +// IsRFC4862 returns whether or not the passed address is part of the IPv6 stateless address autoconfiguration range as +// defined by RFC4862 (FE80::/64). +func IsRFC4862(na *wire.NetAddress) bool { + return rfc4862Net.Contains(na.IP) +} + +// IsRFC5737 returns whether or not the passed address is part of the IPv4 documentation address space as defined by +// RFC5737 (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) +func IsRFC5737(na *wire.NetAddress) bool { + for _, rfc := range rfc5737Net { + if rfc.Contains(na.IP) { + return true + } + } + return false +} + +// IsRFC6052 returns whether or not the passed address is part of the IPv6 well-known prefix range as defined by RFC6052 +// (64:FF9B::/96). +func IsRFC6052(na *wire.NetAddress) bool { + return rfc6052Net.Contains(na.IP) +} + +// IsRFC6145 returns whether or not the passed address is part of the IPv6 to IPv4 translated address range as defined +// by RFC6145 (::FFFF:0:0:0/96). +func IsRFC6145(na *wire.NetAddress) bool { + return rfc6145Net.Contains(na.IP) +} + +// IsRFC6598 returns whether or not the passed address is part of the IPv4 shared address space specified by RFC6598 +// (100.64.0.0/10) +func IsRFC6598(na *wire.NetAddress) bool { + return rfc6598Net.Contains(na.IP) +} + +// IsValid returns whether or not the passed address is valid. The address is considered invalid under the following +// circumstances: +// +// IPv4: It is either a zero or all bits set address. I +// +// Pv6: It is either a zero or RFC3849 documentation address. +func IsValid(na *wire.NetAddress) bool { + // IsUnspecified returns if address is 0, so only all bits set, and RFC3849 need to be explicitly checked. + return na.IP != nil && !(na.IP.IsUnspecified() || + na.IP.Equal(net.IPv4bcast)) +} + +// IsRoutable returns whether or not the passed address is routable over the public internet. This is true as long as +// the address is valid and is not in any reserved ranges. +func IsRoutable(na *wire.NetAddress) bool { + return IsValid(na) && !(IsRFC1918(na) || IsRFC2544(na) || IsRFC3927(na) || IsRFC4862(na) || IsRFC3849(na) || IsRFC4843(na) || IsRFC5737(na) || IsRFC6598(na) || IsLocal(na) || (IsRFC4193(na) && !IsOnionCatTor(na))) +} + +// GroupKey returns a string representing the network group an address is part of. This is the /16 for IPv4, the /32 +// (/36 for he.net) for IPv6, the string "local" for a local address, the string "tor:key" where key is the /4 of the +// onion address for Tor address, and the string "unroutable" for an unroutable address. +func GroupKey(na *wire.NetAddress) string { + if IsLocal(na) { + return "local" + } + if !IsRoutable(na) { + return "unroutable" + } + if IsIPv4(na) { + return na.IP.Mask(net.CIDRMask(16, 32)).String() + } + if IsRFC6145(na) || IsRFC6052(na) { + // last four bytes are the ip address + ip := na.IP[12:16] + return ip.Mask(net.CIDRMask(16, 32)).String() + } + if IsRFC3964(na) { + ip := na.IP[2:6] + return ip.Mask(net.CIDRMask(16, 32)).String() + } + if IsRFC4380(na) { + // teredo tunnels have the last 4 bytes as the v4 address XOR 0xff. + ip := net.IP(make([]byte, 4)) + for i, byt := range na.IP[12:16] { + ip[i] = byt ^ 0xff + } + return ip.Mask(net.CIDRMask(16, 32)).String() + } + if IsOnionCatTor(na) { + // group is keyed off the first 4 bits of the actual onion key. + return fmt.Sprintf("tor:%d", na.IP[6]&((1<<4)-1)) + } + // OK, so now we know ourselves to be a IPv6 address. bitcoind uses /32 for everything, except for Hurricane + // Electric's (he.net) IP range, which it uses /36 for. + bits := 32 + if heNet.Contains(na.IP) { + bits = 36 + } + return na.IP.Mask(net.CIDRMask(bits, 128)).String() +} diff --git a/pkg/addrmgr/network_test.go b/pkg/addrmgr/network_test.go new file mode 100644 index 0000000..f5d8878 --- /dev/null +++ b/pkg/addrmgr/network_test.go @@ -0,0 +1,197 @@ +package addrmgr_test + +import ( + "net" + "testing" + + "github.com/p9c/p9/pkg/addrmgr" + "github.com/p9c/p9/pkg/wire" +) + +// TestIPTypes ensures the various functions which determine the type of an IP address based on RFCs work as intended. +func TestIPTypes(t *testing.T) { + type ipTest struct { + in wire.NetAddress + rfc1918 bool + rfc2544 bool + rfc3849 bool + rfc3927 bool + rfc3964 bool + rfc4193 bool + rfc4380 bool + rfc4843 bool + rfc4862 bool + rfc5737 bool + rfc6052 bool + rfc6145 bool + rfc6598 bool + local bool + valid bool + routable bool + } + newIPTest := func(ip string, rfc1918, rfc2544, rfc3849, rfc3927, rfc3964, + rfc4193, rfc4380, rfc4843, rfc4862, rfc5737, rfc6052, rfc6145, rfc6598, + local, valid, routable bool, + ) ipTest { + nip := net.ParseIP(ip) + na := *wire.NewNetAddressIPPort(nip, 11047, wire.SFNodeNetwork) + test := ipTest{na, rfc1918, rfc2544, rfc3849, rfc3927, rfc3964, rfc4193, rfc4380, + rfc4843, rfc4862, rfc5737, rfc6052, rfc6145, rfc6598, local, valid, routable, + } + return test + } + tests := []ipTest{ + newIPTest("10.255.255.255", true, false, false, false, false, false, + false, false, false, false, false, false, false, false, true, false, + ), + newIPTest("192.168.0.1", true, false, false, false, false, false, + false, false, false, false, false, false, false, false, true, false, + ), + newIPTest("172.31.255.1", true, false, false, false, false, false, + false, false, false, false, false, false, false, false, true, false, + ), + newIPTest("172.32.1.1", false, false, false, false, false, false, false, false, + false, false, false, false, false, false, true, true, + ), + newIPTest("169.254.250.120", false, false, false, true, false, false, + false, false, false, false, false, false, false, false, true, false, + ), + newIPTest("0.0.0.0", false, false, false, false, false, false, false, + false, false, false, false, false, false, true, false, false, + ), + newIPTest("255.255.255.255", false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, + ), + newIPTest("127.0.0.1", false, false, false, false, false, false, + false, false, false, false, false, false, false, true, true, false, + ), + newIPTest("fd00:dead::1", false, false, false, false, false, true, + false, false, false, false, false, false, false, false, true, false, + ), + newIPTest("2001::1", false, false, false, false, false, false, + true, false, false, false, false, false, false, false, true, true, + ), + newIPTest("2001:10:abcd::1:1", false, false, false, false, false, false, + false, true, false, false, false, false, false, false, true, false, + ), + newIPTest("fe80::1", false, false, false, false, false, false, + false, false, true, false, false, false, false, false, true, false, + ), + newIPTest("fe80:1::1", false, false, false, false, false, false, + false, false, false, false, false, false, false, false, true, true, + ), + newIPTest("64:ff9b::1", false, false, false, false, false, false, + false, false, false, false, true, false, false, false, true, true, + ), + newIPTest("::ffff:abcd:ef12:1", false, false, false, false, false, false, + false, false, false, false, false, false, false, false, true, true, + ), + newIPTest("::1", false, false, false, false, false, false, false, false, + false, false, false, false, false, true, true, false, + ), + newIPTest("198.18.0.1", false, true, false, false, false, false, false, + false, false, false, false, false, false, false, true, false, + ), + newIPTest("100.127.255.1", false, false, false, false, false, false, false, + false, false, false, false, false, true, false, true, false, + ), + newIPTest("203.0.113.1", false, false, false, false, false, false, false, + false, false, false, false, false, false, false, true, false, + ), + } + t.Logf("Running %d tests", len(tests)) + for _, test := range tests { + if rv := addrmgr.IsRFC1918(&test.in); rv != test.rfc1918 { + t.Errorf("IsRFC1918 %s\n got: %v want: %v", test.in.IP, rv, test.rfc1918) + } + if rv := addrmgr.IsRFC3849(&test.in); rv != test.rfc3849 { + t.Errorf("IsRFC3849 %s\n got: %v want: %v", test.in.IP, rv, test.rfc3849) + } + if rv := addrmgr.IsRFC3927(&test.in); rv != test.rfc3927 { + t.Errorf("IsRFC3927 %s\n got: %v want: %v", test.in.IP, rv, test.rfc3927) + } + if rv := addrmgr.IsRFC3964(&test.in); rv != test.rfc3964 { + t.Errorf("IsRFC3964 %s\n got: %v want: %v", test.in.IP, rv, test.rfc3964) + } + if rv := addrmgr.IsRFC4193(&test.in); rv != test.rfc4193 { + t.Errorf("IsRFC4193 %s\n got: %v want: %v", test.in.IP, rv, test.rfc4193) + } + if rv := addrmgr.IsRFC4380(&test.in); rv != test.rfc4380 { + t.Errorf("IsRFC4380 %s\n got: %v want: %v", test.in.IP, rv, test.rfc4380) + } + if rv := addrmgr.IsRFC4843(&test.in); rv != test.rfc4843 { + t.Errorf("IsRFC4843 %s\n got: %v want: %v", test.in.IP, rv, test.rfc4843) + } + if rv := addrmgr.IsRFC4862(&test.in); rv != test.rfc4862 { + t.Errorf("IsRFC4862 %s\n got: %v want: %v", test.in.IP, rv, test.rfc4862) + } + if rv := addrmgr.IsRFC6052(&test.in); rv != test.rfc6052 { + t.Errorf("isRFC6052 %s\n got: %v want: %v", test.in.IP, rv, test.rfc6052) + } + if rv := addrmgr.IsRFC6145(&test.in); rv != test.rfc6145 { + t.Errorf("IsRFC1918 %s\n got: %v want: %v", test.in.IP, rv, test.rfc6145) + } + if rv := addrmgr.IsLocal(&test.in); rv != test.local { + t.Errorf("IsLocal %s\n got: %v want: %v", test.in.IP, rv, test.local) + } + if rv := addrmgr.IsValid(&test.in); rv != test.valid { + t.Errorf("IsValid %s\n got: %v want: %v", test.in.IP, rv, test.valid) + } + if rv := addrmgr.IsRoutable(&test.in); rv != test.routable { + t.Errorf("IsRoutable %s\n got: %v want: %v", test.in.IP, rv, test.routable) + } + } +} + +// TestGroupKey tests the GroupKey function to ensure it properly groups various IP addresses. +func TestGroupKey(t *testing.T) { + tests := []struct { + name string + ip string + expected string + }{ + // Local addresses. + {name: "ipv4 localhost", ip: "127.0.0.1", expected: "local"}, + {name: "ipv6 localhost", ip: "::1", expected: "local"}, + {name: "ipv4 zero", ip: "0.0.0.0", expected: "local"}, + {name: "ipv4 first octet zero", ip: "0.1.2.3", expected: "local"}, + // Unroutable addresses. + {name: "ipv4 invalid bcast", ip: "255.255.255.255", expected: "unroutable"}, + {name: "ipv4 rfc1918 10/8", ip: "10.1.2.3", expected: "unroutable"}, + {name: "ipv4 rfc1918 172.16/12", ip: "172.16.1.2", expected: "unroutable"}, + {name: "ipv4 rfc1918 192.168/16", ip: "192.168.1.2", expected: "unroutable"}, + {name: "ipv6 rfc3849 2001:db8::/32", ip: "2001:db8::1234", expected: "unroutable"}, + {name: "ipv4 rfc3927 169.254/16", ip: "169.254.1.2", expected: "unroutable"}, + {name: "ipv6 rfc4193 fc00::/7", ip: "fc00::1234", expected: "unroutable"}, + {name: "ipv6 rfc4843 2001:10::/28", ip: "2001:10::1234", expected: "unroutable"}, + {name: "ipv6 rfc4862 fe80::/64", ip: "fe80::1234", expected: "unroutable"}, + // IPv4 normal. + {name: "ipv4 normal class a", ip: "12.1.2.3", expected: "12.1.0.0"}, + {name: "ipv4 normal class b", ip: "173.1.2.3", expected: "173.1.0.0"}, + {name: "ipv4 normal class c", ip: "196.1.2.3", expected: "196.1.0.0"}, + // IPv6/IPv4 translations. + {name: "ipv6 rfc3964 with ipv4 encap", ip: "2002:0c01:0203::", expected: "12.1.0.0"}, + {name: "ipv6 rfc4380 toredo ipv4", ip: "2001:0:1234::f3fe:fdfc", expected: "12.1.0.0"}, + {name: "ipv6 rfc6052 well-known prefix with ipv4", ip: "64:ff9b::0c01:0203", expected: "12.1.0.0"}, + {name: "ipv6 rfc6145 translated ipv4", ip: "::ffff:0:0c01:0203", expected: "12.1.0.0"}, + // Tor. + {name: "ipv6 tor onioncat", ip: "fd87:d87e:eb43:1234::5678", expected: "tor:2"}, + {name: "ipv6 tor onioncat 2", ip: "fd87:d87e:eb43:1245::6789", expected: "tor:2"}, + {name: "ipv6 tor onioncat 3", ip: "fd87:d87e:eb43:1345::6789", expected: "tor:3"}, + // IPv6 normal. + {name: "ipv6 normal", ip: "2602:100::1", expected: "2602:100::"}, + {name: "ipv6 normal 2", ip: "2602:0100::1234", expected: "2602:100::"}, + {name: "ipv6 hurricane electric", ip: "2001:470:1f10:a1::2", expected: "2001:470:1000::"}, + {name: "ipv6 hurricane electric 2", ip: "2001:0470:1f10:a1::2", expected: "2001:470:1000::"}, + } + for i, test := range tests { + nip := net.ParseIP(test.ip) + na := *wire.NewNetAddressIPPort(nip, 11047, wire.SFNodeNetwork) + if key := addrmgr.GroupKey(&na); key != test.expected { + t.Errorf("TestGroupKey #%d (%s): unexpected group key "+ + "- got '%s', want '%s'", i, test.name, + key, test.expected, + ) + } + } +} diff --git a/pkg/amt/amount.go b/pkg/amt/amount.go new file mode 100644 index 0000000..3643687 --- /dev/null +++ b/pkg/amt/amount.go @@ -0,0 +1,111 @@ +package amt + +import ( + "errors" + "math" + "strconv" +) + +// Unit describes a method of converting an Amount to something other than the base unit of a bitcoin. The value +// of the AmountUnit is the exponent component of the decadic multiple to convert from an amount in bitcoin to an amount +// counted in units. +type Unit int + +// These constants define various units used when describing a bitcoin monetary amount. +const ( + MegaDUO Unit = 6 + KiloDUO Unit = 3 + DUO Unit = 0 + MilliDUO Unit = -3 + MicroDUO Unit = -6 + Satoshi Unit = -8 +) + +// String returns the unit as a string. For recognized units, the SI prefix is used, or "Satoshi" for the base unit. For +// all unrecognized units, "1eN DUO" is returned, where N is the AmountUnit. +func (u Unit) String() string { + switch u { + case MegaDUO: + return "MDUO" + case KiloDUO: + return "kDUO" + case DUO: + return "DUO" + case MilliDUO: + return "mDUO" + case MicroDUO: + return "μDUO" + case Satoshi: + return "Satoshi" + default: + return "1e" + strconv.FormatInt(int64(u), 10) + " DUO" + } +} + +// Amount represents the base bitcoin monetary unit (colloquially referred to as a `Satoshi'). A single Amount is equal +// to 1e-8 of a bitcoin. +type Amount int64 + +func (a Amount) Int64() int64 { + return int64(a) +} + +// round converts a floating point number, which may or may not be representable as an integer, to the Amount integer +// type by rounding to the nearest integer. This is performed by adding or subtracting 0.5 depending on the sign, and +// relying on integer truncation to round the value to the nearest Amount. +func round(f float64) Amount { + if f < 0 { + return Amount(f - 0.5) + } + return Amount(f + 0.5) +} + +// NewAmount creates an Amount from a floating point value representing some value in bitcoin. NewAmount errors if f is +// NaN or +-Infinity, but does not check that the amount is within the total amount of bitcoin producible as f may not +// refer to an amount at a single moment in time. NewAmount is for specifically for converting DUO to Satoshi. For +// creating a new Amount with an int64 value which denotes a quantity of Satoshi, do a simple type conversion from type +// int64 to Amount. See GoDoc for example: http://godoc.org/github.com/p9c/monorepo/util#example-Amount +func NewAmount(f float64) (Amount, error) { + // The amount is only considered invalid if it cannot be represented as an integer type. This may happen if f is NaN + // or +-Infinity. + switch { + case math.IsNaN(f): + fallthrough + case math.IsInf(f, 1): + fallthrough + case math.IsInf(f, -1): + return 0, errors.New("invalid bitcoin amount") + } + return round(f * float64(SatoshiPerBitcoin)), nil +} + +// ToUnit converts a monetary amount counted in bitcoin base units to a floating point value representing an amount of +// bitcoin. +func (a Amount) ToUnit(u Unit) float64 { + return float64(a) / math.Pow10(int(u+8)) +} + +// ToDUO is the equivalent of calling ToUnit with AmountDUO. +func (a Amount) ToDUO() float64 { + return a.ToUnit(DUO) +} + +// Format formats a monetary amount counted in bitcoin base units as a string for a given unit. The conversion will +// succeed for any unit, however, known units will be formated with an appended label describing the units with SI +// notation, or "Satoshi" for the base unit. +func (a Amount) Format(u Unit) string { + units := " " + u.String() + return strconv.FormatFloat(a.ToUnit(u), 'f', -int(u+8), 64) + units +} + +// String is the equivalent of calling Format with AmountDUO. +func (a Amount) String() string { + return a.Format(DUO) +} + +// MulF64 multiplies an Amount by a floating point value. While this is not an operation that must typically be done by +// a full node or wallet, it is useful for services that podbuild on top of bitcoin (for example, calculating a fee by +// multiplying by a percentage). +func (a Amount) MulF64(f float64) Amount { + return round(float64(a) * f) +} diff --git a/pkg/amt/amount_test.go b/pkg/amt/amount_test.go new file mode 100644 index 0000000..4fc70cf --- /dev/null +++ b/pkg/amt/amount_test.go @@ -0,0 +1,291 @@ +package amt_test + +import ( + "github.com/p9c/p9/pkg/amt" + "math" + "testing" +) + +func TestAmountCreation(t *testing.T) { + tests := []struct { + name string + amount float64 + valid bool + expected amt.Amount + }{ + // Positive tests. + { + name: "zero", + amount: 0, + valid: true, + expected: 0, + }, + { + name: "max producible", + amount: 21e6, + valid: true, + expected: amt.MaxSatoshi, + }, + { + name: "min producible", + amount: -21e6, + valid: true, + expected: -amt.MaxSatoshi, + }, + { + name: "exceeds max producible", + amount: 21e6 + 1e-8, + valid: true, + expected: amt.MaxSatoshi + 1, + }, + { + name: "exceeds min producible", + amount: -21e6 - 1e-8, + valid: true, + expected: -amt.MaxSatoshi - 1, + }, + { + name: "one hundred", + amount: 100, + valid: true, + expected: 100 * amt.SatoshiPerBitcoin, + }, + { + name: "fraction", + amount: 0.01234567, + valid: true, + expected: 1234567, + }, + { + name: "rounding up", + amount: 54.999999999999943157, + valid: true, + expected: 55 * amt.SatoshiPerBitcoin, + }, + { + name: "rounding down", + amount: 55.000000000000056843, + valid: true, + expected: 55 * amt.SatoshiPerBitcoin, + }, + // Negative tests. + { + name: "not-a-number", + amount: math.NaN(), + valid: false, + }, + { + name: "-infinity", + amount: math.Inf(-1), + valid: false, + }, + { + name: "+infinity", + amount: math.Inf(1), + valid: false, + }, + } + for _, test := range tests { + a, e := amt.NewAmount(test.amount) + switch { + case test.valid && e != nil: + t.Errorf("%v: Positive test Amount creation failed with: %v", test.name, e) + continue + case !test.valid && e == nil: + t.Errorf("%v: Negative test Amount creation succeeded (value %v) when should fail", test.name, a) + continue + } + if a != test.expected { + t.Errorf("%v: Created amount %v does not match expected %v", test.name, a, test.expected) + continue + } + } +} +func TestAmountUnitConversions(t *testing.T) { + tests := []struct { + name string + amount amt.Amount + unit amt.Unit + converted float64 + s string + }{ + { + name: "MDUO", + amount: amt.MaxSatoshi, + unit: amt.MegaDUO, + converted: 21, + s: "21 MDUO", + }, + { + name: "kDUO", + amount: 44433322211100, + unit: amt.KiloDUO, + converted: 444.33322211100, + s: "444.333222111 kDUO", + }, + { + name: "DUO", + amount: 44433322211100, + unit: amt.DUO, + converted: 444333.22211100, + s: "444333.222111 DUO", + }, + { + name: "mDUO", + amount: 44433322211100, + unit: amt.MilliDUO, + converted: 444333222.11100, + s: "444333222.111 mDUO", + }, + { + name: "μDUO", + amount: 44433322211100, + unit: amt.MicroDUO, + converted: 444333222111.00, + s: "444333222111 μDUO", + }, + { + name: "satoshi", + amount: 44433322211100, + unit: amt.Satoshi, + converted: 44433322211100, + s: "44433322211100 Satoshi", + }, + { + name: "non-standard unit", + amount: 44433322211100, + unit: amt.Unit(-1), + converted: 4443332.2211100, + s: "4443332.22111 1e-1 DUO", + }, + } + for _, test := range tests { + f := test.amount.ToUnit(test.unit) + if f != test.converted { + t.Errorf("%v: converted value %v does not match expected %v", test.name, f, test.converted) + continue + } + s := test.amount.Format(test.unit) + if s != test.s { + t.Errorf("%v: format '%v' does not match expected '%v'", test.name, s, test.s) + continue + } + // Verify that Amount.ToDUO works as advertised. + f1 := test.amount.ToUnit(amt.DUO) + f2 := test.amount.ToDUO() + if f1 != f2 { + t.Errorf("%v: ToDUO does not match ToUnit(AmountDUO): %v != %v", test.name, f1, f2) + } + // Verify that Amount.String works as advertised. + s1 := test.amount.Format(amt.DUO) + s2 := test.amount.String() + if s1 != s2 { + t.Errorf("%v: String does not match Format(AmountBitcoin): %v != %v", test.name, s1, s2) + } + } +} +func TestAmountMulF64(t *testing.T) { + tests := []struct { + name string + amt amt.Amount + mul float64 + res amt.Amount + }{ + { + name: "Multiply 0.1 DUO by 2", + amt: 100e5, // 0.1 DUO + mul: 2, + res: 200e5, // 0.2 DUO + }, + { + name: "Multiply 0.2 DUO by 0.02", + amt: 200e5, // 0.2 DUO + mul: 1.02, + res: 204e5, // 0.204 DUO + }, + { + name: "Multiply 0.1 DUO by -2", + amt: 100e5, // 0.1 DUO + mul: -2, + res: -200e5, // -0.2 DUO + }, + { + name: "Multiply 0.2 DUO by -0.02", + amt: 200e5, // 0.2 DUO + mul: -1.02, + res: -204e5, // -0.204 DUO + }, + { + name: "Multiply -0.1 DUO by 2", + amt: -100e5, // -0.1 DUO + mul: 2, + res: -200e5, // -0.2 DUO + }, + { + name: "Multiply -0.2 DUO by 0.02", + amt: -200e5, // -0.2 DUO + mul: 1.02, + res: -204e5, // -0.204 DUO + }, + { + name: "Multiply -0.1 DUO by -2", + amt: -100e5, // -0.1 DUO + mul: -2, + res: 200e5, // 0.2 DUO + }, + { + name: "Multiply -0.2 DUO by -0.02", + amt: -200e5, // -0.2 DUO + mul: -1.02, + res: 204e5, // 0.204 DUO + }, + { + name: "Round down", + amt: 49, // 49 Satoshis + mul: 0.01, + res: 0, + }, + { + name: "Round up", + amt: 50, // 50 Satoshis + mul: 0.01, + res: 1, // 1 Satoshi + }, + { + name: "Multiply by 0.", + amt: 1e8, // 1 DUO + mul: 0, + res: 0, // 0 DUO + }, + { + name: "Multiply 1 by 0.5.", + amt: 1, // 1 Satoshi + mul: 0.5, + res: 1, // 1 Satoshi + }, + { + name: "Multiply 100 by 66%.", + amt: 100, // 100 Satoshis + mul: 0.66, + res: 66, // 66 Satoshis + }, + { + name: "Multiply 100 by 66.6%.", + amt: 100, // 100 Satoshis + mul: 0.666, + res: 67, // 67 Satoshis + }, + { + name: "Multiply 100 by 2/3.", + amt: 100, // 100 Satoshis + mul: 2.0 / 3, + res: 67, // 67 Satoshis + }, + } + for _, test := range tests { + a := test.amt.MulF64(test.mul) + if a != test.res { + t.Errorf("%v: expected %v got %v", test.name, test.res, a) + } + } +} diff --git a/pkg/amt/const.go b/pkg/amt/const.go new file mode 100644 index 0000000..de3d652 --- /dev/null +++ b/pkg/amt/const.go @@ -0,0 +1,10 @@ +package amt + +const ( + // SatoshiPerBitcent is the number of satoshi in one bitcoin cent. + SatoshiPerBitcent Amount = 1e6 + // SatoshiPerBitcoin is the number of satoshi in one bitcoin (1 DUO). + SatoshiPerBitcoin Amount = 1e8 + // MaxSatoshi is the maximum transaction amount allowed in satoshi. + MaxSatoshi = 21e6 * SatoshiPerBitcoin +) diff --git a/pkg/appdata/appdata.go b/pkg/appdata/appdata.go new file mode 100644 index 0000000..ea05fa1 --- /dev/null +++ b/pkg/appdata/appdata.go @@ -0,0 +1,88 @@ +package appdata + +import ( + "os" + "os/user" + "path/filepath" + "runtime" + "strings" + "unicode" +) + +// GetDataDir returns an operating system specific directory to be used for +// storing application data for an application. +// See Dir for more details. This unexported version takes an operating system argument primarily to enable the testing +// package to properly test the function by forcing an operating system that is not the currently one. +func GetDataDir(goos, appName string, roaming bool) string { + if appName == "" || appName == "." { + return "." + } + // The caller really shouldn't prepend the appName with a period, but if they do, handle it gracefully by trimming + // it. + appName = strings.TrimPrefix(appName, ".") + appNameUpper := string(unicode.ToUpper(rune(appName[0]))) + appName[1:] + appNameLower := string(unicode.ToLower(rune(appName[0]))) + appName[1:] + // Get the OS specific home directory via the Go standard lib. + var homeDir string + var usr *user.User + var e error + if usr, e = user.Current(); e == nil { + homeDir = usr.HomeDir + } + // Fall back to standard HOME environment variable that works for most POSIX OSes if the directory from the Go + // standard lib failed. + if e != nil || homeDir == "" { + homeDir = os.Getenv("HOME") + } + switch goos { + // Attempt to use the LOCALAPPDATA or APPDATA environment variable on Windows. + case "windows": + // Windows XP and before didn't have a LOCALAPPDATA, so fallback to regular APPDATA when LOCALAPPDATA is not + // set. + appData := os.Getenv("LOCALAPPDATA") + if roaming || appData == "" { + appData = os.Getenv("APPDATA") + } + if appData != "" { + return filepath.Join(appData, appNameUpper) + } + case "darwin": + if homeDir != "" { + return filepath.Join( + homeDir, "Library", + "Application Support", appNameUpper, + ) + } + case "plan9": + if homeDir != "" { + return filepath.Join(homeDir, appNameLower) + } + default: + if homeDir != "" { + return filepath.Join(homeDir, "."+appNameLower) + } + } + // Fall back to the current directory if all else fails. + return "." +} + +// Dir returns an operating system specific directory to be used for storing application data for an application. The +// appName parameter is the name of the application the data directory is being requested for. This function will +// prepend a period to the appName for POSIX style operating systems since that is standard practice. +// +// An empty appName or one with a single dot is treated as requesting the current directory so only "." will be +// returned. Further, the first character of appName will be made lowercase for POSIX style operating systems and +// uppercase for Mac and Windows since that is standard practice. +// +// The roaming parameter only applies to Windows where it specifies the roaming application data profile (%APPDATA%) +// should be used instead of the local one (%LOCALAPPDATA%) that is used by default. Example results: +// +// dir := Dir("myapp", false) +// +// POSIX (Linux/BSD): ~/.myapp +// Mac OS: $HOME/Library/Application Support/Myapp +// Windows: %LOCALAPPDATA%\Myapp +// Plan 9: $home/myapp +func Dir(appName string, roaming bool) string { + return GetDataDir(runtime.GOOS, appName, roaming) +} diff --git a/pkg/appdata/appdata_test.go b/pkg/appdata/appdata_test.go new file mode 100644 index 0000000..b50abe4 --- /dev/null +++ b/pkg/appdata/appdata_test.go @@ -0,0 +1,126 @@ +package appdata_test + +import ( + "os" + "os/user" + "path/filepath" + "runtime" + "testing" + "unicode" + + "github.com/p9c/p9/pkg/appdata" +) + +// TestAppDataDir tests the API for Dir to ensure it gives expected results for various operating systems. +func TestAppDataDir(t *testing.T) { + // App name plus upper and lowercase variants. + appName := "myapp" + appNameUpper := string(unicode.ToUpper(rune(appName[0]))) + appName[1:] + appNameLower := string(unicode.ToLower(rune(appName[0]))) + appName[1:] + // When we're on Windows, set the expected local and roaming directories per the environment vars. When we aren't + // on Windows, the function should return the current directory when forced to provide the Windows path since the + // environment variables won't exist. + winLocal := "." + winRoaming := "." + if runtime.GOOS == "windows" { + localAppData := os.Getenv("LOCALAPPDATA") + roamingAppData := os.Getenv("APPDATA") + if localAppData == "" { + localAppData = roamingAppData + } + winLocal = filepath.Join(localAppData, appNameUpper) + winRoaming = filepath.Join(roamingAppData, appNameUpper) + } + // Get the home directory to use for testing expected results. + var homeDir string + usr, e := user.Current() + if e != nil { + t.Errorf("user.Current: %v", e) + return + } + homeDir = usr.HomeDir + // Mac node data directory. + macAppData := filepath.Join(homeDir, "Library", "Application Support") + tests := []struct { + goos string + appName string + roaming bool + want string + }{ + // Various combinations of application name casing, leading period, operating system, and roaming flags. + {"windows", appNameLower, false, winLocal}, + {"windows", appNameUpper, false, winLocal}, + {"windows", "." + appNameLower, false, winLocal}, + {"windows", "." + appNameUpper, false, winLocal}, + {"windows", appNameLower, true, winRoaming}, + {"windows", appNameUpper, true, winRoaming}, + {"windows", "." + appNameLower, true, winRoaming}, + {"windows", "." + appNameUpper, true, winRoaming}, + {"linux", appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"linux", appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"linux", "." + appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"linux", "." + appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"darwin", appNameLower, false, filepath.Join(macAppData, appNameUpper)}, + {"darwin", appNameUpper, false, filepath.Join(macAppData, appNameUpper)}, + {"darwin", "." + appNameLower, false, filepath.Join(macAppData, appNameUpper)}, + {"darwin", "." + appNameUpper, false, filepath.Join(macAppData, appNameUpper)}, + {"openbsd", appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"openbsd", appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"openbsd", "." + appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"openbsd", "." + appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"freebsd", appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"freebsd", appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"freebsd", "." + appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"freebsd", "." + appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"netbsd", appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"netbsd", appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"netbsd", "." + appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"netbsd", "." + appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"plan9", appNameLower, false, filepath.Join(homeDir, appNameLower)}, + {"plan9", appNameUpper, false, filepath.Join(homeDir, appNameLower)}, + {"plan9", "." + appNameLower, false, filepath.Join(homeDir, appNameLower)}, + {"plan9", "." + appNameUpper, false, filepath.Join(homeDir, appNameLower)}, + {"unrecognized", appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"unrecognized", appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + {"unrecognized", "." + appNameLower, false, filepath.Join(homeDir, "."+appNameLower)}, + {"unrecognized", "." + appNameUpper, false, filepath.Join(homeDir, "."+appNameLower)}, + // No application name provided, so expect current directory. + {"windows", "", false, "."}, + {"windows", "", true, "."}, + {"linux", "", false, "."}, + {"darwin", "", false, "."}, + {"openbsd", "", false, "."}, + {"freebsd", "", false, "."}, + {"netbsd", "", false, "."}, + {"plan9", "", false, "."}, + {"unrecognized", "", false, "."}, + // Single dot provided for application name, so expect current + // directory. + {"windows", ".", false, "."}, + {"windows", ".", true, "."}, + {"linux", ".", false, "."}, + {"darwin", ".", false, "."}, + {"openbsd", ".", false, "."}, + {"freebsd", ".", false, "."}, + {"netbsd", ".", false, "."}, + {"plan9", ".", false, "."}, + {"unrecognized", ".", false, "."}, + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + ret := TstAppDataDir(test.goos, test.appName, test.roaming) + if ret != test.want { + t.Errorf( + "AppDataDir #%d (%s) does not match - "+ + "expected got %s, want %s", i, test.goos, ret, + test.want, + ) + continue + } + } +} + +// TstAppDataDir makes the internal appDataDir function available to the test package. +func TstAppDataDir(goos, appName string, roaming bool) string { + return appdata.GetDataDir(goos, appName, roaming) +} diff --git a/pkg/apputil/apputil.go b/pkg/apputil/apputil.go new file mode 100644 index 0000000..6df6db1 --- /dev/null +++ b/pkg/apputil/apputil.go @@ -0,0 +1,62 @@ +package apputil + +import ( + "os" + "path/filepath" + "runtime" +) + +// EnsureDir checks a file could be written to a path, creates the directories as needed +func EnsureDir(fileName string) { + dirName := filepath.Dir(fileName) + if _, serr := os.Stat(dirName); serr != nil { + merr := os.MkdirAll(dirName, os.ModePerm) + if merr != nil { + panic(merr) + } + } +} + +// FileExists reports whether the named file or directory exists. +func FileExists(filePath string) bool { + _, e := os.Stat(filePath) + return e == nil +} + +// MinUint32 is a helper function to return the minimum of two uint32s. This avoids a math import and the need to cast +// to floats. +func MinUint32(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +// PrependForWindows runs a command with a terminal +func PrependForWindows(args []string) []string { + if runtime.GOOS == "windows" { + args = append( + []string{ + "cmd.exe", + "/C", + }, + args..., + ) + } + return args +} + +// PrependForWindowsWithStart runs a process independently +func PrependForWindowsWithStart(args []string) []string { + if runtime.GOOS == "windows" { + args = append( + []string{ + "cmd.exe", + "/C", + "start", + }, + args..., + ) + } + return args +} diff --git a/pkg/base58/README.md b/pkg/base58/README.md new file mode 100755 index 0000000..03a16cc --- /dev/null +++ b/pkg/base58/README.md @@ -0,0 +1,34 @@ +# base58 + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/parallelcoin/pod/btcutil/base58) + +Package base58 provides an API for encoding and decoding to and from the +modified base58 encoding. It also provides an API to do Base58Check encoding, as +described [here](https://en.bitcoin.it/wiki/Base58Check_encoding). A +comprehensive suite of tests is provided to ensure proper functionality. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/btcutil/base58 +``` + +## Examples + +- [Decode Example](http://godoc.org/github.com/p9c/p9/base58#example-Decode) + Demonstrates how to decode modified base58 encoded data. + +- [Encode Example](http://godoc.org/github.com/p9c/p9/base58#example-Encode) + Demonstrates how to encode data using the modified base58 encoding scheme. + +- [CheckDecode Example](http://godoc.org/github.com/p9c/p9/base58#example-CheckDecode) + Demonstrates how to decode Base58Check encoded data. + +- [CheckEncode Example](http://godoc.org/github.com/p9c/p9/base58#example-CheckEncode) + Demonstrates how to encode data using the Base58Check encoding scheme. + +## License + +Package base58 is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/base58/alphabet.go b/pkg/base58/alphabet.go new file mode 100644 index 0000000..11525fb --- /dev/null +++ b/pkg/base58/alphabet.go @@ -0,0 +1,44 @@ +//go:generate go run -tags generate ./genalphabet/. +// AUTOGENERATED by genalphabet.go; do not edit. + +package base58 + +const ( + // alphabet is the modified base58 alphabet used by Bitcoin. + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + alphabetIdx0 = '1' +) +var b58 = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 255, 255, 255, 255, 255, 255, + 255, 9, 10, 11, 12, 13, 14, 15, + 16, 255, 17, 18, 19, 20, 21, 255, + 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 255, 255, 255, 255, 255, + 255, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 255, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, + 55, 56, 57, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, +} diff --git a/pkg/base58/base58.go b/pkg/base58/base58.go new file mode 100644 index 0000000..c3ef306 --- /dev/null +++ b/pkg/base58/base58.go @@ -0,0 +1,61 @@ +package base58 + +import ( + "math/big" +) + +var bigRadix = big.NewInt(58) +var bigZero = big.NewInt(0) + +// Decode decodes a modified base58 string to a byte slice. +func Decode(b string) []byte { + answer := big.NewInt(0) + j := big.NewInt(1) + scratch := new(big.Int) + for i := len(b) - 1; i >= 0; i-- { + tmp := b58[b[i]] + if tmp == 255 { + return []byte("") + } + scratch.SetInt64(int64(tmp)) + scratch.Mul(j, scratch) + answer.Add(answer, scratch) + j.Mul(j, bigRadix) + } + tmpval := answer.Bytes() + var numZeros int + for numZeros = 0; numZeros < len(b); numZeros++ { + if b[numZeros] != alphabetIdx0 { + break + } + } + flen := numZeros + len(tmpval) + val := make([]byte, flen) + copy(val[numZeros:], tmpval) + return val +} + +// Encode encodes a byte slice to a modified base58 string. +func Encode(b []byte) string { + x := new(big.Int) + x.SetBytes(b) + answer := make([]byte, 0, len(b)*136/100) + for x.Cmp(bigZero) > 0 { + mod := new(big.Int) + x.DivMod(x, bigRadix, mod) + answer = append(answer, alphabet[mod.Int64()]) + } + // leading zero bytes + for _, i := range b { + if i != 0 { + break + } + answer = append(answer, alphabetIdx0) + } + // reverse + alen := len(answer) + for i := 0; i < alen/2; i++ { + answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i] + } + return string(answer) +} diff --git a/pkg/base58/base58_test.go b/pkg/base58/base58_test.go new file mode 100644 index 0000000..4b0b0a5 --- /dev/null +++ b/pkg/base58/base58_test.go @@ -0,0 +1,95 @@ +package base58_test + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/p9c/p9/pkg/base58" +) + +var stringTests = []struct { + in string + out string +}{ + {"", ""}, + {" ", "Z"}, + {"-", "n"}, + {"0", "q"}, + {"1", "r"}, + {"-1", "4SU"}, + {"11", "4k8"}, + {"abc", "ZiCa"}, + {"1234598760", "3mJr7AoUXx2Wqd"}, + {"abcdefghijklmnopqrstuvwxyz", "3yxU3u1igY8WkgtjK92fbJQCd4BZiiT1v25f"}, + {"00000000000000000000000000000000000000000000000000000000000000", + "3sN2THZeE9Eh9eYrwkvZqNstbHGvrxSAM7gXUXvyFQP8XvQLUqNCS27icwUeDT7ckHm4FUHM2mTVh1vbLmk7y", + }, +} +var invalidStringTests = []struct { + in string + out string +}{ + {"0", ""}, + {"O", ""}, + {"I", ""}, + {"l", ""}, + {"3mJr0", ""}, + {"O3yxU", ""}, + {"3sNI", ""}, + {"4kl8", ""}, + {"0OIl", ""}, + {"!@#$%^&*()-_=+~`", ""}, +} +var hexTests = []struct { + in string + out string +}{ + {"61", "2g"}, + {"626262", "a3gV"}, + {"636363", "aPEr"}, + {"73696d706c792061206c6f6e6720737472696e67", "2cFupjhnEsSn59qHXstmK2ffpLv2"}, + {"00eb15231dfceb60925886b67d065299925915aeb172c06647", "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"}, + {"516b6fcd0f", "ABnLTmg"}, + {"bf4f89001e670274dd", "3SEo3LWLoPntC"}, + {"572e4794", "3EFU7m"}, + {"ecac89cad93923c02321", "EJDM8drfXA6uyA"}, + {"10c8511e", "Rt5zm"}, + {"00000000000000000000", "1111111111"}, +} + +func TestBase58(t *testing.T) { + // Encode tests + for x, test := range stringTests { + tmp := []byte(test.in) + if res := base58.Encode(tmp); res != test.out { + t.Errorf("Encode test #%d failed: got: %s want: %s", + x, res, test.out, + ) + continue + } + } + // Decode tests + for x, test := range hexTests { + b, e := hex.DecodeString(test.in) + if e != nil { + t.Errorf("hex.DecodeString failed failed #%d: got: %s", x, test.in) + continue + } + if res := base58.Decode(test.out); !bytes.Equal(res, b) { + t.Errorf("Decode test #%d failed: got: %q want: %q", + x, res, test.in, + ) + continue + } + } + // Decode with invalid input + for x, test := range invalidStringTests { + if res := base58.Decode(test.in); string(res) != test.out { + t.Errorf("Decode invalidString test #%d failed: got: %q want: %q", + x, res, test.out, + ) + continue + } + } +} diff --git a/pkg/base58/base58bench_test.go b/pkg/base58/base58bench_test.go new file mode 100644 index 0000000..6b5ae75 --- /dev/null +++ b/pkg/base58/base58bench_test.go @@ -0,0 +1,28 @@ +package base58_test + +import ( + "bytes" + "testing" + + "github.com/p9c/p9/pkg/base58" +) + +func BenchmarkBase58Encode(b *testing.B) { + b.StopTimer() + data := bytes.Repeat([]byte{0xff}, 5000) + b.SetBytes(int64(len(data))) + b.StartTimer() + for i := 0; i < b.N; i++ { + base58.Encode(data) + } +} +func BenchmarkBase58Decode(b *testing.B) { + b.StopTimer() + data := bytes.Repeat([]byte{0xff}, 5000) + encoded := base58.Encode(data) + b.SetBytes(int64(len(encoded))) + b.StartTimer() + for i := 0; i < b.N; i++ { + base58.Decode(encoded) + } +} diff --git a/pkg/base58/base58check.go b/pkg/base58/base58check.go new file mode 100644 index 0000000..e74ea91 --- /dev/null +++ b/pkg/base58/base58check.go @@ -0,0 +1,47 @@ +package base58 + +import ( + "crypto/sha256" + "errors" +) + +// ErrChecksum indicates that the checksum of a check-encoded string does not verify against the checksum. +var ErrChecksum = errors.New("checksum error") + +// ErrInvalidFormat indicates that the check-encoded string has an invalid format. +var ErrInvalidFormat = errors.New("invalid format: version and/or checksum bytes missing") + +// checksum: first four bytes of sha256^2 +func checksum(input []byte) (cksum [4]byte) { + h := sha256.Sum256(input) + h2 := sha256.Sum256(h[:]) + copy(cksum[:], h2[:4]) + return +} + +// CheckEncode prepends a version byte and appends a four byte checksum. +func CheckEncode(input []byte, version byte) string { + b := make([]byte, 0, 1+len(input)+4) + b = append(b, version) + b = append(b, input[:]...) + cksum := checksum(b) + b = append(b, cksum[:]...) + return Encode(b) +} + +// CheckDecode decodes a string that was encoded with CheckEncode and verifies the checksum. +func CheckDecode(input string) (result []byte, version byte, e error) { + decoded := Decode(input) + if len(decoded) < 5 { + return nil, 0, ErrInvalidFormat + } + version = decoded[0] + var cksum [4]byte + copy(cksum[:], decoded[len(decoded)-4:]) + if checksum(decoded[:len(decoded)-4]) != cksum { + return nil, 0, ErrChecksum + } + payload := decoded[1 : len(decoded)-4] + result = append(result, payload...) + return +} diff --git a/pkg/base58/base58check_test.go b/pkg/base58/base58check_test.go new file mode 100644 index 0000000..97b87a3 --- /dev/null +++ b/pkg/base58/base58check_test.go @@ -0,0 +1,61 @@ +package base58_test + +import ( + "testing" + + "github.com/p9c/p9/pkg/base58" +) + +var checkEncodingStringTests = []struct { + version byte + in string + out string +}{ + {20, "", "3MNQE1X"}, + {20, " ", "B2Kr6dBE"}, + {20, "-", "B3jv1Aft"}, + {20, "0", "B482yuaX"}, + {20, "1", "B4CmeGAC"}, + {20, "-1", "mM7eUf6kB"}, + {20, "11", "mP7BMTDVH"}, + {20, "abc", "4QiVtDjUdeq"}, + {20, "1234598760", "ZmNb8uQn5zvnUohNCEPP"}, + {20, "abcdefghijklmnopqrstuvwxyz", "K2RYDcKfupxwXdWhSAxQPCeiULntKm63UXyx5MvEH2"}, + {20, "00000000000000000000000000000000000000000000000000000000000000", + "bi1EWXwJay2udZVxLJozuTb8Meg4W9c6xnmJaRDjg6pri5MBAxb9XwrpQXbtnqEoRV5U2pixnFfwyXC8tRAVC8XxnjK", + }, +} + +func TestBase58Check(t *testing.T) { + for x, test := range checkEncodingStringTests { + // test encoding + if res := base58.CheckEncode([]byte(test.in), test.version); res != test.out { + t.Errorf("CheckEncode test #%d failed: got %s, want: %s", x, res, test.out) + } + // test decoding + res, version, e := base58.CheckDecode(test.out) + if e != nil { + t.Errorf("CheckDecode test #%d failed with e: %v", x, e) + } else if version != test.version { + t.Errorf("CheckDecode test #%d failed: got version: %d want: %d", x, version, test.version) + } else if string(res) != test.in { + t.Errorf("CheckDecode test #%d failed: got: %s want: %s", x, res, test.in) + } + } + // test the two decoding failure cases + // case 1: checksum error + var e error + _, _, e = base58.CheckDecode("3MNQE1Y") + if e != base58.ErrChecksum { + t.Error("Checkdecode test failed, expected ErrChecksum") + } + // case 2: invalid formats (string lengths below 5 mean the version byte and/or the checksum bytes are missing). + testString := "" + for length := 0; length < 4; length++ { + // make a string of length `len` + _, _, e = base58.CheckDecode(testString) + if e != base58.ErrInvalidFormat { + t.Error("Checkdecode test failed, expected ErrInvalidFormat") + } + } +} diff --git a/pkg/base58/doc.go b/pkg/base58/doc.go new file mode 100644 index 0000000..cf931eb --- /dev/null +++ b/pkg/base58/doc.go @@ -0,0 +1,19 @@ +/*Package base58 provides an API for working with modified base58 and Base58Check encodings. + +Modified Base58 Encoding + +Standard base58 encoding is similar to standard base64 encoding except, as the name implies, it uses a 58 character +alphabet which results in an alphanumeric string and allows some characters which are problematic for humans to be +excluded. Due to this, there can be various base58 alphabets. + +The modified base58 alphabet used by Bitcoin, and hence this package, omits the 0, O, I, and l characters that look the +same in many fonts and are therefore hard to humans to distinguish. + +Base58Check Encoding Scheme + +The Base58Check encoding scheme is primarily used for Bitcoin addresses at the time of this writing, however it can be +used to generically encode arbitrary byte arrays into human-readable strings along with a version byte that can be used +to differentiate the same payload. For Bitcoin addresses, the extra version is used to differentiate the network of +otherwise identical public keys which helps prevent using an address intended for one network on another. +*/ +package base58 diff --git a/pkg/base58/example_test.go b/pkg/base58/example_test.go new file mode 100644 index 0000000..47965df --- /dev/null +++ b/pkg/base58/example_test.go @@ -0,0 +1,57 @@ +package base58_test + +import ( + "fmt" + + "github.com/p9c/p9/pkg/base58" +) + +// This example demonstrates how to decode modified base58 encoded data. +func ExampleDecode() { + // Decode example modified base58 encoded data. + encoded := "25JnwSn7XKfNQ" + decoded := base58.Decode(encoded) + // Show the decoded data. + fmt.Println("Decoded Data:", string(decoded)) + // Output: + // Decoded Data: Test data +} + +// This example demonstrates how to encode data using the modified base58 encoding scheme. +func ExampleEncode() { + // Encode example data with the modified base58 encoding scheme. + data := []byte("Test data") + encoded := base58.Encode(data) + // Show the encoded data. + fmt.Println("Encoded Data:", encoded) + // Output: + // Encoded Data: 25JnwSn7XKfNQ +} + +// This example demonstrates how to decode Base58Check encoded data. +func ExampleCheckDecode() { + // Decode an example Base58Check encoded data. + encoded := "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa" + decoded, version, e := base58.CheckDecode(encoded) + if e != nil { + fmt.Println(e) + return + } + // Show the decoded data. + fmt.Printf("Decoded data: %x\n", decoded) + fmt.Println("Version Byte:", version) + // Output: + // Decoded data: 62e907b15cbf27d5425399ebf6f0fb50ebb88f18 + // Version Byte: 0 +} + +// This example demonstrates how to encode data using the Base58Check encoding scheme. +func ExampleCheckEncode() { + // Encode example data with the Base58Check encoding scheme. + data := []byte("Test data") + encoded := base58.CheckEncode(data, 0) + // Show the encoded data. + fmt.Println("Encoded Data:", encoded) + // Output: + // Encoded Data: 182iP79GRURMp7oMHDU +} diff --git a/pkg/base58/genalphabet/genalphabet.go b/pkg/base58/genalphabet/genalphabet.go new file mode 100644 index 0000000..7a23acb --- /dev/null +++ b/pkg/base58/genalphabet/genalphabet.go @@ -0,0 +1,65 @@ +// +build generate + +package main + +import ( + "bytes" + "io" + "os" + "strconv" +) + +var ( + end = []byte(`}`) + start = []byte(`//go:generate go run -tags generate ./genalphabet/. +// AUTOGENERATED by genalphabet.go; do not edit. + +package base58 + +const ( + // alphabet is the modified base58 alphabet used by Bitcoin. + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + alphabetIdx0 = '1' +) +var b58 = [256]byte{`) + alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + tab = []byte("\t") + invalid = []byte("255") + comma = []byte(",") + space = []byte(" ") + nl = []byte("\n") +) + +func write(w io.Writer, b []byte) { + _, e := w.Write(b) + if e != nil { + F.Ln(e) + } +} +func main() { + fi, e := os.Create("alphabet.go") + if e != nil { + F.Ln(e) + } + defer fi.Close() + write(fi, start) + write(fi, nl) + for i := byte(0); i < 32; i++ { + write(fi, tab) + for j := byte(0); j < 8; j++ { + idx := bytes.IndexByte(alphabet, i*8+j) + if idx == -1 { + write(fi, invalid) + } else { + write(fi, strconv.AppendInt(nil, int64(idx), 10)) + } + write(fi, comma) + if j != 7 { + write(fi, space) + } + } + write(fi, nl) + } + write(fi, end) + write(fi, nl) +} diff --git a/pkg/base58/genalphabet/log.go b/pkg/base58/genalphabet/log.go new file mode 100644 index 0000000..0aeba74 --- /dev/null +++ b/pkg/base58/genalphabet/log.go @@ -0,0 +1,45 @@ +// +build generate + +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/base58/log.go b/pkg/base58/log.go new file mode 100644 index 0000000..e84217d --- /dev/null +++ b/pkg/base58/log.go @@ -0,0 +1,43 @@ +package base58 + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/bits/bits.go b/pkg/bits/bits.go new file mode 100644 index 0000000..8704b40 --- /dev/null +++ b/pkg/bits/bits.go @@ -0,0 +1,79 @@ +package bits + +import "math/big" + +// CompactToBig converts a compact representation of a whole number N to an unsigned 32-bit number. The representation +// is similar to IEEE754 floating point numbers. Like IEEE754 floating point, there are three basic components: the +// sign, the exponent, and the mantissa. They are broken out as follows: +// +// * the most significant 8 bits represent the unsigned base 256 exponent +// * bit 23 (the 24th bit) represents the sign bit +// * the least significant 23 bits represent the mantissa +// ------------------------------------------------- +// | Exponent | Sign | Mantissa | +// ------------------------------------------------- +// | 8 bits [31-24] | 1 bit [23] | 23 bits [22-00] | +// ------------------------------------------------- +// +// The formula to calculate N is: +// +// N = (-1^sign) * mantissa * 256^(exponent-3) +// +// This compact form is only used in bitcoin to encode unsigned 256-bit numbers which represent difficulty targets, thus +// there really is not a need for a sign bit, but it is implemented here to stay consistent with bitcoind. +func CompactToBig(compact uint32) *big.Int { + // Extract the mantissa, sign bit, and exponent. + mantissa := compact & 0x007fffff + isNegative := compact&0x00800000 != 0 + exponent := uint(compact >> 24) + // Since the base for the exponent is 256, the exponent can be treated as the number of bytes to represent the full + // 256-bit number. So, treat the exponent as the number of bytes and shift the mantissa right or left accordingly. + // This is equivalent to N = mantissa * 256^(exponent-3) + var bn *big.Int + if exponent <= 3 { + mantissa >>= 8 * (3 - exponent) + bn = big.NewInt(int64(mantissa)) + } else { + bn = big.NewInt(int64(mantissa)) + bn.Lsh(bn, 8*(exponent-3)) + } + // Make it negative if the sign bit is set. + if isNegative { + bn = bn.Neg(bn) + } + return bn +} + +// BigToCompact converts a whole number N to a compact representation using an unsigned 32-bit number. The compact +// representation only provides 23 bits of precision, so values larger than (2^23 - 1) only encode the most significant +// digits of the number. See CompactToBig for details. +func BigToCompact(n *big.Int) uint32 { + // No need to do any work if it's zero. + if n.Sign() == 0 { + return 0 + } + // Since the base for the exponent is 256, the exponent can be treated as the number of bytes. So, shift the number + // right or left accordingly. This is equivalent to: mantissa = mantissa / 256^(exponent-3) + var mantissa uint32 + exponent := uint(len(n.Bytes())) + if exponent <= 3 { + mantissa = uint32(n.Bits()[0]) + mantissa <<= 8 * (3 - exponent) + } else { + // Use a copy to avoid modifying the caller's original number. + tn := new(big.Int).Set(n) + mantissa = uint32(tn.Rsh(tn, 8*(exponent-3)).Bits()[0]) + } + // When the mantissa already has the sign bit set, the number is too large to fit into the available 23-bits, so + // divide the number by 256 and increment the exponent accordingly. + if mantissa&0x00800000 != 0 { + mantissa >>= 8 + exponent++ + } + // Pack the exponent, sign bit, and mantissa into an unsigned 32-bit int and return it. + compact := uint32(exponent<<24) | mantissa + if n.Sign() < 0 { + compact |= 0x00800000 + } + return compact +} diff --git a/pkg/block/block.go b/pkg/block/block.go new file mode 100644 index 0000000..a4634ab --- /dev/null +++ b/pkg/block/block.go @@ -0,0 +1,239 @@ +package block + +import ( + "bytes" + "fmt" + "github.com/p9c/p9/pkg/util" + "io" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// OutOfRangeError describes an error due to accessing an element that is out of range. +type OutOfRangeError string + +// BlockHeightUnknown is the value returned for a block height that is unknown. This is typically because the block has +// not been inserted into the main chain yet. +const BlockHeightUnknown = int32(-1) + +// Error satisfies the error interface and prints human-readable errors. +func (e OutOfRangeError) Error() string { + return string(e) +} + +// SetBlockBytes sets the internal serialized block byte buffer to the passed buffer. It is used to inject errors and is +// only available to the test package. +func (b *Block) SetBlockBytes(buf []byte) { + b.serializedBlock = buf +} + +// Block defines a bitcoin block that provides easier and more efficient manipulation of raw blocks. It also memorizes +// hashes for the block and its transactions on their first access so subsequent accesses don't have to repeat the +// relatively expensive hashing operations. +type Block struct { + msgBlock *wire.Block // Underlying WireBlock + serializedBlock []byte // Serialized bytes for the block + serializedBlockNoWitness []byte // Serialized bytes for block w/o witness data + blockHash *chainhash.Hash // Cached block hash + blockHeight int32 // Height in the main block chain + transactions []*util.Tx // Transactions + txnsGenerated bool // ALL wrapped transactions generated +} + +// WireBlock returns the underlying wire.Block for the Block. +func (b *Block) WireBlock() *wire.Block { + // Return the cached block. + return b.msgBlock +} + +// Bytes returns the serialized bytes for the Block. +// This is equivalent to calling Serialize on the underlying wire.Block, +// however it caches the result so subsequent calls are more efficient. +func (b *Block) Bytes() ([]byte, error) { + // Return the cached serialized bytes if it has already been generated. + if len(b.serializedBlock) != 0 { + return b.serializedBlock, nil + } + // Serialize the Block. + w := bytes.NewBuffer(make([]byte, 0, b.msgBlock.SerializeSize())) + e := b.msgBlock.Serialize(w) + if e != nil { + return nil, e + } + serializedBlock := w.Bytes() + // Cache the serialized bytes and return them. + b.serializedBlock = serializedBlock + return serializedBlock, nil +} + +// BytesNoWitness returns the serialized bytes for the block with transactions +// encoded without any witness data. +func (b *Block) BytesNoWitness() ([]byte, error) { + // Return the cached serialized bytes if it has already been generated. + if len(b.serializedBlockNoWitness) != 0 { + return b.serializedBlockNoWitness, nil + } + // Serialize the Block. + var w bytes.Buffer + e := b.msgBlock.SerializeNoWitness(&w) + if e != nil { + return nil, e + } + serializedBlock := w.Bytes() + // Cache the serialized bytes and return them. + b.serializedBlockNoWitness = serializedBlock + return serializedBlock, nil +} + +// Hash returns the block identifier hash for the Block. This is equivalent to calling BlockHash on the underlying +// wire.Block, however it caches the result so subsequent calls are more efficient. +func (b *Block) Hash() *chainhash.Hash { + // Return the cached block hash if it has already been generated. + if b.blockHash != nil { + return b.blockHash + } + // Cache the block hash and return it. + hash := b.msgBlock.BlockHash() + b.blockHash = &hash + return &hash +} + +// Tx returns a wrapped transaction (util.Tx) for the transaction at the specified index in the Block. The supplied +// index is 0 based. That is to say, the first transaction in the block is txNum 0. This is nearly equivalent to +// accessing the raw transaction (wire.MsgTx) from the underlying wire.Block, however the wrapped transaction has +// some helpful properties such as caching the hash so subsequent calls are more efficient. +func (b *Block) Tx(txNum int) (*util.Tx, error) { + // Ensure the requested transaction is in range. + numTx := uint64(len(b.msgBlock.Transactions)) + if txNum < 0 || uint64(txNum) > numTx { + str := fmt.Sprintf( + "transaction index %d is out of range - max %d", + txNum, numTx-1, + ) + return nil, OutOfRangeError(str) + } + // Generate slice to hold all of the wrapped transactions if needed. + if len(b.transactions) == 0 { + b.transactions = make([]*util.Tx, numTx) + } + // Return the wrapped transaction if it has already been generated. + if b.transactions[txNum] != nil { + return b.transactions[txNum], nil + } + // Generate and cache the wrapped transaction and return it. + newTx := util.NewTx(b.msgBlock.Transactions[txNum]) + newTx.SetIndex(txNum) + b.transactions[txNum] = newTx + return newTx, nil +} + +// Transactions returns a slice of wrapped transactions (util.Tx) for all transactions in the Block. This is nearly +// equivalent to accessing the raw transactions (wire.MsgTx) in the underlying wire.Block, however it instead +// provides easy access to wrapped versions (util.Tx) of them. +func (b *Block) Transactions() []*util.Tx { + // Return transactions if they have ALL already been generated. This flag is necessary because the wrapped + // transactions are lazily generated in a sparse fashion. + if b.txnsGenerated { + return b.transactions + } + // Generate slice to hold all of the wrapped transactions if needed. + if len(b.transactions) == 0 { + b.transactions = make([]*util.Tx, len(b.msgBlock.Transactions)) + } + // Generate and cache the wrapped transactions for all that haven't already been done. + for i, tx := range b.transactions { + if tx == nil { + newTx := util.NewTx(b.msgBlock.Transactions[i]) + newTx.SetIndex(i) + b.transactions[i] = newTx + } + } + b.txnsGenerated = true + return b.transactions +} + +// TxHash returns the hash for the requested transaction number in the Block. The supplied index is 0 based. That is to +// say, the first transaction in the block is txNum 0. This is equivalent to calling TxHash on the underlying +// wire.MsgTx, however it caches the result so subsequent calls are more efficient. +func (b *Block) TxHash(txNum int) (*chainhash.Hash, error) { + // Attempt to get a wrapped transaction for the specified index. It will be created lazily if needed or simply + // return the cached version if it has already been generated. + tx, e := b.Tx(txNum) + if e != nil { + return nil, e + } + // Defer to the wrapped transaction which will return the cached hash if it has already been generated. + return tx.Hash(), nil +} + +// TxLoc returns the offsets and lengths of each transaction in a raw block. It is used to allow fast indexing into +// transactions within the raw byte stream. +func (b *Block) TxLoc() ([]wire.TxLoc, error) { + rawMsg, e := b.Bytes() + if e != nil { + return nil, e + } + rbuf := bytes.NewBuffer(rawMsg) + var mblock wire.Block + txLocs, e := mblock.DeserializeTxLoc(rbuf) + if e != nil { + return nil, e + } + return txLocs, e +} + +// Height returns the saved height of the block in the block chain. This value will be BlockHeightUnknown if it hasn't +// already explicitly been set. +func (b *Block) Height() int32 { + return b.blockHeight +} + +// SetHeight sets the height of the block in the block chain. +func (b *Block) SetHeight(height int32) { + b.blockHeight = height +} + +// NewBlock returns a new instance of a bitcoin block given an underlying wire.Block. See Block. +func NewBlock(msgBlock *wire.Block) *Block { + return &Block{ + msgBlock: msgBlock, + blockHeight: BlockHeightUnknown, + } +} + +// NewFromBytes returns a new instance of a bitcoin block given the serialized bytes. See Block. +func NewFromBytes(serializedBlock []byte) (*Block, error) { + br := bytes.NewReader(serializedBlock) + b, e := NewFromReader(br) + if e != nil { + return nil, e + } + b.serializedBlock = serializedBlock + return b, nil +} + +// NewFromReader returns a new instance of a bitcoin block given a Reader to deserialize the block. See Block. +func NewFromReader(r io.Reader) (*Block, error) { + // Deserialize the bytes into a Block. + var msgBlock wire.Block + e := msgBlock.Deserialize(r) + if e != nil { + return nil, e + } + b := Block{ + msgBlock: &msgBlock, + blockHeight: BlockHeightUnknown, + } + return &b, nil +} + +// NewFromBlockAndBytes returns a new instance of a bitcoin block given an underlying wire.Block and the +// serialized bytes for it. See Block. +func NewFromBlockAndBytes(msgBlock *wire.Block, serializedBlock []byte) *Block { + return &Block{ + msgBlock: msgBlock, + serializedBlock: serializedBlock, + blockHeight: BlockHeightUnknown, + } +} diff --git a/pkg/block/block_test.go b/pkg/block/block_test.go new file mode 100644 index 0000000..848621e --- /dev/null +++ b/pkg/block/block_test.go @@ -0,0 +1,547 @@ +package block_test + +import ( + "bytes" + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/util" + "io" + "reflect" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// TestBlock tests the API for Block. +func TestBlock(t *testing.T) { + b := block.NewBlock(&Block100000) + // Ensure we get the same data back out. + if msgBlock := b.WireBlock(); !reflect.DeepEqual(msgBlock, &Block100000) { + t.Errorf("Block: mismatched Block - got %v, want %v", + spew.Sdump(msgBlock), spew.Sdump(&Block100000), + ) + } + // Ensure block height set and get work properly. + wantHeight := int32(100000) + b.SetHeight(wantHeight) + if gotHeight := b.Height(); gotHeight != wantHeight { + t.Errorf("Height: mismatched height - got %v, want %v", + gotHeight, wantHeight, + ) + } + // Hash for block 100,000. + wantHashStr := "3ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506" + wantHash, e := chainhash.NewHashFromStr(wantHashStr) + if e != nil { + t.Errorf("NewHashFromStr: %v", e) + } + // Request the hash multiple times to test generation and caching. + for i := 0; i < 2; i++ { + hash := b.Hash() + if !hash.IsEqual(wantHash) { + t.Errorf("Hash #%d mismatched hash - got %v, want %v", + i, hash, wantHash, + ) + } + } + // Merkles for the transactions in Block100000. + wantTxHashes := []string{ + "8c14f0db3df150123e6f3dbbf30f8b955a8249b62ac1d1ff16284aefa3d06d87", + "fff2525b8931402dd09222c50775608f75787bd2b87e56995a7bdd30f79702c4", + "6359f0868171b1d194cbee1af2f16ea598ae8fad666d9b012c8ed2b79a236ec4", + "e9a66845e05d5abc0ad04ec80f774a7e585c6e8db975962d069a522137b80c1d", + } + // Create a new block to nuke all cached data. + b = block.NewBlock(&Block100000) + // Request hash for all transactions one at a time via Tx. + for i, txHash := range wantTxHashes { + wantHash, e = chainhash.NewHashFromStr(txHash) + if e != nil { + t.Errorf("NewHashFromStr: %v", e) + } + // Request the hash multiple times to test generation and + // caching. + for j := 0; j < 2; j++ { + var tx *util.Tx + tx, e = b.Tx(i) + if e != nil { + t.Errorf("Tx #%d: %v", i, e) + continue + } + hash := tx.Hash() + if !hash.IsEqual(wantHash) { + t.Errorf("Hash #%d mismatched hash - got %v, "+ + "want %v", j, hash, wantHash, + ) + continue + } + } + } + // Create a new block to nuke all cached data. + b = block.NewBlock(&Block100000) + // Request slice of all transactions multiple times to test generation and caching. + for i := 0; i < 2; i++ { + transactions := b.Transactions() + // Ensure we get the expected number of transactions. + if len(transactions) != len(wantTxHashes) { + t.Errorf("Transactions #%d mismatched number of "+ + "transactions - got %d, want %d", i, + len(transactions), len(wantTxHashes), + ) + continue + } + // Ensure all of the hashes match. + for j, tx := range transactions { + wantHash, e = chainhash.NewHashFromStr(wantTxHashes[j]) + if e != nil { + t.Errorf("NewHashFromStr: %v", e) + } + hash := tx.Hash() + if !hash.IsEqual(wantHash) { + t.Errorf("Transactions #%d mismatched hashes "+ + "- got %v, want %v", j, hash, wantHash, + ) + continue + } + } + } + // Serialize the test block. + var block100000Buf bytes.Buffer + e = Block100000.Serialize(&block100000Buf) + if e != nil { + t.Errorf("Serialize: %v", e) + } + block100000Bytes := block100000Buf.Bytes() + // Request serialized bytes multiple times to test generation and caching. + for i := 0; i < 2; i++ { + var serializedBytes []byte + serializedBytes, e = b.Bytes() + if e != nil { + t.Errorf("Hash: %v", e) + continue + } + if !bytes.Equal(serializedBytes, block100000Bytes) { + t.Errorf("Hash #%d wrong bytes - got %v, want %v", i, + spew.Sdump(serializedBytes), + spew.Sdump(block100000Bytes), + ) + continue + } + } + // Transaction offsets and length for the transaction in Block100000. + wantTxLocs := []wire.TxLoc{ + {TxStart: 81, TxLen: 135}, + {TxStart: 216, TxLen: 259}, + {TxStart: 475, TxLen: 257}, + {TxStart: 732, TxLen: 225}, + } + // Ensure the transaction location information is accurate. + txLocs, e := b.TxLoc() + if e != nil { + t.Errorf("TxLoc: %v", e) + return + } + if !reflect.DeepEqual(txLocs, wantTxLocs) { + t.Errorf("TxLoc: mismatched transaction location information "+ + "- got %v, want %v", spew.Sdump(txLocs), + spew.Sdump(wantTxLocs), + ) + } +} + +// TestNewBlockFromBytes tests creation of a Block from serialized bytes. +func TestNewBlockFromBytes(t *testing.T) { + // Serialize the test block. + var block100000Buf bytes.Buffer + e := Block100000.Serialize(&block100000Buf) + if e != nil { + t.Errorf("Serialize: %v", e) + } + block100000Bytes := block100000Buf.Bytes() + // Create a new block from the serialized bytes. + b, e := block.NewFromBytes(block100000Bytes) + if e != nil { + t.Errorf("NewFromBytes: %v", e) + return + } + // Ensure we get the same data back out. + serializedBytes, e := b.Bytes() + if e != nil { + t.Errorf("Hash: %v", e) + return + } + if !bytes.Equal(serializedBytes, block100000Bytes) { + t.Errorf("Hash: wrong bytes - got %v, want %v", + spew.Sdump(serializedBytes), + spew.Sdump(block100000Bytes), + ) + } + // Ensure the generated Block is correct. + if msgBlock := b.WireBlock(); !reflect.DeepEqual(msgBlock, &Block100000) { + t.Errorf("Block: mismatched Block - got %v, want %v", + spew.Sdump(msgBlock), spew.Sdump(&Block100000), + ) + } +} + +// TestNewBlockFromBlockAndBytes tests creation of a Block from a Block and raw bytes. +func TestNewBlockFromBlockAndBytes(t *testing.T) { + // Serialize the test block. + var block100000Buf bytes.Buffer + e := Block100000.Serialize(&block100000Buf) + if e != nil { + t.Errorf("Serialize: %v", e) + } + block100000Bytes := block100000Buf.Bytes() + // Create a new block from the serialized bytes. + b := block.NewFromBlockAndBytes(&Block100000, block100000Bytes) + // Ensure we get the same data back out. + serializedBytes, e := b.Bytes() + if e != nil { + t.Errorf("Hash: %v", e) + return + } + if !bytes.Equal(serializedBytes, block100000Bytes) { + t.Errorf("Hash: wrong bytes - got %v, want %v", + spew.Sdump(serializedBytes), + spew.Sdump(block100000Bytes), + ) + } + if msgBlock := b.WireBlock(); !reflect.DeepEqual(msgBlock, &Block100000) { + t.Errorf("Block: mismatched Block - got %v, want %v", + spew.Sdump(msgBlock), spew.Sdump(&Block100000), + ) + } +} + +// TestBlockErrors tests the error paths for the Block API. +func TestBlockErrors(t *testing.T) { + // Ensure out of range errors are as expected. + wantErr := "transaction index -1 is out of range - max 3" + testErr := block.OutOfRangeError(wantErr) + if testErr.Error() != wantErr { + t.Errorf("OutOfRangeError: wrong error - got %v, want %v", + testErr.Error(), wantErr, + ) + } + // Serialize the test block. + var block100000Buf bytes.Buffer + e := Block100000.Serialize(&block100000Buf) + if e != nil { + t.Errorf("Serialize: %v", e) + } + block100000Bytes := block100000Buf.Bytes() + // Create a new block from the serialized bytes. + b, e := block.NewFromBytes(block100000Bytes) + if e != nil { + t.Errorf("NewFromBytes: %v", e) + return + } + // Truncate the block byte buffer to force errors. + shortBytes := block100000Bytes[:80] + _, e = block.NewFromBytes(shortBytes) + if e != io.EOF { + t.Errorf("NewFromBytes: did not get expected error - "+ + "got %v, want %v", e, io.EOF, + ) + } + // Ensure TxHash returns expected error on invalid indices. + _, e = b.TxHash(-1) + if _, ok := e.(block.OutOfRangeError); !ok { + t.Errorf("TxHash: wrong error - got: %v <%T>, "+ + "want: <%T>", e, e, block.OutOfRangeError(""), + ) + } + _, e = b.TxHash(len(Block100000.Transactions) + 1) + if _, ok := e.(block.OutOfRangeError); !ok { + t.Errorf("TxHash: wrong error - got: %v <%T>, "+ + "want: <%T>", e, e, block.OutOfRangeError(""), + ) + } + // Ensure Tx returns expected error on invalid indices. + _, e = b.Tx(-1) + if _, ok := e.(block.OutOfRangeError); !ok { + t.Errorf("Tx: wrong error - got: %v <%T>, "+ + "want: <%T>", e, e, block.OutOfRangeError(""), + ) + } + _, e = b.Tx(len(Block100000.Transactions) + 1) + if _, ok := e.(block.OutOfRangeError); !ok { + t.Errorf("Tx: wrong error - got: %v <%T>, "+ + "want: <%T>", e, e, block.OutOfRangeError(""), + ) + } + // Ensure TxLoc returns expected error with short byte buffer. This makes use of the test package only function, + // SetBlockBytes, to inject a short byte buffer. + b.SetBlockBytes(shortBytes) + _, e = b.TxLoc() + if e != io.EOF { + t.Errorf("TxLoc: did not get expected error - "+ + "got %v, want %v", e, io.EOF, + ) + } +} + +// Block100000 defines block 100,000 of the block chain. It is used to test Block operations. +var Block100000 = wire.Block{ + Header: wire.BlockHeader{ + Version: 1, + PrevBlock: chainhash.Hash([32]byte{ // Make go vet happy. + 0x50, 0x12, 0x01, 0x19, 0x17, 0x2a, 0x61, 0x04, + 0x21, 0xa6, 0xc3, 0x01, 0x1d, 0xd3, 0x30, 0xd9, + 0xdf, 0x07, 0xb6, 0x36, 0x16, 0xc2, 0xcc, 0x1f, + 0x1c, 0xd0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + ), + // 000000000002d01c1fccc21636b607dfd930d31d01c3a62104612a1719011250 + MerkleRoot: chainhash.Hash([32]byte{ // Make go vet happy. + 0x66, 0x57, 0xa9, 0x25, 0x2a, 0xac, 0xd5, 0xc0, + 0xb2, 0x94, 0x09, 0x96, 0xec, 0xff, 0x95, 0x22, + 0x28, 0xc3, 0x06, 0x7c, 0xc3, 0x8d, 0x48, 0x85, + 0xef, 0xb5, 0xa4, 0xac, 0x42, 0x47, 0xe9, 0xf3, + }, + ), + // f3e94742aca4b5ef85488dc37c06c3282295ffec960994b2c0d5ac2a25a95766 + Timestamp: time.Unix(1293623863, 0), // 2010-12-29 11:57:43 +0000 UTC + Bits: 0x1b04864c, // 453281356 + Nonce: 0x10572b0f, // 274148111 + }, + Transactions: []*wire.MsgTx{ + { + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + SignatureScript: []byte{ + 0x04, 0x4c, 0x86, 0x04, 0x1b, 0x02, 0x06, 0x02, + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0x12a05f200, // 5000000000 + PkScript: []byte{ + 0x41, // OP_DATA_65 + 0x04, 0x1b, 0x0e, 0x8c, 0x25, 0x67, 0xc1, 0x25, + 0x36, 0xaa, 0x13, 0x35, 0x7b, 0x79, 0xa0, 0x73, + 0xdc, 0x44, 0x44, 0xac, 0xb8, 0x3c, 0x4e, 0xc7, + 0xa0, 0xe2, 0xf9, 0x9d, 0xd7, 0x45, 0x75, 0x16, + 0xc5, 0x81, 0x72, 0x42, 0xda, 0x79, 0x69, 0x24, + 0xca, 0x4e, 0x99, 0x94, 0x7d, 0x08, 0x7f, 0xed, + 0xf9, 0xce, 0x46, 0x7c, 0xb9, 0xf7, 0xc6, 0x28, + 0x70, 0x78, 0xf8, 0x01, 0xdf, 0x27, 0x6f, 0xdf, + 0x84, // 65-byte signature + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 0, + }, + { + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash([32]byte{ // Make go vet happy. + 0x03, 0x2e, 0x38, 0xe9, 0xc0, 0xa8, 0x4c, 0x60, + 0x46, 0xd6, 0x87, 0xd1, 0x05, 0x56, 0xdc, 0xac, + 0xc4, 0x1d, 0x27, 0x5e, 0xc5, 0x5f, 0xc0, 0x07, + 0x79, 0xac, 0x88, 0xfd, 0xf3, 0x57, 0xa1, 0x87, + }, + ), + // 87a157f3fd88ac7907c05fc55e271dc4acdc5605d187d646604ca8c0e9382e03 + Index: 0, + }, + SignatureScript: []byte{ + 0x49, // OP_DATA_73 + 0x30, 0x46, 0x02, 0x21, 0x00, 0xc3, 0x52, 0xd3, + 0xdd, 0x99, 0x3a, 0x98, 0x1b, 0xeb, 0xa4, 0xa6, + 0x3a, 0xd1, 0x5c, 0x20, 0x92, 0x75, 0xca, 0x94, + 0x70, 0xab, 0xfc, 0xd5, 0x7d, 0xa9, 0x3b, 0x58, + 0xe4, 0xeb, 0x5d, 0xce, 0x82, 0x02, 0x21, 0x00, + 0x84, 0x07, 0x92, 0xbc, 0x1f, 0x45, 0x60, 0x62, + 0x81, 0x9f, 0x15, 0xd3, 0x3e, 0xe7, 0x05, 0x5c, + 0xf7, 0xb5, 0xee, 0x1a, 0xf1, 0xeb, 0xcc, 0x60, + 0x28, 0xd9, 0xcd, 0xb1, 0xc3, 0xaf, 0x77, 0x48, + 0x01, // 73-byte signature + 0x41, // OP_DATA_65 + 0x04, 0xf4, 0x6d, 0xb5, 0xe9, 0xd6, 0x1a, 0x9d, + 0xc2, 0x7b, 0x8d, 0x64, 0xad, 0x23, 0xe7, 0x38, + 0x3a, 0x4e, 0x6c, 0xa1, 0x64, 0x59, 0x3c, 0x25, + 0x27, 0xc0, 0x38, 0xc0, 0x85, 0x7e, 0xb6, 0x7e, + 0xe8, 0xe8, 0x25, 0xdc, 0xa6, 0x50, 0x46, 0xb8, + 0x2c, 0x93, 0x31, 0x58, 0x6c, 0x82, 0xe0, 0xfd, + 0x1f, 0x63, 0x3f, 0x25, 0xf8, 0x7c, 0x16, 0x1b, + 0xc6, 0xf8, 0xa6, 0x30, 0x12, 0x1d, 0xf2, 0xb3, + 0xd3, // 65-byte pubkey + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0x2123e300, // 556000000 + PkScript: []byte{ + 0x76, // OP_DUP + 0xa9, // OP_HASH160 + 0x14, // OP_DATA_20 + 0xc3, 0x98, 0xef, 0xa9, 0xc3, 0x92, 0xba, 0x60, + 0x13, 0xc5, 0xe0, 0x4e, 0xe7, 0x29, 0x75, 0x5e, + 0xf7, 0xf5, 0x8b, 0x32, + 0x88, // OP_EQUALVERIFY + 0xac, // OP_CHECKSIG + }, + }, + { + Value: 0x108e20f00, // 4444000000 + PkScript: []byte{ + 0x76, // OP_DUP + 0xa9, // OP_HASH160 + 0x14, // OP_DATA_20 + 0x94, 0x8c, 0x76, 0x5a, 0x69, 0x14, 0xd4, 0x3f, + 0x2a, 0x7a, 0xc1, 0x77, 0xda, 0x2c, 0x2f, 0x6b, + 0x52, 0xde, 0x3d, 0x7c, + 0x88, // OP_EQUALVERIFY + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 0, + }, + { + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash([32]byte{ // Make go vet happy. + 0xc3, 0x3e, 0xbf, 0xf2, 0xa7, 0x09, 0xf1, 0x3d, + 0x9f, 0x9a, 0x75, 0x69, 0xab, 0x16, 0xa3, 0x27, + 0x86, 0xaf, 0x7d, 0x7e, 0x2d, 0xe0, 0x92, 0x65, + 0xe4, 0x1c, 0x61, 0xd0, 0x78, 0x29, 0x4e, 0xcf, + }, + ), + // cf4e2978d0611ce46592e02d7e7daf8627a316ab69759a9f3df109a7f2bf3ec3 + Index: 1, + }, + SignatureScript: []byte{ + 0x47, // OP_DATA_71 + 0x30, 0x44, 0x02, 0x20, 0x03, 0x2d, 0x30, 0xdf, + 0x5e, 0xe6, 0xf5, 0x7f, 0xa4, 0x6c, 0xdd, 0xb5, + 0xeb, 0x8d, 0x0d, 0x9f, 0xe8, 0xde, 0x6b, 0x34, + 0x2d, 0x27, 0x94, 0x2a, 0xe9, 0x0a, 0x32, 0x31, + 0xe0, 0xba, 0x33, 0x3e, 0x02, 0x20, 0x3d, 0xee, + 0xe8, 0x06, 0x0f, 0xdc, 0x70, 0x23, 0x0a, 0x7f, + 0x5b, 0x4a, 0xd7, 0xd7, 0xbc, 0x3e, 0x62, 0x8c, + 0xbe, 0x21, 0x9a, 0x88, 0x6b, 0x84, 0x26, 0x9e, + 0xae, 0xb8, 0x1e, 0x26, 0xb4, 0xfe, 0x01, + 0x41, // OP_DATA_65 + 0x04, 0xae, 0x31, 0xc3, 0x1b, 0xf9, 0x12, 0x78, + 0xd9, 0x9b, 0x83, 0x77, 0xa3, 0x5b, 0xbc, 0xe5, + 0xb2, 0x7d, 0x9f, 0xff, 0x15, 0x45, 0x68, 0x39, + 0xe9, 0x19, 0x45, 0x3f, 0xc7, 0xb3, 0xf7, 0x21, + 0xf0, 0xba, 0x40, 0x3f, 0xf9, 0x6c, 0x9d, 0xee, + 0xb6, 0x80, 0xe5, 0xfd, 0x34, 0x1c, 0x0f, 0xc3, + 0xa7, 0xb9, 0x0d, 0xa4, 0x63, 0x1e, 0xe3, 0x95, + 0x60, 0x63, 0x9d, 0xb4, 0x62, 0xe9, 0xcb, 0x85, + 0x0f, // 65-byte pubkey + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0xf4240, // 1000000 + PkScript: []byte{ + 0x76, // OP_DUP + 0xa9, // OP_HASH160 + 0x14, // OP_DATA_20 + 0xb0, 0xdc, 0xbf, 0x97, 0xea, 0xbf, 0x44, 0x04, + 0xe3, 0x1d, 0x95, 0x24, 0x77, 0xce, 0x82, 0x2d, + 0xad, 0xbe, 0x7e, 0x10, + 0x88, // OP_EQUALVERIFY + 0xac, // OP_CHECKSIG + }, + }, + { + Value: 0x11d260c0, // 299000000 + PkScript: []byte{ + 0x76, // OP_DUP + 0xa9, // OP_HASH160 + 0x14, // OP_DATA_20 + 0x6b, 0x12, 0x81, 0xee, 0xc2, 0x5a, 0xb4, 0xe1, + 0xe0, 0x79, 0x3f, 0xf4, 0xe0, 0x8a, 0xb1, 0xab, + 0xb3, 0x40, 0x9c, 0xd9, + 0x88, // OP_EQUALVERIFY + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 0, + }, + { + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash([32]byte{ // Make go vet happy. + 0x0b, 0x60, 0x72, 0xb3, 0x86, 0xd4, 0xa7, 0x73, + 0x23, 0x52, 0x37, 0xf6, 0x4c, 0x11, 0x26, 0xac, + 0x3b, 0x24, 0x0c, 0x84, 0xb9, 0x17, 0xa3, 0x90, + 0x9b, 0xa1, 0xc4, 0x3d, 0xed, 0x5f, 0x51, 0xf4, + }, + ), + // f4515fed3dc4a19b90a317b9840c243bac26114cf637522373a7d486b372600b + Index: 0, + }, + SignatureScript: []byte{ + 0x49, // OP_DATA_73 + 0x30, 0x46, 0x02, 0x21, 0x00, 0xbb, 0x1a, 0xd2, + 0x6d, 0xf9, 0x30, 0xa5, 0x1c, 0xce, 0x11, 0x0c, + 0xf4, 0x4f, 0x7a, 0x48, 0xc3, 0xc5, 0x61, 0xfd, + 0x97, 0x75, 0x00, 0xb1, 0xae, 0x5d, 0x6b, 0x6f, + 0xd1, 0x3d, 0x0b, 0x3f, 0x4a, 0x02, 0x21, 0x00, + 0xc5, 0xb4, 0x29, 0x51, 0xac, 0xed, 0xff, 0x14, + 0xab, 0xba, 0x27, 0x36, 0xfd, 0x57, 0x4b, 0xdb, + 0x46, 0x5f, 0x3e, 0x6f, 0x8d, 0xa1, 0x2e, 0x2c, + 0x53, 0x03, 0x95, 0x4a, 0xca, 0x7f, 0x78, 0xf3, + 0x01, // 73-byte signature + 0x41, // OP_DATA_65 + 0x04, 0xa7, 0x13, 0x5b, 0xfe, 0x82, 0x4c, 0x97, + 0xec, 0xc0, 0x1e, 0xc7, 0xd7, 0xe3, 0x36, 0x18, + 0x5c, 0x81, 0xe2, 0xaa, 0x2c, 0x41, 0xab, 0x17, + 0x54, 0x07, 0xc0, 0x94, 0x84, 0xce, 0x96, 0x94, + 0xb4, 0x49, 0x53, 0xfc, 0xb7, 0x51, 0x20, 0x65, + 0x64, 0xa9, 0xc2, 0x4d, 0xd0, 0x94, 0xd4, 0x2f, + 0xdb, 0xfd, 0xd5, 0xaa, 0xd3, 0xe0, 0x63, 0xce, + 0x6a, 0xf4, 0xcf, 0xaa, 0xea, 0x4e, 0xa1, 0x4f, + 0xbb, // 65-byte pubkey + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 0xf4240, // 1000000 + PkScript: []byte{ + 0x76, // OP_DUP + 0xa9, // OP_HASH160 + 0x14, // OP_DATA_20 + 0x39, 0xaa, 0x3d, 0x56, 0x9e, 0x06, 0xa1, 0xd7, + 0x92, 0x6d, 0xc4, 0xbe, 0x11, 0x93, 0xc9, 0x9b, + 0xf2, 0xeb, 0x9e, 0xe0, + 0x88, // OP_EQUALVERIFY + 0xac, // OP_CHECKSIG + }, + }, + }, + LockTime: 0, + }, + }, +} diff --git a/pkg/blockchain/README.md b/pkg/blockchain/README.md new file mode 100755 index 0000000..661ed56 --- /dev/null +++ b/pkg/blockchain/README.md @@ -0,0 +1,111 @@ +# blockchain + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/blockchain) + +Package blockchain implements bitcoin block handling and chain selection rules. + +The test coverage is currently only around 60%, but will be increasing over +time. See `test_coverage.txt` for the gocov coverage report. Alternatively, if +you are running a POSIX OS, you can run the `cov_report.sh` script for a +real-time report. Package blockchain is licensed under the liberal ISC license. +There is an associated blog post about the release of this +package [here](https://blog.conformal.com/btcchain-the-bitcoin-chain-package-from-bctd/) +. + +This package has intentionally been designed so it can be used as a standalone +package for any projects needing to handle processing of blocks into the bitcoin +block chain. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/blockchain +``` + +## Bitcoin Chain Processing Overview + +Before a block is allowed into the block chain, it must go through an intensive +series of validation rules. The following list serves as a general outline of +those rules to provide some intuition into what is going on under the hood, but +is by no means exhaustive: + +- Reject duplicate blocks + +- Perform a series of sanity checks on the block and its transactions such as + verifying proof of work, timestamps, number and character of transactions, + transaction amounts, script complexity, and merkle root calculations + +- Compare the block against predetermined checkpoints for expected timestamps + and difficulty based on elapsed time since the checkpoint + +- Save the most recent orphan blocks for a limited time in case their parent + blocks become available + +- Stop processing if the block is an orphan as the rest of the processing + depends on the block's position within the block chain + +- Perform a series of more thorough checks that depend on the block's position + within the block chain such as verifying block difficulties adhere to + difficulty retarget rules, timestamps are after the median of the last several + blocks, all transactions are finalized, checkpoint blocks match, and block + versions are in line with the previous blocks + +- Determine how the block fits into the chain and perform different actions + accordingly in order to ensure any side chains which have higher difficulty + than the main chain become the new main chain + +- When a block is being connected to the main chain (either through + reorganization of a side chain to the main chain or just extending the main + chain), perform further checks on the block's transactions such as verifying + transaction duplicates, script complexity for the combination of connected + scripts, coinbase maturity, double spends, and connected transaction values + +- Run the transaction scripts to verify the spender is allowed to spend the + coins + +- Insert the block into the block database + +## Examples + +- [ProcessBlock Example](http://godoc.org/github.com/p9c/p9/blockchain#example-BlockChain-ProcessBlock) + Demonstrates how to create a new chain instance and use ProcessBlock to + attempt to add a block to the chain. This example intentionally attempts to + insert a duplicate genesis block to illustrate how an invalid block is + handled. + +- [CompactToBig Example](http://godoc.org/github.com/p9c/p9/blockchain#example-CompactToBig) + Demonstrates how to convert the compact "bits" in a block header which + represent the target difficulty to a big integer and display it using the + typical hex notation. + +- [BigToCompact Example](http://godoc.org/github.com/p9c/p9/blockchain#example-BigToCompact) + Demonstrates how to convert a target difficulty into the compact "bits" in a + block header which represent that target difficulty. + +## GPG Verification Key + +All official release tags are signed by Conformal so users can ensure the code +has not been tampered with and is coming from the btcsuite developers. To verify +the signature perform the following: + +- Download the public key from the Conformal website at + https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt + +- Import the public key into your GPG keyring: + + ```bash + gpg --import GIT-GPG-KEY-conformal.txt + ``` + +- Verify the release tag with the following command where `TAG_NAME` is a + placeholder for the specific tag: + + ```bash + git tag -v TAG_NAME + ``` + +## License + +Package blockchain is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/blockchain/accept.go b/pkg/blockchain/accept.go new file mode 100644 index 0000000..1296d87 --- /dev/null +++ b/pkg/blockchain/accept.go @@ -0,0 +1,120 @@ +package blockchain + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/hardfork" +) + +// maybeAcceptBlock potentially accepts a block into the block chain +// and, if accepted, returns whether or not it is on the main chain. +// It performs several validation checks which depend on its position within +// the block chain before adding it. +// The block is expected to have already gone through ProcessBlock before +// calling this function with it. +// The flags are also passed to checkBlockContext and connectBestChain. +// See their documentation for how the flags modify their behavior. +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) maybeAcceptBlock(workerNumber uint32, block *block.Block, flags BehaviorFlags) (bool, error) { + T.Ln("maybeAcceptBlock starting") + // The height of this block is one more than the referenced previous block. + prevHash := &block.WireBlock().Header.PrevBlock + prevNode := b.Index.LookupNode(prevHash) + if prevNode == nil { + str := fmt.Sprintf("previous block %s is unknown", prevHash) + E.Ln(str) + return false, ruleError(ErrPreviousBlockUnknown, str) + } else if b.Index.NodeStatus(prevNode).KnownInvalid() { + str := fmt.Sprintf("previous block %s is known to be invalid", prevHash) + E.Ln(str) + return false, ruleError(ErrInvalidAncestorBlock, str) + } + blockHeight := prevNode.height + 1 + T.Ln("block not found, good, setting height", blockHeight) + block.SetHeight(blockHeight) + // // To deal with multiple mining algorithms, we must check first the block header version. Rather than pass the + // // direct previous by height, we look for the previous of the same algorithm and pass that. + // if blockHeight < b.params.BIP0034Height { + // + // } + T.Ln("sanitizing header versions for legacy") + var DoNotCheckPow bool + var pn *BlockNode + var a int32 = 2 + if block.WireBlock().Header.Version == 514 { + a = 514 + } + var aa int32 = 2 + if prevNode.version == 514 { + aa = 514 + } + if a != aa { + var i int64 + pn = prevNode + for ; i < b.params.AveragingInterval-1; i++ { + pn = pn.GetLastWithAlgo(a) + if pn == nil { + break + } + } + } + T.Ln("check for blacklisted addresses") + txs := block.Transactions() + for i := range txs { + if ContainsBlacklisted(b, txs[i], hardfork.Blacklist) { + return false, ruleError(ErrBlacklisted, "block contains a blacklisted address ") + } + } + T.Ln("found no blacklisted addresses") + var e error + if pn != nil { + // The block must pass all of the validation rules which depend on the position + // of the block within the block chain. + if e = b.checkBlockContext(block, prevNode, flags, DoNotCheckPow); E.Chk(e) { + return false, e + } + } + // Insert the block into the database if it's not already there. Even though it + // is possible the block will ultimately fail to connect, it has already passed + // all proof-of-work and validity tests which means it would be prohibitively + // expensive for an attacker to fill up the disk with a bunch of blocks that + // fail to connect. This is necessary since it allows block download to be + // decoupled from the much more expensive connection logic. It also has some + // other nice properties such as making blocks that never become part of the + // main chain or blocks that fail to connect available for further analysis. + T.Ln("inserting block into database") + if e = b.db.Update(func(dbTx database.Tx) (e error) { + return dbStoreBlock(dbTx, block) + }, + ); E.Chk(e) { + return false, e + } + // Create a new block node for the block and add it to the node index. Even if the block ultimately gets connected + // to the main chain, it starts out on a side chain. + blockHeader := &block.WireBlock().Header + newNode := NewBlockNode(blockHeader, prevNode) + newNode.status = statusDataStored + b.Index.AddNode(newNode) + T.Ln("flushing db") + if e = b.Index.flushToDB(); E.Chk(e) { + return false, e + } + + // Connect the passed block to the chain while respecting proper chain selection + // according to the chain with the most proof of work. This also handles + // validation of the transaction scripts. + T.Ln("connecting to best chain") + var isMainChain bool + if isMainChain, e = b.connectBestChain(newNode, block, flags); E.Chk(e) { + return false, e + } + // Notify the caller that the new block was accepted into the block chain. The caller would typically want to react + // by relaying the inventory to other peers. + T.Ln("sending out block notifications for block accepted") + b.ChainLock.Unlock() + b.sendNotification(NTBlockAccepted, block) + b.ChainLock.Lock() + return isMainChain, nil +} diff --git a/pkg/blockchain/bench_test.go b/pkg/blockchain/bench_test.go new file mode 100644 index 0000000..96d4626 --- /dev/null +++ b/pkg/blockchain/bench_test.go @@ -0,0 +1,24 @@ +package blockchain + +import ( + "github.com/p9c/p9/pkg/block" + "testing" +) + +// BenchmarkIsCoinBase performs a simple benchmark against the IsCoinBase function. +func BenchmarkIsCoinBase(b *testing.B) { + tx, _ := block.NewBlock(&Block100000).Tx(1) + b.ResetTimer() + for i := 0; i < b.N; i++ { + IsCoinBase(tx) + } +} + +// BenchmarkIsCoinBaseTx performs a simple benchmark against the IsCoinBaseTx function. +func BenchmarkIsCoinBaseTx(b *testing.B) { + tx := Block100000.Transactions[1] + b.ResetTimer() + for i := 0; i < b.N; i++ { + IsCoinBaseTx(tx) + } +} diff --git a/pkg/blockchain/blacklist.go b/pkg/blockchain/blacklist.go new file mode 100644 index 0000000..0e24a14 --- /dev/null +++ b/pkg/blockchain/blacklist.go @@ -0,0 +1,59 @@ +package blockchain + +import ( + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/fork" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" +) + +// ContainsBlacklisted returns true if one of the given addresses is found in the transaction +func ContainsBlacklisted(b *BlockChain, tx *util.Tx, blacklist []btcaddr.Address) (hasBlacklisted bool) { + // in tests this function is not relevant + if b == nil { + return false + } + // blacklist only applies from hard fork + if fork.GetCurrent(b.BestSnapshot().Height) < 1 { + return false + } + var addrs []btcaddr.Address + // first decode transaction and collect all addresses in the transaction outputs + txo := tx.MsgTx().TxOut + for i := range txo { + script := txo[i].PkScript + _, a, _, _ := txscript.ExtractPkScriptAddrs(script, b.params) + addrs = append(addrs, a...) + } + // next get the addresses from the input transactions outpoints + txi := tx.MsgTx().TxIn + for i := range txi { + bb, e := b.BlockByHash(&txi[i].PreviousOutPoint.Hash) + if e == nil { + txs := bb.WireBlock().Transactions + for j := range txs { + txitxo := txs[j].TxOut + for k := range txitxo { + script := txitxo[k].PkScript + _, a, _, _ := txscript.ExtractPkScriptAddrs(script, b.params) + addrs = append(addrs, a...) + } + } + } + } + // check if the set of addresses intersects with the blacklist + return Intersects(addrs, blacklist) +} + +// Intersects returns whether one slice of byte slices contains a match in another +func Intersects(a, b []btcaddr.Address) bool { + for x := range a { + for y := range b { + if a[x].EncodeAddress() == b[y].EncodeAddress() { + // If we find one match the two arrays intersect + return true + } + } + } + return false +} diff --git a/pkg/blockchain/blockindex.go b/pkg/blockchain/blockindex.go new file mode 100644 index 0000000..9306492 --- /dev/null +++ b/pkg/blockchain/blockindex.go @@ -0,0 +1,378 @@ +package blockchain + +import ( + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/fork" + "math/big" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/wire" +) + +// blockStatus is a bit field representing the validation state of the block. +type blockStatus byte + +const ( + // statusDataStored indicates that the block's payload is stored on disk. + statusDataStored blockStatus = 1 << iota + // statusValid indicates that the block has been fully validated. + statusValid + // statusValidateFailed indicates that the block has failed validation. + statusValidateFailed + // statusInvalidAncestor indicates that one of the block's ancestors has has failed validation, thus the block is + // also invalid. + statusInvalidAncestor + // statusNone indicates that the block has no validation state flags set. + // + // NOTE: This must be defined last in order to avoid influencing iota. + statusNone blockStatus = 0 +) + +// HaveData returns whether the full block data is stored in the database. This will return false for a block node where +// only the header is downloaded or kept. +func (status blockStatus) HaveData() bool { + return status&statusDataStored != 0 +} + +// KnownValid returns whether the block is known to be valid. This will return false for a valid block that has not been +// fully validated yet. +func (status blockStatus) KnownValid() bool { + return status&statusValid != 0 +} + +// KnownInvalid returns whether the block is known to be invalid. This may be because the block itself failed validation +// or any of its ancestors is invalid. This will return false for invalid blocks that have not been proven invalid yet. +func (status blockStatus) KnownInvalid() bool { + return status&(statusValidateFailed|statusInvalidAncestor) != 0 +} + +// BlockNode represents a block within the block chain and is primarily used to aid in selecting the best chain to be +// the main chain. The main chain is stored into the block database. +type BlockNode struct { + // NOTE: Additions, deletions, or modifications to the order of the definitions in this struct should not be changed + // without considering how it affects alignment on 64-bit platforms. The current order is specifically crafted to + // result in minimal padding. There will be hundreds of thousands of these in memory, so a few extra bytes of + // padding adds up. parent is the parent block for this node. + parent *BlockNode + // hash is the double sha 256 of the block. + hash chainhash.Hash + // workSum is the total amount of work in the chain up to and including this node. + workSum *big.Int + // height is the position in the block chain. + height int32 + // Some fields from block headers to aid in best chain selection and reconstructing headers from memory. These must + // be treated as immutable and are intentionally ordered to avoid padding on 64-bit platforms. + version int32 + bits uint32 + nonce uint32 + timestamp int64 + merkleRoot chainhash.Hash + // status is a bitfield representing the validation state of the block. The status field, unlike the other fields, + // may be written to and so should only be accessed using the concurrent -safe NodeStatus method on blockIndex once + // the node has been added to the global index. + status blockStatus + // Diffs is the computed difficulty targets for a block to be connected to this one + Diffs atomic.Value +} + +// initBlockNode initializes a block node from the given header and parent node, calculating the height and workSum from +// the respective fields on the parent. This function is NOT safe for concurrent access. It must only be called when +// initially creating a node. +func initBlockNode(node *BlockNode, blockHeader *wire.BlockHeader, parent *BlockNode) { + *node = BlockNode{ + hash: blockHeader.BlockHash(), + version: blockHeader.Version, + bits: blockHeader.Bits, + nonce: blockHeader.Nonce, + timestamp: blockHeader.Timestamp.Unix(), + merkleRoot: blockHeader.MerkleRoot, + } + if parent != nil { + node.parent = parent + node.height = parent.height + 1 + node.workSum = CalcWork(blockHeader.Bits, node.height, node.version) + parent.workSum = CalcWork(parent.bits, parent.height, parent.version) + node.workSum = node.workSum.Add(parent.workSum, node.workSum) + } +} + +// NewBlockNode returns a new block node for the given block header and parent node, calculating the height and workSum +// from the respective fields on the parent. This function is NOT safe for concurrent access. +func NewBlockNode(blockHeader *wire.BlockHeader, parent *BlockNode) *BlockNode { + var node BlockNode + initBlockNode(&node, blockHeader, parent) + return &node +} + +// Header constructs a block header from the node and returns it. This function is safe for concurrent access. +func (node *BlockNode) Header() wire.BlockHeader { + // No lock is needed because all accessed fields are immutable. + prevHash := &zeroHash + if node.parent != nil { + prevHash = &node.parent.hash + } + return wire.BlockHeader{ + Version: node.version, + PrevBlock: *prevHash, + MerkleRoot: node.merkleRoot, + Timestamp: time.Unix(node.timestamp, 0), + Bits: node.bits, + Nonce: node.nonce, + } +} + +// Ancestor returns the ancestor block node at the provided height by following the chain backwards from this node. The +// returned block will be nil when a height is requested that is after the height of the passed node or is less than +// zero. This function is safe for concurrent access. +func (node *BlockNode) Ancestor(height int32) *BlockNode { + if height < 0 || height > node.height { + return nil + } + n := node + for ; n != nil && n.height != height; n = n.parent { + // Intentionally left blank + } + return n +} + +// RelativeAncestor returns the ancestor block node a relative 'distance' blocks before this node. This is equivalent to +// calling Ancestor with the node's height minus provided distance. This function is safe for concurrent access. +func (node *BlockNode) RelativeAncestor(distance int32) *BlockNode { + return node.Ancestor(node.height - distance) +} + +// CalcPastMedianTime calculates the median time of the previous few blocks prior to, and including, the block node. +// This function is safe for concurrent access. +func (node *BlockNode) CalcPastMedianTime() time.Time { + // Create a slice of the previous few block timestamps used to calculate the median per the number defined by the + // constant medianTimeBlocks. + timestamps := make([]int64, medianTimeBlocks) + numNodes := 0 + iterNode := node + for i := 0; i < medianTimeBlocks && iterNode != nil; i++ { + timestamps[i] = iterNode.timestamp + numNodes++ + iterNode = iterNode.parent + } + // Prune the slice to the actual number of available timestamps which will be fewer than desired near the beginning + // of the block chain and txsort them. + timestamps = timestamps[:numNodes] + sort.Sort(timeSorter(timestamps)) + // NOTE: The consensus rules incorrectly calculate the median for even numbers of blocks. A true median averages the + // middle two elements for a set with an even number of elements in it. Since the constant for the previous number + // of blocks to be used is odd, this is only an issue for a few blocks near the beginning of the chain. I suspect + // this is an optimization even though the result is slightly wrong for a few of the first blocks since after the + // first few blocks, there will always be an odd number of blocks in the set per the constant. This code follows + // suit to ensure the same rules are used, however, be aware that should the medianTimeBlocks constant ever be + // changed to an even number, this code will be wrong. + medianTimestamp := timestamps[numNodes/2] + return time.Unix(medianTimestamp, 0) +} + +// blockIndex provides facilities for keeping track of an in-memory index of the block chain. Although the name block +// chain suggests a single chain of blocks, it is actually a tree-shaped structure where any node can have multiple +// children. However, there can only be one active branch which does indeed form a chain from the tip all the way back +// to the genesis block. +type blockIndex struct { + // The following fields are set when the instance is created and can't be changed afterwards, so there is no need to + // protect them with a separate mutex. + db database.DB + chainParams *chaincfg.Params + sync.RWMutex + index map[chainhash.Hash]*BlockNode + dirty map[*BlockNode]struct{} +} + +// newBlockIndex returns a new empty instance of a block index. The index will be dynamically populated as block nodes +// are loaded from the database and manually added. +func newBlockIndex(db database.DB, chainParams *chaincfg.Params) *blockIndex { + return &blockIndex{ + db: db, + chainParams: chainParams, + index: make(map[chainhash.Hash]*BlockNode), + dirty: make(map[*BlockNode]struct{}), + } +} + +// HaveBlock returns whether or not the block index contains the provided hash. This function is safe for concurrent +// access. +func (bi *blockIndex) HaveBlock(hash *chainhash.Hash) bool { + bi.RLock() + _, hasBlock := bi.index[*hash] + bi.RUnlock() + return hasBlock +} + +// LookupNode returns the block node identified by the provided hash. It will return nil if there is no entry for the +// hash. This function is safe for concurrent access. +func (bi *blockIndex) LookupNode(hash *chainhash.Hash) *BlockNode { + bi.RLock() + node := bi.index[*hash] + bi.RUnlock() + return node +} + +// AddNode adds the provided node to the block index and marks it as dirty. Duplicate entries are not checked so it is +// up to caller to avoid adding them. This function is safe for concurrent access. +func (bi *blockIndex) AddNode(node *BlockNode) { + bi.Lock() + bi.addNode(node) + bi.dirty[node] = struct{}{} + bi.Unlock() +} + +// addNode adds the provided node to the block index, but does not mark it as dirty. This can be used while initializing +// the block index. This function is NOT safe for concurrent access. +func (bi *blockIndex) addNode(node *BlockNode) { + bi.index[node.hash] = node +} + +// NodeStatus provides concurrent-safe access to the status field of a node. This function is safe for concurrent +// access. +func (bi *blockIndex) NodeStatus(node *BlockNode) blockStatus { + bi.RLock() + status := node.status + bi.RUnlock() + return status +} + +// SetStatusFlags flips the provided status flags on the block node to on, regardless of whether they were on or off +// previously. This does not unset any flags currently on. This function is safe for concurrent access. +func (bi *blockIndex) SetStatusFlags(node *BlockNode, flags blockStatus) { + bi.Lock() + node.status |= flags + bi.dirty[node] = struct{}{} + bi.Unlock() +} + +// UnsetStatusFlags flips the provided status flags on the block node to off, regardless of whether they were on or off +// previously. This function is safe for concurrent access. +func (bi *blockIndex) UnsetStatusFlags(node *BlockNode, flags blockStatus) { + bi.Lock() + node.status &^= flags + bi.dirty[node] = struct{}{} + bi.Unlock() +} + +// flushToDB writes all dirty block nodes to the database. If all writes succeed, this clears the dirty set. +func (bi *blockIndex) flushToDB() (e error) { + bi.Lock() + if len(bi.dirty) == 0 { + bi.Unlock() + return nil + } + e = bi.db.Update( + func(dbTx database.Tx) (e error) { + for node := range bi.dirty { + e := dbStoreBlockNode(dbTx, node) + if e != nil { + E.Ln(e) + return e + } + } + return nil + }, + ) + // If write was successful, clear the dirty set. + if e == nil { + bi.dirty = make(map[*BlockNode]struct{}) + } + bi.Unlock() + return e +} + +// GetAlgo returns the algorithm of a block node +func (node *BlockNode) GetAlgo() int32 { + return node.version +} + +// GetLastWithAlgo returns the newest block from node with specified algo +func (node *BlockNode) GetLastWithAlgo(algo int32) (prev *BlockNode) { + if node == nil { + return + } + if fork.GetCurrent(node.height+1) == 0 { + // F.Ln("checking pre-hardfork algo versions") + if algo != 514 && + algo != 2 { + D.Ln("irregular version", algo, "block, assuming 2 (sha256d)") + algo = 2 + } + } + prev = node + for { + if prev == nil { + return nil + } + // Tracef("node %d %d %8x", prev.height, prev.version, prev.bits) + prevversion := prev.version + if fork.GetCurrent(prev.height) == 0 { + // F.Ln("checking pre-hardfork algo versions") + if prev.version != 514 && + prev.version != 2 { + D.Ln("irregular version block", prev.version, ", assuming 2 (sha256d)") + prevversion = 2 + } + } + if prevversion == algo { + // Tracef( + // "found height %d version %d prev version %d prev bits %8x", + // prev.height, prev.version, prevversion, prev.bits) + return + } + prev = prev.RelativeAncestor(1) + } +} + +// if node == nil { +// F.Ln("this node is nil") +// return nil +// } +// prev = node.RelativeAncestor(1) +// if prev == nil { +// F.Ln("the previous node was nil") +// return nil +// } +// prevFork := fork.GetCurrent(prev.height) +// if prevFork == 0 { +// if algo != 514 && +// algo != 2 { +// F.Ln("bogus version halcyon", algo) +// algo = 2 +// } +// } +// if prev.version == algo { +// Tracef("found previous %d %d %08x", prev.height, prev.version, +// prev.bits) +// return prev +// } +// prev = prev.RelativeAncestor(1) +// for { +// if prev == nil { +// F.Ln("passed through genesis") +// return nil +// } +// F.Ln(prev.height) +// prevVersion := prev.version +// if fork.GetCurrent(prev.height) == 0 { +// if prevVersion != 514 && +// prevVersion != 2 { +// F.Ln("bogus version", prevVersion) +// prevVersion = 2 +// } +// } +// if prevVersion == algo { +// Tracef("found previous %d %d %08x", prev.height, prev.version, +// prev.bits) +// return prev +// } else { +// F.Ln(prev.height) +// prev = prev.RelativeAncestor(1) +// } +// } +// } diff --git a/pkg/blockchain/chain.go b/pkg/blockchain/chain.go new file mode 100644 index 0000000..411199d --- /dev/null +++ b/pkg/blockchain/chain.go @@ -0,0 +1,1579 @@ +package blockchain + +import ( + "container/list" + "fmt" + block2 "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/fork" + "sync" + "time" + + "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/wire" +) + +const // maxOrphanBlocks is the maximum number of orphan blocks that can be + // queued. + maxOrphanBlocks = 100 + +// BlockLocator is used to help locate a specific block. The algorithm for building the block locator is to add the +// hashes in reverse order until the genesis block is reached. In order to keep the list of locator hashes to a +// reasonable number of entries, first the most recent previous 12 block hashes are added, then the step is doubled each +// loop iteration to exponentially decrease the number of hashes as a function of the distance from the block being +// located. For example: assume a block chain with a side chain as depicted below: +// +// genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 +// \-> 16a -> 17a +// The block locator for block 17a would be the hashes of blocks: +// [ 17a 16a 15 14 13 12 11 10 9 8 7 6 4... ] +type BlockLocator []*chainhash.Hash + +// orphanBlock represents a block that we don't yet have the parent for. +// It is a normal block plus an expiration time to prevent caching the orphan +// forever. +type orphanBlock struct { + block *block2.Block + expiration time.Time +} + +// BestState houses information about the current best block and other info related to the state of the main chain as it +// exists from the point of view of the current best block. The BestSnapshot method can be used to obtain access to this +// information in a concurrent safe manner and the data will not be changed out from under the caller when chain state +// changes occur as the function name implies. However, the returned snapshot must be treated as immutable since it is +// shared by all callers. +type BestState struct { + Hash chainhash.Hash // The hash of the block. + Height int32 // The height of the block. + Version int32 + Bits uint32 // The difficulty bits of the block. + BlockSize uint64 // The size of the block. + BlockWeight uint64 // The weight of the block. + NumTxns uint64 // The number of txns in the block. + TotalTxns uint64 // The total number of txns in the chain. + MedianTime time.Time // Median time as per CalcPastMedianTime. +} + +// newBestState returns a new best stats instance for the given parameters. +func newBestState( + node *BlockNode, blockSize, blockWeight, numTxns, + totalTxns uint64, medianTime time.Time, +) *BestState { + return &BestState{ + Hash: node.hash, + Height: node.height, + Version: node.version, + Bits: node.bits, + BlockSize: blockSize, + BlockWeight: blockWeight, + NumTxns: numTxns, + TotalTxns: totalTxns, + MedianTime: medianTime, + } +} + +// BlockChain provides functions for working with the bitcoin block chain. It +// includes functionality such as rejecting duplicate blocks, ensuring blocks +// follow all rules, orphan handling, checkpoint handling, and best chain +// selection with reorganization. +type BlockChain struct { + // The following fields are set when the instance is created and can't be + // changed afterwards so there is no need to protect them with a separate mutex. + checkpoints []chaincfg.Checkpoint + checkpointsByHeight map[int32]*chaincfg.Checkpoint + db database.DB + params *chaincfg.Params + timeSource MedianTimeSource + sigCache *txscript.SigCache + indexManager IndexManager + hashCache *txscript.HashCache + // The following fields are calculated based upon the provided chain parameters. + // They are also set when the instance is created and can't be changed + // afterwards, so there is no need to protect them with a separate mutex. + minRetargetTimespan int64 // target timespan / adjustment factor + maxRetargetTimespan int64 // target timespan * adjustment factor + blocksPerRetarget int32 // target timespan / target time per block + // ChainLock protects concurrent access to the vast majority of the fields in this struct below this point. + ChainLock sync.RWMutex + // These fields are related to the memory block index. They both have their own + // locks, however they are often also protected by the chain lock to help + // prevent logic races when blocks are being processed. index houses the entire + // block index in memory. The block index is a tree-shaped structure. BestChain + // tracks the current active chain by making use of an efficient chain view into + // the block index. + Index *blockIndex + BestChain *chainView + // These fields are related to handling of orphan blocks. They are protected by + // a combination of the chain lock and the orphan lock. + orphanLock sync.RWMutex + orphans map[chainhash.Hash]*orphanBlock + prevOrphans map[chainhash.Hash][]*orphanBlock + oldestOrphan *orphanBlock + // These fields are related to checkpoint handling. They are protected by the chain lock. + nextCheckpoint *chaincfg.Checkpoint + checkpointNode *BlockNode + // The state is used as a fairly efficient way to cache information about the + // current best chain state that is returned to callers when requested. It + // operates on the principle of MVCC such that any time a new block becomes the + // best block, the state pointer is replaced with a new struct and the old state + // is left untouched. In this way, multiple callers can be pointing to different + // best chain states. This is acceptable for most callers because the state is + // only being queried at a specific point in time. In addition, some of the + // fields are stored in the database so the chain state can be quickly + // reconstructed on load. + stateLock sync.RWMutex + stateSnapshot *BestState + // // The following caches are used to efficiently keep track of the current deployment threshold state of each rule + // // change deployment. This information is stored in the database so it can be quickly reconstructed on load. + // // warningCaches caches the current deployment threshold state for blocks in each of the **possible** deployments. + // // This is used in order to detect when new unrecognized rule changes are being voted on and/or have been activated + // // such as will be the case when older versions of the software are being used deploymentCaches caches the current + // // deployment threshold state for blocks in each of the actively defined deployments. + // + // warningCaches []thresholdStateCache + // deploymentCaches []thresholdStateCache + // // The following fields are used to determine if certain warnings have already been shown. unknownRulesWarned refers + // // to warnings due to unknown rules being activated. unknownVersionsWarned refers to warnings due to unknown + // // versions being mined. + // unknownRulesWarned bool + // unknownVersionsWarned bool + // The notifications field stores a slice of callbacks to be executed on certain + // blockchain events. + notifications []NotificationCallback + notificationsLock sync.RWMutex + // DifficultyAdjustments keeps track of the latest difficulty adjustment for each algorithm + DifficultyAdjustments map[string]float64 + DifficultyBits atomic.Value + DifficultyHeight atomic.Int32 +} + +// HaveBlock returns whether or not the chain instance has the block represented +// by the passed hash. This includes checking the various places a block can be +// like part of the main chain, on a side chain, or in the orphan pool. This +// function is safe for concurrent access. +func (b *BlockChain) HaveBlock(hash *chainhash.Hash) (bool, error) { + exists, e := b.blockExists(hash) + if e != nil { + return false, e + } + return exists || b.IsKnownOrphan(hash), nil +} + +// IsKnownOrphan returns whether the passed hash is currently a known orphan. +// Keep in mind that only a limited number of orphans are held onto for a +// limited amount of time so this function must not be used as an absolute way +// to test if a block is an orphan block. A full block (as opposed to just its +// hash) must be passed to ProcessBlock for that purpose. +// +// However, calling ProcessBlock with an orphan that already exists results in +// an error, so this function provides a mechanism for a caller to intelligently +// detect *recent* duplicate orphans and react accordingly. This function is +// safe for concurrent access. +func (b *BlockChain) IsKnownOrphan(hash *chainhash.Hash) bool { + // Protect concurrent access. Using a read lock only so multiple readers can query without blocking each other. + b.orphanLock.RLock() + _, exists := b.orphans[*hash] + b.orphanLock.RUnlock() + return exists +} + +// GetOrphanRoot returns the head of the chain for the provided hash from the +// map of orphan blocks. This function is safe for concurrent access. +func (b *BlockChain) GetOrphanRoot(hash *chainhash.Hash) *chainhash.Hash { + // Protect concurrent access. Using a read lock only so multiple readers can query without blocking each other. + b.orphanLock.RLock() + defer b.orphanLock.RUnlock() + // Keep looping while the parent of each orphaned block is known and is an orphan itself. + orphanRoot := hash + prevHash := hash + for { + orphan, exists := b.orphans[*prevHash] + if !exists { + break + } + orphanRoot = prevHash + prevHash = &orphan.block.WireBlock().Header.PrevBlock + } + return orphanRoot +} + +// removeOrphanBlock removes the passed orphan block from the orphan pool and +// previous orphan index. +func (b *BlockChain) removeOrphanBlock(orphan *orphanBlock) { + // Protect concurrent access. + b.orphanLock.Lock() + defer b.orphanLock.Unlock() + // Remove the orphan block from the orphan pool. + orphanHash := orphan.block.Hash() + delete(b.orphans, *orphanHash) + // Remove the reference from the previous orphan index too. An indexing for loop + // is intentionally used over a range here as range does not reevaluate the + // slice on each iteration nor does it adjust the index for the modified slice. + prevHash := &orphan.block.WireBlock().Header.PrevBlock + orphans := b.prevOrphans[*prevHash] + for i := 0; i < len(orphans); i++ { + hash := orphans[i].block.Hash() + if hash.IsEqual(orphanHash) { + copy(orphans[i:], orphans[i+1:]) + orphans[len(orphans)-1] = nil + orphans = orphans[:len(orphans)-1] + i-- + } + } + b.prevOrphans[*prevHash] = orphans + // Remove the map entry altogether if there are no longer any orphans which + // depend on the parent hash. + if len(b.prevOrphans[*prevHash]) == 0 { + delete(b.prevOrphans, *prevHash) + } +} + +// addOrphanBlock adds the passed block (which is already determined to be an +// orphan prior calling this function) to the orphan pool. It lazily cleans up +// any expired blocks so a separate cleanup poller doesn't need to be run. It +// also imposes a maximum limit on the number of outstanding orphan blocks and +// will remove the oldest received orphan block if the limit is exceeded. +func (b *BlockChain) addOrphanBlock(block *block2.Block) { + // Remove expired orphan blocks. + for _, oBlock := range b.orphans { + if time.Now().After(oBlock.expiration) { + b.removeOrphanBlock(oBlock) + continue + } + // Update the oldest orphan block pointer so it can be discarded in case the orphan pool fills up. + if b.oldestOrphan == nil || oBlock.expiration.Before( + b.oldestOrphan. + expiration, + ) { + b.oldestOrphan = oBlock + } + } + // Limit orphan blocks to prevent memory exhaustion. + if len(b.orphans)+1 > maxOrphanBlocks { + // Remove the oldest orphan to make room for the new one. + b.removeOrphanBlock(b.oldestOrphan) + b.oldestOrphan = nil + } + // Protect concurrent access. This is intentionally done here instead of near + // the top since removeOrphanBlock does its own locking and the range iterator + // is not invalidated by removing map entries. + b.orphanLock.Lock() + defer b.orphanLock.Unlock() + // Insert the block into the orphan map with an expiration time 1 hour from now. + expiration := time.Now().Add(time.Hour) + oBlock := &orphanBlock{ + block: block, + expiration: expiration, + } + b.orphans[*block.Hash()] = oBlock + // Add to previous hash lookup index for faster dependency lookups. + prevHash := &block.WireBlock().Header.PrevBlock + b.prevOrphans[*prevHash] = append(b.prevOrphans[*prevHash], oBlock) +} + +// +// // SequenceLock represents the converted relative lock-time in seconds, and absolute block-height for a transaction +// // input's relative lock-times. According to SequenceLock after the referenced input has been confirmed within a block a +// // transaction spending that input can be included into a block either after 'seconds' (according to past median time) +// // or once the 'BlockHeight' has been reached. +// type SequenceLock struct { +// Seconds int64 +// BlockHeight int32 +// } + +// // CalcSequenceLock computes a relative lock-time SequenceLock for the passed transaction using the passed UtxoViewpoint +// // to obtain the past median time for blocks in which the referenced inputs of the transactions were included within. +// // The generated SequenceLock lock can be used in conjunction with a block height and adjusted median block time to +// // determine if all the inputs referenced within a transaction have reached sufficient maturity allowing the candidate +// // transaction to be included in a block. This function is safe for concurrent access. +// func (b *BlockChain) CalcSequenceLock(tx *util.Tx, utxoView *UtxoViewpoint, mempool bool) (*SequenceLock, error) { +// b.chainLock.Lock() +// defer b.chainLock.Unlock() +// return b.calcSequenceLock(b.BestChain.Tip(), tx, utxoView, mempool) +// } + +// // calcSequenceLock computes the relative lock-times for the passed transaction. See the exported version +// // CalcSequenceLock for further details. This function MUST be called with the chain state lock held (for writes). +// func (b *BlockChain) calcSequenceLock(node *BlockNode, tx *util.Tx, utxoView *UtxoViewpoint, mempool bool) (*SequenceLock, error) { +// // A value of -1 for each relative lock type represents a relative time lock value that will allow a transaction to +// // be included in a block at any given height or time. +// // +// // This value is returned as the relative lock time in the case that BIP 68 is disabled, or has not yet been +// // activated. +// sequenceLock := &SequenceLock{Seconds: -1, BlockHeight: -1} +// // The sequence locks semantics are always active for transactions within the mempool. +// csvSoftforkActive := mempool +// // If we're performing block validation, then we need to query the BIP9 state. +// if !csvSoftforkActive { +// // Obtain the latest BIP9 version bits state for the CSV-package soft -fork deployment. The adherence of +// // sequence locks depends on the current soft-fork state. +// csvState, e := b.deploymentState(node.parent, chaincfg.DeploymentCSV) +// if e != nil { +// // return nil, e +// } +// csvSoftforkActive = csvState == ThresholdActive +// } +// // If the transaction's version is less than 2, and BIP 68 has not yet been activated then sequence locks are +// // disabled. Additionally, sequence locks don't apply to coinbase transactions Therefore, we return sequence lock +// // values of -1 indicating that this transaction can be included within a block at any given height or time. +// mTx := tx.MsgTx() +// sequenceLockActive := mTx.Version >= 2 && csvSoftforkActive +// if !sequenceLockActive || IsCoinBase(tx) { +// return sequenceLock, nil +// } +// // Grab the next height from the PoV of the passed BlockNode to use for inputs present in the mempool. +// nextHeight := node.height + 1 +// for txInIndex, txIn := range mTx.TxIn { +// utxo := utxoView.LookupEntry(txIn.PreviousOutPoint) +// if utxo == nil { +// str := fmt.Sprintf( +// "output %v referenced from transaction %s:%d either does not"+ +// " exist or has already been spent", +// txIn.PreviousOutPoint, tx.Hash(), txInIndex, +// ) +// return sequenceLock, ruleError(ErrMissingTxOut, str) +// } +// // If the input height is set to the mempool height, then we assume the transaction makes it into the next block +// // when evaluating its sequence blocks. +// inputHeight := utxo.BlockHeight() +// if inputHeight == 0x7fffffff { +// inputHeight = nextHeight +// } +// // Given a sequence number, we apply the relative time lock mask in order to obtain the time lock delta required +// // before this input can be spent. +// sequenceNum := txIn.Sequence +// relativeLock := int64(sequenceNum & wire.SequenceLockTimeMask) +// switch { +// // Relative time locks are disabled for this input, so we can skip any further calculation. +// case sequenceNum&wire.SequenceLockTimeDisabled == wire.SequenceLockTimeDisabled: +// continue +// case sequenceNum&wire.SequenceLockTimeIsSeconds == wire.SequenceLockTimeIsSeconds: +// // This input requires a relative time lock expressed in seconds before it can be spent. Therefore, we need +// // to query for the block prior to the one in which this input was included within so we can compute the +// // past median time for the block prior to the one which included this referenced output. +// prevInputHeight := inputHeight - 1 +// if prevInputHeight < 0 { +// prevInputHeight = 0 +// } +// blockNode := node.Ancestor(prevInputHeight) +// medianTime := blockNode.CalcPastMedianTime() +// // Time based relative time-locks as defined by BIP 68 have a time granularity of RelativeLockSeconds, so we +// // shift left by this amount to convert to the proper relative time-lock. +// // +// // We also subtract one from the relative lock to maintain the original lockTime semantics. +// timeLockSeconds := (relativeLock << wire.SequenceLockTimeGranularity) - 1 +// timeLock := medianTime.Unix() + timeLockSeconds +// if timeLock > sequenceLock.Seconds { +// sequenceLock.Seconds = timeLock +// } +// default: +// // The relative lock-time for this input is expressed in blocks so we calculate the relative offset from the +// // input's height as its converted absolute lock-time. +// // +// // We subtract one from the relative lock in order to maintain the original lockTime semantics. +// blockHeight := inputHeight + int32(relativeLock-1) +// if blockHeight > sequenceLock.BlockHeight { +// sequenceLock.BlockHeight = blockHeight +// } +// } +// } +// return sequenceLock, nil +// } + +// // LockTimeToSequence converts the passed relative locktime to a sequence number in accordance to BIP-68. See: +// // https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki +// // * (Compatibility) +// func LockTimeToSequence(isSeconds bool, locktime uint32) uint32 { +// // If we're expressing the relative lock time in blocks, then the corresponding sequence number is simply the +// // desired input age. +// if !isSeconds { +// return locktime +// } +// // Set the 22nd bit which indicates the lock time is in seconds, then shift the locktime over by 9 since the time +// // granularity is in 512 -second intervals (2^9). This results in a max lock-time of 33,553, 920 seconds, or 1.1 +// // years. +// return wire.SequenceLockTimeIsSeconds | +// locktime>>wire.SequenceLockTimeGranularity +// } + +// getReorganizeNodes finds the fork point between the main chain and the passed +// node and returns a list of block nodes that would need to be detached from +// the main chain and a list of block nodes that would need to be attached to +// the fork point ( which will be the end of the main chain after detaching the +// returned list of block nodes) in order to reorganize the chain such that the +// passed node is the new end of the main chain. The lists will be empty if the +// passed node is not on a side chain. This function may modify node statuses in +// the block index without flushing. This function MUST be called with the chain +// state lock held ( for reads). +func (b *BlockChain) getReorganizeNodes(node *BlockNode) (*list.List, *list.List) { + attachNodes := list.New() + detachNodes := list.New() + // Do not reorganize to a known invalid chain. Ancestors deeper than the direct + // parent are checked below but this is a quick check before doing more + // unnecessary work. + if b.Index.NodeStatus(node.parent).KnownInvalid() { + b.Index.SetStatusFlags(node, statusInvalidAncestor) + return detachNodes, attachNodes + } + // Find the fork point (if any) adding each block to the list of nodes to attach + // to the main tree. + // + // Push them onto the list in reverse order so they are attached in the appropriate order when iterating the list + // later. + forkNode := b.BestChain.FindFork(node) + invalidChain := false + for n := node; n != nil && n != forkNode; n = n.parent { + if b.Index.NodeStatus(n).KnownInvalid() { + invalidChain = true + break + } + attachNodes.PushFront(n) + } + // If any of the node's ancestors are invalid, unwind attachNodes, marking each + // one as invalid for future reference. + if invalidChain { + var next *list.Element + for e := attachNodes.Front(); e != nil; e = next { + next = e.Next() + n := attachNodes.Remove(e).(*BlockNode) + b.Index.SetStatusFlags(n, statusInvalidAncestor) + } + return detachNodes, attachNodes + } + // Start from the end of the main chain and work backwards until the common + // ancestor adding each block to the list of nodes to detach from the main + // chain. + for n := b.BestChain.Tip(); n != nil && n != forkNode; n = n.parent { + detachNodes.PushBack(n) + } + return detachNodes, attachNodes +} + +// connectBlock handles connecting the passed node/block to the end of the main +// (best) chain. +// +// This passed utxo view must have all referenced txos the block spends marked +// as spent and all of the new txos the block creates added to it. +// +// In addition, the passed stxos slice must be populated with all of the +// information for the spent txos. +// +// This approach is used because the connection validation that must happen +// prior to calling this function requires the same details, so it would be +// inefficient to repeat it. This function MUST be called with the chain state +// lock held (for writes). +func (b *BlockChain) connectBlock( + node *BlockNode, block *block2.Block, + view *UtxoViewpoint, stxos []SpentTxOut, +) (e error) { + // Make sure it's extending the end of the best chain. + prevHash := &block.WireBlock().Header.PrevBlock + if !prevHash.IsEqual(&b.BestChain.Tip().hash) { + str := "connectBlock must be called with a block that extends the" + + " main chain" + D.Ln(str) + return AssertError(str) + } + // Sanity check the correct number of stxos are provided. + if len(stxos) != countSpentOutputs(block) { + str := "connectBlock called with inconsistent spent transaction out" + + " information" + D.Ln(str) + return AssertError(str) + } + + // // No warnings about unknown rules or versions until the chain is current. + // if b.isCurrent() { + // // // Warn if any unknown new rules are either about to activate or have already + // // // been activated. + // // if e := b.warnUnknownRuleActivations(node); E.Chk(e) { + // // T.Ln("warnUnknownRuleActivations ", err) + // // return e + // // } + // } + // Write any block status changes to DB before updating best state. + T.Ln("flushing block status changes to db before updating best state") + if e = b.Index.flushToDB(); E.Chk(e) { + return e + } + // Generate a new best state snapshot that will be used to update the database and later memory if all database + // updates are successful. + b.stateLock.RLock() + curTotalTxns := b.stateSnapshot.TotalTxns + b.stateLock.RUnlock() + numTxns := uint64(len(block.WireBlock().Transactions)) + blockSize := uint64(block.WireBlock().SerializeSize()) + blockWeight := uint64(GetBlockWeight(block)) + state := newBestState( + node, blockSize, blockWeight, numTxns, + curTotalTxns+numTxns, node.CalcPastMedianTime(), + ) + // Atomically insert info into the database. + T.Ln("inserting block into database") + e = b.db.Update( + func(dbTx database.Tx) (e error) { + // update best block state. + e = dbPutBestState(dbTx, state, node.workSum) + if e != nil { + T.Ln("dbPutBestState", e) + return e + } + // Add the block hash and height to the block index which tracks the main chain. + e = dbPutBlockIndex(dbTx, block.Hash(), node.height) + if e != nil { + T.Ln("dbPutBlockIndex", e) + return e + } + // update the utxo set using the state of the utxo view. This entails removing all of the utxos spent and adding + // the new ones created by the block. + e = dbPutUtxoView(dbTx, view) + if e != nil { + T.Ln("dbPutUtxoView", e) + return e + } + + // Update the transaction spend journal by adding a record for the block that contains all txos spent by it. + e = dbPutSpendJournalEntry(dbTx, block.Hash(), stxos) + if e != nil { + T.Ln("dbPutSpendJournalEntry", e) + return e + } + // Allow the index manager to call each of the currently active optional indexes with the block being connected + // so they can update themselves accordingly + if b.indexManager != nil { + e := b.indexManager.ConnectBlock(dbTx, block, stxos) + if e != nil { + T.Ln("connectBlock ", e) + return e + } + } + return nil + }, + ) + if e != nil { + T.Ln("error updating database ", e) + return e + } + // Prune fully spent entries and mark all entries in the view unmodified now that the modifications have been + // committed to the database. + T.Ln("committing new view") + view.commit() + + // This node is now the end of the best chain. + T.Ln("setting new chain tip") + b.BestChain.SetTip(node) + // Update the state for the best block. Notice how this replaces the entire struct instead of updating the existing + // one. This effectively allows the old version to act as a snapshot which callers can use freely without needing to + // hold a lock for the duration. See the comments on the state variable for more details. + b.stateLock.Lock() + b.stateSnapshot = state + b.stateLock.Unlock() + // + // // TODO: this should not run if the chain is syncing + // tN := time.Now() + // tB, e := b.CalcNextRequiredDifficultyPlan9Controller(node) + // if e != nil { + // L.Script // } + // T.Ln(time.Now().Sub(tN), "to compute all block difficulties") + // node.Bits = tB + // + // Notify the caller that the block was connected to the main chain. The caller would typically want to react with + // actions such as updating wallets. + T.Ln("sending notifications for new block") + b.ChainLock.Unlock() + b.sendNotification(NTBlockConnected, block) + b.ChainLock.Lock() + return nil +} + +// disconnectBlock handles disconnecting the passed node/block from the end of the main (best) chain. This function MUST +// be called with the chain state lock held (for writes). +func (b *BlockChain) disconnectBlock( + node *BlockNode, block *block2.Block, + view *UtxoViewpoint, +) (e error) { + // Make sure the node being disconnected is the end of the best chain. + if !node.hash.IsEqual(&b.BestChain.Tip().hash) { + return AssertError( + "disconnectBlock must be called with the block at" + + " the end of the main chain", + ) + } + // Load the previous block since some details for it are needed below. + prevNode := node.parent + var prevBlock *block2.Block + e = b.db.View( + func(dbTx database.Tx) (e error) { + prevBlock, e = dbFetchBlockByNode(dbTx, prevNode) + return e + }, + ) + if e != nil { + return e + } + // Write any block status changes to DB before updating best state. + e = b.Index.flushToDB() + if e != nil { + return e + } + // Generate a new best state snapshot that will be used to update the database and later memory if all database + // updates are successful. + b.stateLock.RLock() + curTotalTxns := b.stateSnapshot.TotalTxns + b.stateLock.RUnlock() + numTxns := uint64(len(prevBlock.WireBlock().Transactions)) + blockSize := uint64(prevBlock.WireBlock().SerializeSize()) + blockWeight := uint64(GetBlockWeight(prevBlock)) + newTotalTxns := curTotalTxns - uint64(len(block.WireBlock().Transactions)) + state := newBestState( + prevNode, blockSize, blockWeight, numTxns, + newTotalTxns, prevNode.CalcPastMedianTime(), + ) + e = b.db.Update( + func(dbTx database.Tx) (e error) { + // Update best block state. + e = dbPutBestState(dbTx, state, node.workSum) + if e != nil { + return e + } + // Remove the block hash and height from the block index which tracks the main chain. + e = dbRemoveBlockIndex(dbTx, block.Hash(), node.height) + if e != nil { + return e + } + // Update the utxo set using the state of the utxo view. This entails restoring all of the utxos spent and + // removing the new ones created by the block. + e = dbPutUtxoView(dbTx, view) + if e != nil { + return e + } + // Before we delete the spend journal entry for this back, we'll fetch it as is so the indexers can utilize if + // needed. + stxos, e := dbFetchSpendJournalEntry(dbTx, block) + if e != nil { + return e + } + // Update the transaction spend journal by removing the record that contains all txos spent by the block. + e = dbRemoveSpendJournalEntry(dbTx, block.Hash()) + if e != nil { + return e + } + // Allow the index manager to call each of the currently active optional indexes with the block being + // disconnected so they can update themselves accordingly. + if b.indexManager != nil { + e := b.indexManager.DisconnectBlock(dbTx, block, stxos) + if e != nil { + return e + } + } + return nil + }, + ) + if e != nil { + return e + } + // Prune fully spent entries and mark all entries in the view unmodified now that the modifications have been + // committed to the database. + view.commit() + // This node's parent is now the end of the best chain. + b.BestChain.SetTip(node.parent) + // Update the state for the best block. Notice how this replaces the entire struct instead of updating the existing + // one. This effectively allows the old version to act as a snapshot which callers can use freely without needing to + // hold a lock for the duration. See the comments on the state variable for more details. + b.stateLock.Lock() + b.stateSnapshot = state + b.stateLock.Unlock() + // Notify the caller that the block was disconnected from the main chain. The caller would typically want to react + // with actions such as updating wallets. + b.ChainLock.Unlock() + b.sendNotification(NTBlockDisconnected, block) + b.ChainLock.Lock() + return nil +} + +// countSpentOutputs returns the number of utxos the passed block spends. +func countSpentOutputs(block *block2.Block) int { + // Exclude the coinbase transaction since it can't spend anything. + var numSpent int + for _, tx := range block.Transactions()[1:] { + numSpent += len(tx.MsgTx().TxIn) + } + return numSpent +} + +// reorganizeChain reorganizes the block chain by disconnecting the nodes in the detachNodes list and connecting the +// nodes in the attach list. It expects that the lists are already in the correct order and are in sync with the end of +// the current best chain. Specifically, nodes that are being disconnected must be in reverse order ( think of popping +// them off the end of the chain) and nodes the are being attached must be in forwards order ( think pushing them onto +// the end of the chain). This function may modify node statuses in the block index without flushing. This function MUST +// be called with the chain state lock held ( for writes). +func (b *BlockChain) reorganizeChain(detachNodes, attachNodes *list.List) (e error) { + // Nothing to do if no reorganize nodes were provided. + if detachNodes.Len() == 0 && attachNodes.Len() == 0 { + return nil + } + // Ensure the provided nodes match the current best chain. + tip := b.BestChain.Tip() + if detachNodes.Len() != 0 { + firstDetachNode := detachNodes.Front().Value.(*BlockNode) + if firstDetachNode.hash != tip.hash { + return AssertError( + fmt.Sprintf( + "reorganize nodes to detach are "+ + "not for the current best chain -- first detach node %v, "+ + "current chain %v", &firstDetachNode.hash, &tip.hash, + ), + ) + } + } + // Ensure the provided nodes are for the same fork point. + if attachNodes.Len() != 0 && detachNodes.Len() != 0 { + firstAttachNode := attachNodes.Front().Value.(*BlockNode) + lastDetachNode := detachNodes.Back().Value.(*BlockNode) + if firstAttachNode.parent.hash != lastDetachNode.parent.hash { + return AssertError( + fmt.Sprintf( + "reorganize nodes do not have the same fork point -- first"+ + " attach parent %v, last detach parent %v", + &firstAttachNode.parent.hash, &lastDetachNode.parent.hash, + ), + ) + } + } + // Track the old and new best chains heads. + oldBest := tip + newBest := tip + // All of the blocks to detach and related spend journal entries needed to unspend transaction outputs in the blocks + // being disconnected must be loaded from the database during the reorg check phase below and then they are needed + // again when doing the actual database updates. Rather than doing two loads, cache the loaded data into these + // slices. + detachBlocks := make([]*block2.Block, 0, detachNodes.Len()) + detachSpentTxOuts := make([][]SpentTxOut, 0, detachNodes.Len()) + attachBlocks := make([]*block2.Block, 0, attachNodes.Len()) + // Disconnect all of the blocks back to the point of the fork. This entails loading the blocks and their associated + // spent txos from the database and using that information to unspend all of the spent txos and remove the utxos + // created by the blocks. + view := NewUtxoViewpoint() + view.SetBestHash(&oldBest.hash) + for e := detachNodes.Front(); e != nil; e = e.Next() { + n := e.Value.(*BlockNode) + var block *block2.Block + e := b.db.View( + func(dbTx database.Tx) (e error) { + block, e = dbFetchBlockByNode(dbTx, n) + return e + }, + ) + if e != nil { + return e + } + if n.hash != *block.Hash() { + return AssertError( + fmt.Sprintf( + "detach block node hash %v ("+ + "height %v) does not match previous parent block hash %v", + &n.hash, n.height, block.Hash(), + ), + ) + } + // Load all of the utxos referenced by the block that aren't already in the view. + e = view.fetchInputUtxos(b.db, block) + if e != nil { + return e + } + // Load all of the spent txos for the block from the spend journal. + var stxos []SpentTxOut + e = b.db.View( + func(dbTx database.Tx) (e error) { + stxos, e = dbFetchSpendJournalEntry(dbTx, block) + return e + }, + ) + if e != nil { + return e + } + // Store the loaded block and spend journal entry for later. + detachBlocks = append(detachBlocks, block) + detachSpentTxOuts = append(detachSpentTxOuts, stxos) + e = view.disconnectTransactions(b.db, block, stxos) + if e != nil { + return e + } + newBest = n.parent + } + // Set the fork point only if there are nodes to attach since otherwise blocks are only being disconnected and thus + // there is no fork point. + var forkNode *BlockNode + if attachNodes.Len() > 0 { + forkNode = newBest + } + // Perform several checks to verify each block that needs to be attached to the main chain can be connected without + // violating any rules and without actually connecting the block. + // + // NOTE: These checks could be done directly when connecting a block, however the downside to that approach is that + // if any of these checks fail after disconnecting some blocks or attaching others, all of the operations have to be + // rolled back to get the chain back into the state it was before the rule violation (or other failure). There are + // at least a couple of ways accomplish that rollback, but both involve tweaking the chain and/or database. This + // approach catches these issues before ever modifying the chain. + for e := attachNodes.Front(); e != nil; e = e.Next() { + n := e.Value.(*BlockNode) + var block *block2.Block + er := b.db.View( + func(dbTx database.Tx) (e error) { + block, e = dbFetchBlockByNode(dbTx, n) + return e + }, + ) + if er != nil { + return er + } + // Store the loaded block for later. + attachBlocks = append(attachBlocks, block) + // Skip checks if node has already been fully validated. Although checkConnectBlock gets skipped, we still need + // to update the UTXO view. + if b.Index.NodeStatus(n).KnownValid() { + er = view.fetchInputUtxos(b.db, block) + if er != nil { + return er + } + er = view.connectTransactions(block, nil) + if er != nil { + return er + } + newBest = n + continue + } + // Notice the spent txout details are not requested here and thus will not be generated. + // + // This is done because the state is not being immediately written to the database, so it is not needed. + // + // In the case the block is determined to be invalid due to a rule violation, mark it as invalid and mark all of + // its descendants as having an invalid ancestor. + er = b.checkConnectBlock(n, block, view, nil) + if er != nil { + if _, ok := er.(RuleError); ok { + b.Index.SetStatusFlags(n, statusValidateFailed) + for de := e.Next(); de != nil; de = de.Next() { + dn := de.Value.(*BlockNode) + b.Index.SetStatusFlags(dn, statusInvalidAncestor) + } + } + return er + } + b.Index.SetStatusFlags(n, statusValid) + newBest = n + } + // Reset the view for the actual connection code below. This is required because the view was previously modified + // when checking if the reorg would be successful and the connection code requires the view to be valid from the + // viewpoint of each block being connected or disconnected. + view = NewUtxoViewpoint() + view.SetBestHash(&b.BestChain.Tip().hash) + // Disconnect blocks from the main chain. + for i, e := 0, detachNodes.Front(); e != nil; i, e = i+1, e.Next() { + n := e.Value.(*BlockNode) + block := detachBlocks[i] + // Load all of the utxos referenced by the block that aren't already in the view. + e := view.fetchInputUtxos(b.db, block) + if e != nil { + return e + } + // Update the view to unspend all of the spent txos and remove the utxos created by the block. + e = view.disconnectTransactions( + b.db, block, + detachSpentTxOuts[i], + ) + if e != nil { + return e + } + // Update the database and chain state. + e = b.disconnectBlock(n, block, view) + if e != nil { + return e + } + } + // Connect the new best chain blocks. + for i, e := 0, attachNodes.Front(); e != nil; i, e = i+1, e.Next() { + n := e.Value.(*BlockNode) + block := attachBlocks[i] + // Load all of the utxos referenced by the block that aren't already in the view. + e := view.fetchInputUtxos(b.db, block) + if e != nil { + return e + } + // Update the view to mark all utxos referenced by the block as spent and add all transactions being created by + // this block to it. Also, provide an stxo slice so the spent txout details are generated. + stxos := make([]SpentTxOut, 0, countSpentOutputs(block)) + e = view.connectTransactions(block, &stxos) + if e != nil { + return e + } + // Update the database and chain state. + e = b.connectBlock(n, block, view, stxos) + if e != nil { + return e + } + } + // Log the point where the chain forked and old and new best chain + // heads. + if forkNode != nil { + I.F( + "REORGANIZE: Chain forks at %v (height %v)", + forkNode.hash, forkNode.height, + ) + } + I.F( + "REORGANIZE: Old best chain head was %v (height %v)", + &oldBest.hash, oldBest.height, + ) + I.F( + "REORGANIZE: New best chain head is %v (height %v)", + newBest.hash, newBest.height, + ) + return nil +} + +// connectBestChain handles connecting the passed block to the chain while respecting proper chain selection according +// to the chain with the most proof of work. In the typical case the new block simply extends the main chain. +// +// However it may also be extending (or creating) a side chain (fork) which may or may not end up becoming the main +// chain depending on which fork cumulatively has the most proof of work. It returns whether or not the block ended up +// on the main chain (either due to extending the main chain or causing a reorganization to become the main chain). +// +// The flags modify the behavior of this function as follows: +// +// - BFFastAdd: Avoids several expensive transaction validation operations. +// This is useful when using checkpoints. +// +// This function MUST be called with the chain state lock held (for writes). +func (b *BlockChain) connectBestChain(node *BlockNode, block *block2.Block, flags BehaviorFlags) (bool, error) { + T.Ln("running connectBestChain") + fastAdd := flags&BFFastAdd == BFFastAdd + flushIndexState := func() { + // Intentionally ignore errors writing updated node status to DB. If it fails to write, it's not the end of the + // world. If the block is valid, we flush in connectBlock and if the block is invalid, the worst that can happen + // is we revalidate the block after a restart. + if writeErr := b.Index.flushToDB(); writeErr != nil { + T.Ln("ScriptError flushing block index changes to disk:", writeErr) + } + } + // We are extending the main (best) chain with a new block. This is the most common case. + parentHash := &block.WireBlock().Header.PrevBlock + if parentHash.IsEqual(&b.BestChain.Tip().hash) { + T.Ln("extending main chain") + // Skip checks if node has already been fully validated. + fastAdd = fastAdd || b.Index.NodeStatus(node).KnownValid() + // Perform several checks to verify the block can be connected to the main chain without violating any rules and + // without actually connecting the block. + view := NewUtxoViewpoint() + view.SetBestHash(parentHash) + stxos := make([]SpentTxOut, 0, countSpentOutputs(block)) + if !fastAdd { + e := b.checkConnectBlock(node, block, view, &stxos) + if e == nil { + b.Index.SetStatusFlags(node, statusValid) + } else if _, ok := e.(RuleError); ok { + b.Index.SetStatusFlags(node, statusValidateFailed) + } else { + return false, e + } + flushIndexState() + if e != nil { + return false, e + } + } + // In the fast add case the code to check the block connection was skipped, so the utxo view needs to load the + // referenced utxos, spend them, and add the new utxos being created by this block. + if fastAdd { + e := view.fetchInputUtxos(b.db, block) + if e != nil { + return false, e + } + e = view.connectTransactions(block, &stxos) + if e != nil { + return false, e + } + } + // Connect the block to the main chain. + T.Ln("connecting block to main chain") + var e error + if e = b.connectBlock(node, block, view, stxos); E.Chk(e) { + E.Ln("connect block error: ", e) + // If we got hit with a rule error, then we'll mark that status of the block as invalid and flush the index + // state to disk before returning with the error. + if _, ok := e.(RuleError); ok { + b.Index.SetStatusFlags(node, statusValidateFailed) + } + flushIndexState() + return false, e + } + T.Ln("block connected to main chain") + // If this is fast add, or this block node isn't yet marked as valid, then we'll update its status and flush the + // state to disk again. + if fastAdd || !b.Index.NodeStatus(node).KnownValid() { + b.Index.SetStatusFlags(node, statusValid) + flushIndexState() + } + if fastAdd { + W.F("fastAdd set in the side chain case? %v\n", block.Hash()) + } + return true, nil + } + T.Ln("calculating work sum at new node") + node.workSum = CalcWork(node.bits, node.height, node.version) + // We're extending (or creating) a side chain, but the cumulative work for this new side chain is not enough to make + // it the new chain. + if node.workSum.Cmp(b.BestChain.Tip().workSum) <= 0 { + // Log information about how the block is forking the chain. + f := b.BestChain.FindFork(node) + if f.hash.IsEqual(parentHash) { + T.F( + "FORK: Block %v forks the chain at height %d/block %v, "+ + "but does not cause a reorganize. workSum=%d", + node.hash, f.height, f.hash, f.workSum, + ) + } else { + T.F( + "EXTEND FORK: Height %7d Block %v extends a side chain which"+ + " forks the chain at height %d/block %v. workSum=%d", + node.height, node.hash, f.height, f.hash, f.workSum, + ) + } + return false, nil + } + // We're extending (or creating) a side chain and the cumulative work for this new side chain is more than the old + // best chain, so this side chain needs to become the main chain. In order to accomplish that, find the common + // ancestor of both sides of the fork, disconnect the blocks that form the ( now) old fork from the main chain, and + // attach the blocks that form the new chain to the main chain starting at the common ancestor (the point where the + // chain forked). + detachNodes, attachNodes := b.getReorganizeNodes(node) + // Reorganize the chain. + W.F("REORGANIZE: block %v is causing a reorganize", node.hash) + e := b.reorganizeChain(detachNodes, attachNodes) + // Either getReorganizeNodes or reorganizeChain could have made unsaved changes to the block index, so flush + // regardless of whether there was an error. The index would only be dirty if the block failed to connect, so we can + // ignore any errors writing. + if writeErr := b.Index.flushToDB(); writeErr != nil { + T.Ln("ScriptError flushing block index changes to disk:", writeErr) + } + return e == nil, e +} + +// isCurrent returns whether or not the chain believes it is current. Several factors are used to guess, but the key +// factors that allow the chain to believe it is current are: +// +// - Latest block height is after the latest checkpoint (if enabled) +// - Latest block has a timestamp newer than 24 hours ago +// +// This function MUST be called with the chain state lock held (for reads). +func (b *BlockChain) isCurrent() bool { + // Not current if the latest main (best) chain height is before the latest known good checkpoint (when checkpoints + // are enabled). + checkpoint := b.LatestCheckpoint() + if checkpoint != nil && b.BestChain.Tip().height < checkpoint.Height { + return false + } + // Not current if the latest best block has a timestamp before 24 hours ago. The chain appears to be current if none + // of the checks reported otherwise. + minus24Hours := b.timeSource.AdjustedTime().Add(-24 * time.Hour).Unix() + return b.BestChain.Tip().timestamp >= minus24Hours +} + +// IsCurrent returns whether or not the chain believes it is current. Several factors are used to guess, but the key +// factors that allow the chain to believe it is current are: +// +// - Latest block height is after the latest checkpoint (if enabled) +// - Latest block has a timestamp newer than 24 hours ago +// +// This function is safe for concurrent access. +func (b *BlockChain) IsCurrent() bool { + b.ChainLock.RLock() + defer b.ChainLock.RUnlock() + return b.isCurrent() +} + +// BestSnapshot returns information about the current best chain block and related state as of the current point in +// time. The returned instance must be treated as immutable since it is shared by all callers. This function is safe for +// concurrent access. +func (b *BlockChain) BestSnapshot() *BestState { + b.stateLock.RLock() + snapshot := b.stateSnapshot + b.stateLock.RUnlock() + return snapshot +} + +// HeaderByHash returns the block header identified by the given hash or an error if it doesn't exist. +// +// Note that this will return headers from both the main and side chains. +func (b *BlockChain) HeaderByHash(hash *chainhash.Hash) (bh wire.BlockHeader, e error) { + node := b.Index.LookupNode(hash) + if node == nil { + e = fmt.Errorf("block %s is not known", hash) + return wire.BlockHeader{}, e + } + return node.Header(), nil +} + +// MainChainHasBlock returns whether or not the block with the given hash is in the main chain. This function is safe +// for concurrent access. +func (b *BlockChain) MainChainHasBlock(hash *chainhash.Hash) bool { + node := b.Index.LookupNode(hash) + return node != nil && b.BestChain.Contains(node) +} + +// BlockLocatorFromHash returns a block locator for the passed block hash. See BlockLocator for details on the algorithm +// used to create a block locator. In addition to the general algorithm referenced above, this function will return the +// block locator for the latest known tip of the main (best) chain if the passed hash is not currently known. +// +// This function is safe for concurrent access. +func (b *BlockChain) BlockLocatorFromHash(hash *chainhash.Hash) BlockLocator { + b.ChainLock.RLock() + node := b.Index.LookupNode(hash) + locator := b.BestChain.blockLocator(node) + b.ChainLock.RUnlock() + return locator +} + +// LatestBlockLocator returns a block locator for the latest known tip of the main (best) chain. This function is safe +// for concurrent access. +func (b *BlockChain) LatestBlockLocator() (BlockLocator, error) { + b.ChainLock.RLock() + locator := b.BestChain.BlockLocator(nil) + b.ChainLock.RUnlock() + return locator, nil +} + +// BlockHeightByHash returns the height of the block with the given hash in the main chain. This function is safe for +// concurrent access. +func (b *BlockChain) BlockHeightByHash(hash *chainhash.Hash) (int32, error) { + node := b.Index.LookupNode(hash) + if node == nil || !b.BestChain.Contains(node) { + str := fmt.Sprintf( + "BlockHeightByHash: block %s is not in the main"+ + " chain", hash, + ) + return 0, errNotInMainChain(str) + } + return node.height, nil +} + +// BlockHashByHeight returns the hash of the block at the given height in the main chain. This function is safe for +// concurrent access. +func (b *BlockChain) BlockHashByHeight(blockHeight int32) (*chainhash.Hash, error) { + node := b.BestChain.NodeByHeight(blockHeight) + if node == nil { + str := fmt.Sprintf( + "BlockHashByHeight: no block at height %d exists", + blockHeight, + ) + return nil, errNotInMainChain(str) + } + return &node.hash, nil +} + +// HeightRange returns a range of block hashes for the given start and end heights. It is inclusive of the start height +// and exclusive of the end height. The end height will be limited to the current main chain height. +// +// This function is safe for concurrent access. +func (b *BlockChain) HeightRange(startHeight, endHeight int32) ( + []chainhash. + Hash, error, +) { + // Ensure requested heights are sane. + if startHeight < 0 { + return nil, fmt.Errorf( + "start height of fetch range must not be less"+ + " than zero - got %d", startHeight, + ) + } + if endHeight < startHeight { + return nil, fmt.Errorf( + "end height of fetch range must not be less"+ + " than the start height - got start %d, end %d", + startHeight, endHeight, + ) + } + // There is nothing to do when the start and end heights are the same, so return now to avoid the chain view lock. + if startHeight == endHeight { + return nil, nil + } + // Grab a lock on the chain view to prevent it from changing due to a reorg while building the hashes. + b.BestChain.mtx.Lock() + defer b.BestChain.mtx.Unlock() + // When the requested start height is after the most recent best chain height, there is nothing to do. + latestHeight := b.BestChain.tip().height + if startHeight > latestHeight { + return nil, nil + } + // Limit the ending height to the latest height of the chain. + if endHeight > latestHeight+1 { + endHeight = latestHeight + 1 + } + // Fetch as many as are available within the specified range. + hashes := make([]chainhash.Hash, 0, endHeight-startHeight) + for i := startHeight; i < endHeight; i++ { + hashes = append(hashes, b.BestChain.nodeByHeight(i).hash) + } + return hashes, nil +} + +// HeightToHashRange returns a range of block hashes for the given start height and end hash, inclusive on both ends. +// The hashes are for all blocks that are ancestors of endHash with height greater than or equal to startHeight. +// +// The end hash must belong to a block that is known to be valid. +// +// This function is safe for concurrent access. +func (b *BlockChain) HeightToHashRange( + startHeight int32, + endHash *chainhash.Hash, maxResults int, +) ([]chainhash.Hash, error) { + endNode := b.Index.LookupNode(endHash) + if endNode == nil { + return nil, fmt.Errorf("no known block header with hash %v", endHash) + } + if !b.Index.NodeStatus(endNode).KnownValid() { + return nil, fmt.Errorf("block %v is not yet validated", endHash) + } + endHeight := endNode.height + if startHeight < 0 { + return nil, fmt.Errorf("start height (%d) is below 0", startHeight) + } + if startHeight > endHeight { + return nil, fmt.Errorf( + "start height (%d) is past end height (%d)", + startHeight, endHeight, + ) + } + resultsLength := int(endHeight - startHeight + 1) + if resultsLength > maxResults { + return nil, fmt.Errorf( + "number of results (%d) would exceed max (%d)", + resultsLength, maxResults, + ) + } + // Walk backwards from endHeight to startHeight, collecting block hashes. + node := endNode + hashes := make([]chainhash.Hash, resultsLength) + for i := resultsLength - 1; i >= 0; i-- { + hashes[i] = node.hash + node = node.parent + } + return hashes, nil +} + +// IntervalBlockHashes returns hashes for all blocks that are ancestors of endHash where the block height is a positive +// multiple of interval. +// +// This function is safe for concurrent access. +func (b *BlockChain) IntervalBlockHashes( + endHash *chainhash.Hash, interval int, +) ([]chainhash.Hash, error) { + endNode := b.Index.LookupNode(endHash) + if endNode == nil { + return nil, fmt.Errorf("no known block header with hash %v", endHash) + } + if !b.Index.NodeStatus(endNode).KnownValid() { + return nil, fmt.Errorf("block %v is not yet validated", endHash) + } + endHeight := endNode.height + resultsLength := int(endHeight) / interval + hashes := make([]chainhash.Hash, resultsLength) + b.BestChain.mtx.Lock() + defer b.BestChain.mtx.Unlock() + blockNode := endNode + for index := int(endHeight) / interval; index > 0; index-- { + // Use the BestChain chainView for faster lookups once lookup + // intersects the best chain. + blockHeight := int32(index * interval) + if b.BestChain.contains(blockNode) { + blockNode = b.BestChain.nodeByHeight(blockHeight) + } else { + blockNode = blockNode.Ancestor(blockHeight) + } + hashes[index-1] = blockNode.hash + } + return hashes, nil +} + +// locateInventory returns the node of the block after the first known block in the locator along with the number of +// subsequent nodes needed to either reach the provided stop hash or the provided max number of entries. +// +// In addition, there are two special cases: +// +// - When no locators are provided, the stop hash is treated as a request for that block, so it will either return the +// node associated with the stop hash if it is known, or nil if it is unknown +// +// - When locators are provided, but none of them are known, nodes starting after the genesis block will be returned +// +// This is primarily a helper function for the locateBlocks and locateHeaders functions. This function MUST be called +// with the chain state lock held (for reads). +func (b *BlockChain) locateInventory( + locator BlockLocator, + hashStop *chainhash.Hash, maxEntries uint32, +) (*BlockNode, uint32) { + // There are no block locators so a specific block is being requested as identified by the stop hash. + stopNode := b.Index.LookupNode(hashStop) + if len(locator) == 0 { + if stopNode == nil { + // No blocks with the stop hash were found so there is nothing to do. + return nil, 0 + } + return stopNode, 1 + } + // Find the most recent locator block hash in the main chain. In the case none of the hashes in the locator are in + // the main chain, fall back to the genesis block. + startNode := b.BestChain.Genesis() + for _, hash := range locator { + node := b.Index.LookupNode(hash) + if node != nil && b.BestChain.Contains(node) { + startNode = node + break + } + } + // Start at the block after the most recently known block. When there is no next block it means the most recently + // known block is the tip of the best chain, so there is nothing more to do. + startNode = b.BestChain.Next(startNode) + if startNode == nil { + return nil, 0 + } + // Calculate how many entries are needed. + total := uint32((b.BestChain.Tip().height - startNode.height) + 1) + if stopNode != nil && b.BestChain.Contains(stopNode) && + stopNode.height >= startNode.height { + total = uint32((stopNode.height - startNode.height) + 1) + } + if total > maxEntries { + total = maxEntries + } + return startNode, total +} + +// locateBlocks returns the hashes of the blocks after the first known block in the locator until the provided stop hash +// is reached, or up to the provided max number of block hashes. +// +// See the comment on the exported function for more details on special cases. +// +// This function MUST be called with the chain state lock held (for reads). +func (b *BlockChain) locateBlocks( + locator BlockLocator, hashStop *chainhash.Hash, + maxHashes uint32, +) []chainhash.Hash { + // Find the node after the first known block in the locator and the total number of nodes after it needed while + // respecting the stop hash and max entries. + node, total := b.locateInventory(locator, hashStop, maxHashes) + if total == 0 { + return nil + } + // Populate and return the found hashes. + hashes := make([]chainhash.Hash, 0, total) + for i := uint32(0); i < total; i++ { + hashes = append(hashes, node.hash) + node = b.BestChain.Next(node) + } + return hashes +} + +// LocateBlocks returns the hashes of the blocks after the first known block in the locator until the provided stop hash +// is reached, or up to the provided max number of block hashes. +// +// In addition, there are two special cases: +// +// - When no locators are provided, the stop hash is treated as a request for that block, so it will either return the +// stop hash itself if it is known, or nil if it is unknown +// +// - When locators are provided, but none of them are known, hashes starting after the genesis block will be returned +// +// This function is safe for concurrent access. +func (b *BlockChain) LocateBlocks(locator BlockLocator, hashStop *chainhash.Hash, maxHashes uint32) []chainhash.Hash { + b.ChainLock.RLock() + hashes := b.locateBlocks(locator, hashStop, maxHashes) + b.ChainLock.RUnlock() + return hashes +} + +// locateHeaders returns the headers of the blocks after the first known block in the locator until the provided stop +// hash is reached, or up to the provided max number of block headers. +// +// See the comment on the exported function for more details on special cases. +// +// This function MUST be called with the chain state lock held ( for reads). +func (b *BlockChain) locateHeaders( + locator BlockLocator, + hashStop *chainhash.Hash, + maxHeaders uint32, +) []wire.BlockHeader { + // Find the node after the first known block in the locator and the total number of nodes after it needed while + // respecting the stop hash and max entries. + node, total := b.locateInventory(locator, hashStop, maxHeaders) + if total == 0 { + return nil + } + // Populate and return the found headers. + headers := make([]wire.BlockHeader, 0, total) + for i := uint32(0); i < total; i++ { + headers = append(headers, node.Header()) + node = b.BestChain.Next(node) + } + return headers +} + +// LocateHeaders returns the headers of the blocks after the first known block in the locator until the provided stop +// hash is reached, or up to a max of wire.MaxBlockHeadersPerMsg headers. +// +// In addition, there are two special cases: +// +// - When no locators are provided, the stop hash is treated as a request for that header, so it will either return the +// header for the stop hash itself if it is known, or nil if it is unknown +// +// - When locators are provided, but none of them are known, headers starting after the genesis block will be returned +// +// This function is safe for concurrent access. +func (b *BlockChain) LocateHeaders(locator BlockLocator, hashStop *chainhash.Hash) []wire.BlockHeader { + b.ChainLock.RLock() + headers := b.locateHeaders(locator, hashStop, wire.MaxBlockHeadersPerMsg) + b.ChainLock.RUnlock() + return headers +} + +// IndexManager provides a generic interface that the is called when blocks are connected and disconnected to and from +// the tip of the main chain for the purpose of supporting optional indexes. +type IndexManager interface { + // Init is invoked during chain initialize in order to allow the index manager to initialize itself and any indexes + // it is managing. The channel parameter specifies a channel the caller can close to signal that the process should + // be interrupted. It can be nil if that behavior is not desired. + Init(*BlockChain, <-chan struct{}) error + // ConnectBlock is invoked when a new block has been connected to the main chain. The set of output spent within a + // block is also passed in so indexers can access the previous output scripts input spent if required. + ConnectBlock(database.Tx, *block2.Block, []SpentTxOut) error + // DisconnectBlock is invoked when a block has been disconnected from the main chain. The set of outputs scripts + // that were spent within this block is also returned so indexers can clean up the prior index state for this block. + DisconnectBlock(database.Tx, *block2.Block, []SpentTxOut) error +} + +// Config is a descriptor which specifies the blockchain instance configuration. +type Config struct { + // DB defines the database which houses the blocks and will be used to store all metadata created by this package + // such as the utxo set. This field is required. + DB database.DB + // Interrupt specifies a channel the caller can close to signal that long running operations, such as catching up + // indexes or performing database migrations, should be interrupted. This field can be nil if the caller does not + // desire the behavior. + Interrupt <-chan struct{} + // ChainParams identifies which chain parameters the chain is associated with. This field is required. + ChainParams *chaincfg.Params + // Checkpoints hold caller-defined checkpoints that should be added to the default checkpoints in ChainParams. + // Checkpoints must be sorted by height. This field can be nil if the caller does not wish to specify any + // checkpoints. + Checkpoints []chaincfg.Checkpoint + // TimeSource defines the median time source to use for things such as block processing and determining whether or + // not the chain is current. The caller is expected to keep a reference to the time source as well and add time + // samples from other peers on the network so the local time is adjusted to be in agreement with other peers. + TimeSource MedianTimeSource + // SigCache defines a signature cache to use when when validating signatures. This is typically most useful when + // individual transactions are already being validated prior to their inclusion in a block such as what is usually + // done via a transaction memory pool. This field can be nil if the caller is not interested in using a signature + // cache. + SigCache *txscript.SigCache + // IndexManager defines an index manager to use when initializing the chain and connecting and disconnecting blocks. + // This field can be nil if the caller does not wish to make use of an index manager. + IndexManager IndexManager + // HashCache defines a transaction hash mid-state cache to use when validating transactions. This cache has the + // potential to greatly speed up transaction validation as re-using the pre-calculated mid-state eliminates the + // O(N^2) validation complexity due to the SigHashAll flag. This field can be nil if the caller is not interested in + // using a signature cache. + HashCache *txscript.HashCache +} + +// New returns a BlockChain instance using the provided configuration details. +func New(config *Config) (*BlockChain, error) { + // Enforce required config fields. + if config.DB == nil { + return nil, AssertError("blockchain.New database is nil") + } + if config.ChainParams == nil { + return nil, AssertError("blockchain.New chain parameters nil") + } + if config.TimeSource == nil { + return nil, AssertError("blockchain.New timesource is nil") + } + // Generate a checkpoint by height map from the provided checkpoints and assert the provided checkpoints are sorted + // by height as required. + var checkpointsByHeight map[int32]*chaincfg.Checkpoint + var prevCheckpointHeight int32 + if len(config.Checkpoints) > 0 { + checkpointsByHeight = make(map[int32]*chaincfg.Checkpoint) + for i := range config.Checkpoints { + checkpoint := &config.Checkpoints[i] + if checkpoint.Height <= prevCheckpointHeight { + return nil, AssertError( + "blockchain.New checkpoints are not sorted by height", + ) + } + checkpointsByHeight[checkpoint.Height] = checkpoint + prevCheckpointHeight = checkpoint.Height + } + } + params := config.ChainParams + targetTimespan := params.TargetTimespan + targetTimePerBlock := params.TargetTimePerBlock + adjustmentFactor := params.RetargetAdjustmentFactor + b := BlockChain{ + checkpoints: config.Checkpoints, + checkpointsByHeight: checkpointsByHeight, + db: config.DB, + params: params, + timeSource: config.TimeSource, + sigCache: config.SigCache, + indexManager: config.IndexManager, + minRetargetTimespan: targetTimespan / adjustmentFactor, + maxRetargetTimespan: targetTimespan * adjustmentFactor, + blocksPerRetarget: int32(targetTimespan / targetTimePerBlock), + Index: newBlockIndex(config.DB, params), + hashCache: config.HashCache, + BestChain: newChainView(nil), + orphans: make(map[chainhash.Hash]*orphanBlock), + prevOrphans: make(map[chainhash.Hash][]*orphanBlock), + // warningCaches: newThresholdCaches(vbNumBits), + // deploymentCaches: newThresholdCaches(chaincfg.DefinedDeployments), + DifficultyAdjustments: make(map[string]float64), + } + b.DifficultyBits.Store(make(Diffs)) + // Initialize the chain state from the passed database. When the db does not yet contain any chain state, both it + // and the chain state will be initialized to contain only the genesis block. + if e := b.initChainState(); E.Chk(e) { + return nil, e + } + // Perform any upgrades to the various chain-specific buckets as needed. + if e := b.maybeUpgradeDbBuckets(config.Interrupt); E.Chk(e) { + return nil, e + } + // Initialize and catch up all of the currently active optional indexes as needed. + if config.IndexManager != nil { + e := config.IndexManager.Init(&b, config.Interrupt) + if e != nil { + return nil, e + } + } + // // Initialize rule change threshold state caches. + // if e := b.initThresholdCaches(); E.Chk(e) { + // return nil, e + // } + bestNode := b.BestChain.Tip() + df, ok := bestNode.Diffs.Load().(Diffs) + if df == nil || !ok || + len(df) != len(fork.List[1].AlgoVers) { + bitsMap, e := b.CalcNextRequiredDifficultyPlan9Controller(bestNode) + if e != nil { + } + bestNode.Diffs.Store(bitsMap) + } + I.F( + "chain state (height %d, hash %v, totaltx %d, work %v)", + bestNode.height, bestNode.hash, b.stateSnapshot.TotalTxns, + bestNode.workSum, + ) + return &b, nil +} diff --git a/pkg/blockchain/chain_test.go b/pkg/blockchain/chain_test.go new file mode 100644 index 0000000..1d77c54 --- /dev/null +++ b/pkg/blockchain/chain_test.go @@ -0,0 +1,875 @@ +package blockchain + +import ( + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" + "reflect" + "testing" +) + +// // TestHaveBlock tests the HaveBlock API to ensure proper functionality. +// func TestHaveBlock(t *testing.T) { +// // Load up blocks such that there is a side chain. +// // (genesis block) -> 1 -> 2 -> 3 -> 4 +// // \-> 3a +// testFiles := []string{ +// "blk_0_to_4.dat.bz2", +// "blk_3A.dat.bz2", +// } +// var blocks []*util.Block +// for _, file := range testFiles { +// blockTmp, e := loadBlocks(file) +// if e != nil { +// t.Errorf("Error loading file: %v\n", e) +// return +// } +// blocks = append(blocks, blockTmp...) +// } +// // Create a new database and chain instance to run tests against. +// chain, teardownFunc, e := chainSetup("haveblock", +// &chaincfg.MainNetParams) +// if e != nil { +// t.Errorf("Failed to setup chain instance: %v", e) +// return +// } +// defer teardownFunc() +// // Since we're not dealing with the real block chain, set the coinbase maturity to 1. +// chain.TstSetCoinbaseMaturity(1) +// for i := 1; i < len(blocks); i++ { +// _, isOrphan, e := chain.ProcessBlock(blocks[i], BFNone, blocks[i].Height()) +// if e != nil { +// t.Errorf("ProcessBlock fail on block %v: %v\n", i, e) +// return +// } +// if isOrphan { +// t.Errorf("ProcessBlock incorrectly returned block %v "+ +// "is an orphan\n", i) +// return +// } +// } +// // Insert an orphan block. +// _, isOrphan, e := chain.ProcessBlock(util.NewBlock(&Block100000), +// BFNone, 100000) +// if e != nil { +// t.Errorf("Unable to process block: %v", e) +// return +// } +// if !isOrphan { +// t.Errorf("ProcessBlock indicated block is an not orphan when " + +// "it should be\n") +// return +// } +// tests := []struct { +// hash string +// want bool +// }{ +// // Genesis block should be present (in the main chain). +// {hash: chaincfg.MainNetParams.GenesisHash.String(), want: true}, +// // Block 3a should be present (on a side chain). +// {hash: "00000000474284d20067a4d33f6a02284e6ef70764a3a26d6a5b9df52ef663dd", want: true}, +// // Block 100000 should be present (as an orphan). +// {hash: "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", want: true}, +// // Random hashes should not be available. +// {hash: "123", want: false}, +// } +// for i, test := range tests { +// hash, e := chainhash.NewHashFromStr(test.hash) +// if e != nil { +// t.Errorf("NewHashFromStr: %v", e) +// continue +// } +// result, e := chain.HaveBlock(hash) +// if e != nil { +// t.Errorf("HaveBlock #%d unexpected error: %v", i, e) +// return +// } +// if result != test.want { +// t.Errorf("HaveBlock #%d got %v want %v", i, result, +// test.want) +// continue +// } +// } +// } + +// // TestCalcSequenceLock tests the LockTimeToSequence function, and the CalcSequenceLock method of a Chain instance. +// // The tests exercise several combinations of inputs to the CalcSequenceLock function in order to ensure the returned +// // SequenceLocks are correct for each test instance. +// func TestCalcSequenceLock(t *testing.T) { +// netParams := &chaincfg.SimNetParams +// // We need to activate CSV in order to test the processing logic, so manually craft the block version that's used to +// // signal the soft-fork activation. +// csvBit := netParams.Deployments[chaincfg.DeploymentCSV].BitNumber +// blockVersion := int32(0x20000000 | (uint32(1) << csvBit)) +// // Generate enough synthetic blocks to activate CSV. +// chain := newFakeChain(netParams) +// node := chain.BestChain.Tip() +// blockTime := node.Header().Timestamp +// numBlocksToActivate := netParams.MinerConfirmationWindow * 3 +// for i := uint32(0); i < numBlocksToActivate; i++ { +// blockTime = blockTime.Add(time.Second) +// node = newFakeNode(node, blockVersion, 0, blockTime) +// chain.Index.AddNode(node) +// chain.BestChain.SetTip(node) +// } +// // Create a utxo view with a fake utxo for the inputs used in the transactions created below. This utxo is added +// // such that it has an age of 4 blocks. +// targetTx := util.NewTx(&wire.MsgTx{ +// TxOut: []*wire.TxOut{{ +// PkScript: nil, +// Value: 10, +// }}, +// }) +// utxoView := NewUtxoViewpoint() +// utxoView.AddTxOuts(targetTx, int32(numBlocksToActivate)-4) +// utxoView.SetBestHash(&node.hash) +// // Create a utxo that spends the fake utxo created above for use in the transactions created in the tests. It has an +// // age of 4 blocks. Note that the sequence lock heights are always calculated from the same point of view that they +// // were originally calculated from for a given utxo. That is to say, the height prior to it. +// utxo := wire.OutPoint{ +// Hash: *targetTx.Hash(), +// Index: 0, +// } +// prevUtxoHeight := int32(numBlocksToActivate) - 4 +// // Obtain the median time past from the PoV of the input created above. The MTP for the input is the MTP from the +// // PoV of the block *prior* to the one that included it. +// medianTime := node.RelativeAncestor(5).CalcPastMedianTime().Unix() +// // The median time calculated from the PoV of the best block in the test chain. For unconfirmed inputs, this value +// // will be used since the MTP will be calculated from the PoV of the yet-to-be-mined block. +// nextMedianTime := node.CalcPastMedianTime().Unix() +// nextBlockHeight := int32(numBlocksToActivate) + 1 +// // Add an additional transaction which will serve as our unconfirmed output. +// unConfTx := &wire.MsgTx{ +// TxOut: []*wire.TxOut{{ +// PkScript: nil, +// Value: 5, +// }}, +// } +// unConfUtxo := wire.OutPoint{ +// Hash: unConfTx.TxHash(), +// Index: 0, +// } +// // Adding a utxo with a height of 0x7fffffff indicates that the output is currently unmined. +// utxoView.AddTxOuts(util.NewTx(unConfTx), 0x7fffffff) +// tests := []struct { +// tx *wire.MsgTx +// view *UtxoViewpoint +// mempool bool +// want *SequenceLock +// }{ +// // A transaction of version one should disable sequence locks as the new sequence number semantics only apply to +// // transactions version 2 or higher. +// { +// tx: &wire.MsgTx{ +// Version: 1, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 3), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: -1, +// BlockHeight: -1, +// }, +// }, +// // A transaction with a single input with max sequence number. This sequence number has the high bit set, so +// // sequence locks should be disabled. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: wire.MaxTxInSequenceNum, +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: -1, +// BlockHeight: -1, +// }, +// }, +// // A transaction with a single input whose lock time is expressed in seconds. However, the specified lock time +// // is below the required floor for time based lock times since they have time granularity of 512 seconds. As a +// // result, the seconds lock-time should be just before the median time of the targeted block. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 2), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: medianTime - 1, +// BlockHeight: -1, +// }, +// }, +// // A transaction with a single input whose lock time is expressed in seconds. The number of seconds should be +// // 1023 seconds after the median past time of the last block in the chain. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 1024), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: medianTime + 1023, +// BlockHeight: -1, +// }, +// }, +// // A transaction with multiple inputs. The first input has a lock time expressed in seconds. The second input +// // has a sequence lock in blocks with a value of 4. The last input has a sequence number with a value of 5, but +// // has the disable bit set. So the first lock should be selected as it's the latest lock that isn't disabled. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 2560), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 4), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 5) | +// wire.SequenceLockTimeDisabled, +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: medianTime + (5 << wire.SequenceLockTimeGranularity) - 1, +// BlockHeight: prevUtxoHeight + 3, +// }, +// }, +// // Transaction with a single input. The input's sequence number encodes a relative lock-time in blocks (3 +// // blocks). The sequence lock should have a value of -1 for seconds, but a height of 2 meaning it can be +// // included at height 3. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 3), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: -1, +// BlockHeight: prevUtxoHeight + 2, +// }, +// }, +// // A transaction with two inputs with lock times expressed in seconds. The selected sequence lock value for +// // seconds should be the time further in the future. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 5120), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 2560), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: medianTime + (10 << wire.SequenceLockTimeGranularity) - 1, +// BlockHeight: -1, +// }, +// }, +// // A transaction with two inputs with lock times expressed in blocks. The selected sequence lock value for +// // blocks should be the height further in the future, so a height of 10 indicating it can be included at height +// // 11. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 1), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 11), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: -1, +// BlockHeight: prevUtxoHeight + 10, +// }, +// }, +// // A transaction with multiple inputs. Two inputs are time based, and the other two are block based. The lock +// // lying further into the future for both inputs should be chosen. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 2560), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(true, 6656), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 3), +// }, +// { +// PreviousOutPoint: utxo, +// Sequence: LockTimeToSequence(false, 9), +// }}, +// }, +// view: utxoView, +// want: &SequenceLock{ +// Seconds: medianTime + (13 << wire.SequenceLockTimeGranularity) - 1, +// BlockHeight: prevUtxoHeight + 8, +// }, +// }, +// // A transaction with a single unconfirmed input. As the input is confirmed, the height of the input should be +// // interpreted as the height of the *next* block. So, a 2 block relative lock means the sequence lock should be +// // for 1 block after the *next* block height, indicating it can be included 2 blocks after that. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: unConfUtxo, +// Sequence: LockTimeToSequence(false, 2), +// }}, +// }, +// view: utxoView, +// mempool: true, +// want: &SequenceLock{ +// Seconds: -1, +// BlockHeight: nextBlockHeight + 1, +// }, +// }, +// // A transaction with a single unconfirmed input. The input has a time based lock, so the lock time should be +// // based off the MTP of the *next* block. +// { +// tx: &wire.MsgTx{ +// Version: 2, +// TxIn: []*wire.TxIn{{ +// PreviousOutPoint: unConfUtxo, +// Sequence: LockTimeToSequence(true, 1024), +// }}, +// }, +// view: utxoView, +// mempool: true, +// want: &SequenceLock{ +// Seconds: nextMedianTime + 1023, +// BlockHeight: -1, +// }, +// }, +// } +// t.Logf("Running %v SequenceLock tests", len(tests)) +// for i, test := range tests { +// utilTx := util.NewTx(test.tx) +// seqLock, e := chain.CalcSequenceLock(utilTx, test.view, test.mempool) +// if e != nil { +// t.Fatalf("test #%d, unable to calc sequence lock: %v", i, e) +// } +// if seqLock.Seconds != test.want.Seconds { +// t.Fatalf("test #%d got %v seconds want %v seconds", +// i, seqLock.Seconds, test.want.Seconds) +// } +// if seqLock.BlockHeight != test.want.BlockHeight { +// t.Fatalf("test #%d got height of %v want height of %v ", +// i, seqLock.BlockHeight, test.want.BlockHeight) +// } +// } +// } + +// nodeHashes is a convenience function that returns the hashes for all of the passed indexes of the provided nodes. It +// is used to construct expected hash slices in the tests. +func nodeHashes(nodes []*BlockNode, indexes ...int) []chainhash.Hash { + hashes := make([]chainhash.Hash, 0, len(indexes)) + for _, idx := range indexes { + hashes = append(hashes, nodes[idx].hash) + } + return hashes +} + +// nodeHeaders is a convenience function that returns the headers for all of the passed indexes of the provided nodes. +// It is used to construct expected located headers in the tests. +func nodeHeaders(nodes []*BlockNode, indexes ...int) []wire.BlockHeader { + headers := make([]wire.BlockHeader, 0, len(indexes)) + for _, idx := range indexes { + headers = append(headers, nodes[idx].Header()) + } + return headers +} + +// TestLocateInventory ensures that locating inventory via the LocateHeaders and LocateBlocks functions behaves as +// expected. +func TestLocateInventory(t *testing.T) { + // Construct a synthetic block chain with a block index consisting of the following structure. + // + // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 + // \-> 16a -> 17a + tip := tstTip + chain := newFakeChain(&chaincfg.MainNetParams) + branch0Nodes := chainedNodes(chain.BestChain.Genesis(), 18) + branch1Nodes := chainedNodes(branch0Nodes[14], 2) + for _, node := range branch0Nodes { + chain.Index.AddNode(node) + } + for _, node := range branch1Nodes { + chain.Index.AddNode(node) + } + chain.BestChain.SetTip(tip(branch0Nodes)) + // Create chain views for different branches of the overall chain to simulate a local and remote node on different + // parts of the chain. + localView := newChainView(tip(branch0Nodes)) + remoteView := newChainView(tip(branch1Nodes)) + // Create a chain view for a completely unrelated block chain to simulate a remote node on a totally different + // chain. + unrelatedBranchNodes := chainedNodes(nil, 5) + unrelatedView := newChainView(tip(unrelatedBranchNodes)) + tests := []struct { + name string + // locator for requested inventory + locator BlockLocator + // stop hash for locator + hashStop chainhash.Hash + // max to locate, 0 = wire const + maxAllowed uint32 + // expected located headers + headers []wire.BlockHeader + // expected located hashes + hashes []chainhash.Hash + }{ + { + // Empty block locators and unknown stop hash. No inventory should be located. + name: "no locators, no stop", + locator: nil, + hashStop: chainhash.Hash{}, + headers: nil, + hashes: nil, + }, + { + // Empty block locators and stop hash in side chain. The expected result is the requested block. + name: "no locators, stop in side", + locator: nil, + hashStop: tip(branch1Nodes).hash, + headers: nodeHeaders(branch1Nodes, 1), + hashes: nodeHashes(branch1Nodes, 1), + }, + { + // Empty block locators and stop hash in main chain. The expected result is the requested block. + name: "no locators, stop in main", + locator: nil, + hashStop: branch0Nodes[12].hash, + headers: nodeHeaders(branch0Nodes, 12), + hashes: nodeHashes(branch0Nodes, 12), + }, + { + // Locators based on remote being on side chain and a stop hash local node doesn't know about. The expected + // result is the blocks after the fork point in the main chain and the stop hash has no effect. + name: "remote side chain, unknown stop", + locator: remoteView.BlockLocator(nil), + hashStop: chainhash.Hash{0x01}, + headers: nodeHeaders(branch0Nodes, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 15, 16, 17), + }, + { + // Locators based on remote being on side chain and a stop hash in side chain. The expected result is the + // blocks after the fork point in the main chain and the stop hash has no effect. + name: "remote side chain, stop in side", + locator: remoteView.BlockLocator(nil), + hashStop: tip(branch1Nodes).hash, + headers: nodeHeaders(branch0Nodes, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 15, 16, 17), + }, + { + // Locators based on remote being on side chain and a stop hash in main chain, but before fork point. The + // expected result is the blocks after the fork point in the main chain and the stop hash has no effect. + name: "remote side chain, stop in main before", + locator: remoteView.BlockLocator(nil), + hashStop: branch0Nodes[13].hash, + headers: nodeHeaders(branch0Nodes, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 15, 16, 17), + }, + { + // Locators based on remote being on side chain and a stop hash in main chain, but exactly at the fork + // point. The expected result is the blocks after the fork point in the main chain and the stop hash has no + // effect. + name: "remote side chain, stop in main exact", + locator: remoteView.BlockLocator(nil), + hashStop: branch0Nodes[14].hash, + headers: nodeHeaders(branch0Nodes, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 15, 16, 17), + }, + { + // Locators based on remote being on side chain and a stop hash in main chain just after the fork point. The + // expected result is the blocks after the fork point in the main chain up to and including the stop hash. + name: "remote side chain, stop in main after", + locator: remoteView.BlockLocator(nil), + hashStop: branch0Nodes[15].hash, + headers: nodeHeaders(branch0Nodes, 15), + hashes: nodeHashes(branch0Nodes, 15), + }, + { + // Locators based on remote being on side chain and a stop hash in main chain some time after the fork + // point. The expected result is the blocks after the fork point in the main chain up to and including the + // stop hash. + name: "remote side chain, stop in main after more", + locator: remoteView.BlockLocator(nil), + hashStop: branch0Nodes[16].hash, + headers: nodeHeaders(branch0Nodes, 15, 16), + hashes: nodeHashes(branch0Nodes, 15, 16), + }, + { + // Locators based on remote being on main chain in the past and a stop hash local node doesn't know about. + // The expected result is the blocks after the known point in the main chain and the stop hash has no + // effect. + name: "remote main chain past, unknown stop", + locator: localView.BlockLocator(branch0Nodes[12]), + hashStop: chainhash.Hash{0x01}, + headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), + }, + { + // Locators based on remote being on main chain in the past and a stop hash in a side chain. The expected + // result is the blocks after the known point in the main chain and the stop hash has no effect. + name: "remote main chain past, stop in side", + locator: localView.BlockLocator(branch0Nodes[12]), + hashStop: tip(branch1Nodes).hash, + headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), + }, + { + // Locators based on remote being on main chain in the past and a stop hash in the main chain before that + // point. The expected result is the blocks after the known point in the main chain and the stop hash has no + // effect. + name: "remote main chain past, stop in main before", + locator: localView.BlockLocator(branch0Nodes[12]), + hashStop: branch0Nodes[11].hash, + headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), + }, + { + // Locators based on remote being on main chain in the past and a stop hash in the main chain exactly at + // that point. The expected result is the blocks after the known point in the main chain and the stop hash + // has no effect. + name: "remote main chain past, stop in main exact", + locator: localView.BlockLocator(branch0Nodes[12]), + hashStop: branch0Nodes[12].hash, + headers: nodeHeaders(branch0Nodes, 13, 14, 15, 16, 17), + hashes: nodeHashes(branch0Nodes, 13, 14, 15, 16, 17), + }, + { + // Locators based on remote being on main chain in the past and a stop hash in the main chain just after + // that point. The expected result is the blocks after the known point in the main chain and the stop hash + // has no effect. + name: "remote main chain past, stop in main after", + locator: localView.BlockLocator(branch0Nodes[12]), + hashStop: branch0Nodes[13].hash, + headers: nodeHeaders(branch0Nodes, 13), + hashes: nodeHashes(branch0Nodes, 13), + }, + { + // Locators based on remote being on main chain in the past and a stop hash in the main chain some time + // after that point. The expected result is the blocks after the known point in the main chain and the stop + // hash has no effect. + name: "remote main chain past, stop in main after more", + locator: localView.BlockLocator(branch0Nodes[12]), + hashStop: branch0Nodes[15].hash, + headers: nodeHeaders(branch0Nodes, 13, 14, 15), + hashes: nodeHashes(branch0Nodes, 13, 14, 15), + }, + { + // Locators based on remote being at exactly the same point in the main chain and a stop hash local node + // doesn't know about. The expected result is no located inventory. + name: "remote main chain same, unknown stop", + locator: localView.BlockLocator(nil), + hashStop: chainhash.Hash{0x01}, + headers: nil, + hashes: nil, + }, + { + // Locators based on remote being at exactly the same point in the main chain and a stop hash at exactly the + // same point. The expected result is no located inventory. + name: "remote main chain same, stop same point", + locator: localView.BlockLocator(nil), + hashStop: tip(branch0Nodes).hash, + headers: nil, + hashes: nil, + }, + { + // Locators from remote that don't include any blocks the local node knows. This would happen if the remote + // node is on a completely separate chain that isn't rooted with the same genesis block. The expected result + // is the blocks after the genesis block. + name: "remote unrelated chain", + locator: unrelatedView.BlockLocator(nil), + hashStop: chainhash.Hash{}, + headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ), + hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ), + }, + { + // Locators from remote for second block in main chain and no stop hash, but with an overridden max limit. + // The expected result is the blocks after the second block limited by the max. + name: "remote genesis", + locator: locatorHashes(branch0Nodes, 0), + hashStop: chainhash.Hash{}, + maxAllowed: 3, + headers: nodeHeaders(branch0Nodes, 1, 2, 3), + hashes: nodeHashes(branch0Nodes, 1, 2, 3), + }, + { + // Poorly formed locator. Locator from remote that only includes a single block on a side chain the local + // node knows. The expected result is the blocks after the genesis block since even though the block is + // known, it is on a side chain and there are no more locators to find the fork point. + name: "weak locator, single known side block", + locator: locatorHashes(branch1Nodes, 1), + hashStop: chainhash.Hash{}, + headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ), + hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ), + }, + { + // Poorly formed locator. Locator from remote that only includes multiple blocks on a side chain the local + // node knows however none in the main chain. The expected result is the blocks after the genesis block + // since even though the blocks are known, they are all on a side chain and there are no more locators to + // find the fork point. + name: "weak locator, multiple known side blocks", + locator: locatorHashes(branch1Nodes, 1), + hashStop: chainhash.Hash{}, + headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ), + hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + ), + }, + { + // Poorly formed locator. Locator from remote that only includes multiple blocks on a side chain the local + // node knows however none in the main chain but includes a stop hash in the main chain. The expected result + // is the blocks after the genesis block up to the stop hash since even though the blocks are known, they + // are all on a side chain and there are no more locators to find the fork point. + name: "weak locator, multiple known side blocks, stop in main", + locator: locatorHashes(branch1Nodes, 1), + hashStop: branch0Nodes[5].hash, + headers: nodeHeaders(branch0Nodes, 0, 1, 2, 3, 4, 5), + hashes: nodeHashes(branch0Nodes, 0, 1, 2, 3, 4, 5), + }, + } + for _, test := range tests { + // Ensure the expected headers are located. + var headers []wire.BlockHeader + if test.maxAllowed != 0 { + // Need to use the unexported function to override the max allowed for headers. + chain.ChainLock.RLock() + headers = chain.locateHeaders(test.locator, + &test.hashStop, test.maxAllowed, + ) + chain.ChainLock.RUnlock() + } else { + headers = chain.LocateHeaders(test.locator, + &test.hashStop, + ) + } + if !reflect.DeepEqual(headers, test.headers) { + t.Errorf("%s: unxpected headers -- got %v, want %v", + test.name, headers, test.headers, + ) + continue + } + // Ensure the expected block hashes are located. + maxAllowed := uint32(wire.MaxBlocksPerMsg) + if test.maxAllowed != 0 { + maxAllowed = test.maxAllowed + } + hashes := chain.LocateBlocks(test.locator, &test.hashStop, + maxAllowed, + ) + if !reflect.DeepEqual(hashes, test.hashes) { + t.Errorf("%s: unxpected hashes -- got %v, want %v", + test.name, hashes, test.hashes, + ) + continue + } + } +} + +// TestHeightToHashRange ensures that fetching a range of block hashes by start height and end hash works as expected. +func TestHeightToHashRange(t *testing.T) { + // Construct a synthetic block chain with a block index consisting of the following structure. + // + // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 + // \-> 16a -> 17a -> 18a (unvalidated) + tip := tstTip + chain := newFakeChain(&chaincfg.MainNetParams) + branch0Nodes := chainedNodes(chain.BestChain.Genesis(), 18) + branch1Nodes := chainedNodes(branch0Nodes[14], 3) + for _, node := range branch0Nodes { + chain.Index.SetStatusFlags(node, statusValid) + chain.Index.AddNode(node) + } + for _, node := range branch1Nodes { + if node.height < 18 { + chain.Index.SetStatusFlags(node, statusValid) + } + chain.Index.AddNode(node) + } + chain.BestChain.SetTip(tip(branch0Nodes)) + tests := []struct { + name string + // locator for requested inventory + startHeight int32 + // stop hash for locator + endHash chainhash.Hash + // max to locate, 0 = wire const + maxResults int + // expected located hashes + hashes []chainhash.Hash + expectError bool + }{ + { + name: "blocks below tip", + startHeight: 11, + endHash: branch0Nodes[14].hash, + maxResults: 10, + hashes: nodeHashes(branch0Nodes, 10, 11, 12, 13, 14), + }, + { + name: "blocks on main chain", + startHeight: 15, + endHash: branch0Nodes[17].hash, + maxResults: 10, + hashes: nodeHashes(branch0Nodes, 14, 15, 16, 17), + }, + { + name: "blocks on stale chain", + startHeight: 15, + endHash: branch1Nodes[1].hash, + maxResults: 10, + hashes: append(nodeHashes(branch0Nodes, 14), + nodeHashes(branch1Nodes, 0, 1)..., + ), + }, + { + name: "invalid start height", + startHeight: 19, + endHash: branch0Nodes[17].hash, + maxResults: 10, + expectError: true, + }, + { + name: "too many results", + startHeight: 1, + endHash: branch0Nodes[17].hash, + maxResults: 10, + expectError: true, + }, + { + name: "unvalidated block", + startHeight: 15, + endHash: branch1Nodes[2].hash, + maxResults: 10, + expectError: true, + }, + } + for _, test := range tests { + hashes, e := chain.HeightToHashRange(test.startHeight, &test.endHash, + test.maxResults, + ) + if e != nil { + if !test.expectError { + t.Errorf("%s: unexpected error: %v", test.name, e) + } + continue + } + if !reflect.DeepEqual(hashes, test.hashes) { + t.Errorf("%s: unxpected hashes -- got %v, want %v", + test.name, hashes, test.hashes, + ) + } + } +} + +// TestIntervalBlockHashes ensures that fetching block hashes at specified intervals by end hash works as expected. +func TestIntervalBlockHashes(t *testing.T) { + // Construct a synthetic block chain with a block index consisting of the following structure. + // + // genesis -> 1 -> 2 -> ... -> 15 -> 16 -> 17 -> 18 + // \-> 16a -> 17a -> 18a (unvalidated) + tip := tstTip + chain := newFakeChain(&chaincfg.MainNetParams) + branch0Nodes := chainedNodes(chain.BestChain.Genesis(), 18) + branch1Nodes := chainedNodes(branch0Nodes[14], 3) + for _, node := range branch0Nodes { + chain.Index.SetStatusFlags(node, statusValid) + chain.Index.AddNode(node) + } + for _, node := range branch1Nodes { + if node.height < 18 { + chain.Index.SetStatusFlags(node, statusValid) + } + chain.Index.AddNode(node) + } + chain.BestChain.SetTip(tip(branch0Nodes)) + tests := []struct { + name string + endHash chainhash.Hash + interval int + hashes []chainhash.Hash + expectError bool + }{ + { + name: "blocks on main chain", + endHash: branch0Nodes[17].hash, + interval: 8, + hashes: nodeHashes(branch0Nodes, 7, 15), + }, + { + name: "blocks on stale chain", + endHash: branch1Nodes[1].hash, + interval: 8, + hashes: append(nodeHashes(branch0Nodes, 7), + nodeHashes(branch1Nodes, 0)..., + ), + }, + { + name: "no results", + endHash: branch0Nodes[17].hash, + interval: 20, + hashes: []chainhash.Hash{}, + }, + { + name: "unvalidated block", + endHash: branch1Nodes[2].hash, + interval: 8, + expectError: true, + }, + } + for _, test := range tests { + hashes, e := chain.IntervalBlockHashes(&test.endHash, test.interval) + if e != nil { + if !test.expectError { + t.Errorf("%s: unexpected error: %v", test.name, e) + } + continue + } + if !reflect.DeepEqual(hashes, test.hashes) { + t.Errorf("%s: unxpected hashes -- got %v, want %v", + test.name, hashes, test.hashes, + ) + } + } +} diff --git a/pkg/blockchain/chainio.go b/pkg/blockchain/chainio.go new file mode 100644 index 0000000..a105374 --- /dev/null +++ b/pkg/blockchain/chainio.go @@ -0,0 +1,1256 @@ +package blockchain + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/p9c/p9/pkg/block" + "math/big" + "sync" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // blockHdrSize is the size of a block header. This is simply the constant from wire and is only provided here for + // convenience since wire.MaxBlockHeaderPayload is quite long. + blockHdrSize = wire.MaxBlockHeaderPayload + // latestUtxoSetBucketVersion is the current version of the utxo set bucket that is used to track all unspent + // outputs. + latestUtxoSetBucketVersion = 2 + // latestSpendJournalBucketVersion is the current version of the spend journal bucket that is used to track all + // spent transactions for use in reorgs. + latestSpendJournalBucketVersion = 1 +) + +var ( + // blockIndexBucketName is the name of the db bucket used to house to the block headers and contextual information. + blockIndexBucketName = []byte("blockheaderidx") + // hashIndexBucketName is the name of the db bucket used to house to the block hash -> block height index. + hashIndexBucketName = []byte("hashidx") + // heightIndexBucketName is the name of the db bucket used to house to the block height -> block hash index. + heightIndexBucketName = []byte("heightidx") + // chainStateKeyName is the name of the db key used to store the best chain state. + chainStateKeyName = []byte("chainstate") + // spendJournalVersionKeyName is the name of the db key used to store the version of the spend journal currently in + // the database. + spendJournalVersionKeyName = []byte("spendjournalversion") + // spendJournalBucketName is the name of the db bucket used to house transactions outputs that are spent in each + // block. + spendJournalBucketName = []byte("spendjournal") + // utxoSetVersionKeyName is the name of the db key used to store the version of the utxo set currently in the + // database. + utxoSetVersionKeyName = []byte("utxosetversion") + // utxoSetBucketName is the name of the db bucket used to house the unspent transaction output set. + utxoSetBucketName = []byte("utxosetv2") + // byteOrder is the preferred byte order used for serializing numeric fields for storage in the database. + byteOrder = binary.LittleEndian +) + +// errNotInMainChain signifies that a block hash or height that is not in the main chain was requested. +type errNotInMainChain string + +// DBError implements the error interface. +func (e errNotInMainChain) Error() string { + return string(e) +} + +// isNotInMainChainErr returns whether or not the passed error is an errNotInMainChain error. +func isNotInMainChainErr(e error) bool { + _, ok := e.(errNotInMainChain) + return ok +} + +// errDeserialize signifies that a problem was encountered when +// deserializing data. +type errDeserialize string + +// DBError is an implementation of the errors.* interface +func (e errDeserialize) Error() string { + return string(e) +} + +// isDeserializeErr returns whether or not the passed error is an errDeserialize error. +func isDeserializeErr(e error) bool { + _, ok := e.(errDeserialize) + return ok +} + +// // isDbBucketNotFoundErr returns whether or not the passed error is a +// database.DBError with an error code of database.ErrBucketNotFound. +// func isDbBucketNotFoundErr(// e error) bool { +// dbErr, ok := err.(database.DBError) +// return ok && dbErr.ErrorCode == database.ErrBucketNotFound +// } + +// dbFetchVersion fetches an individual version with the given key from the metadata bucket. It is primarily used to +// track versions on entities such as buckets. It returns zero if the provided key does not exist. +func dbFetchVersion(dbTx database.Tx, key []byte) uint32 { + serialized := dbTx.Metadata().Get(key) + if serialized == nil { + return 0 + } + return byteOrder.Uint32(serialized[:]) +} + +// dbPutVersion uses an existing database transaction to update the provided key in the metadata bucket to the given +// version. It is primarily used to track versions on entities such as buckets. +func dbPutVersion(dbTx database.Tx, key []byte, version uint32) (e error) { + var serialized [4]byte + byteOrder.PutUint32(serialized[:], version) + return dbTx.Metadata().Put(key, serialized[:]) +} + +// dbFetchOrCreateVersion uses an existing database transaction to attempt to fetch the provided key from the metadata +// bucket as a version and in the case it doesn't exist it adds the entry with the provided default version and returns +// that. +// +// This is useful during upgrades to automatically handle loading and adding version keys as necessary. +func dbFetchOrCreateVersion(dbTx database.Tx, key []byte, defaultVersion uint32) (uint32, error) { + version := dbFetchVersion(dbTx, key) + if version == 0 { + version = defaultVersion + e := dbPutVersion(dbTx, key, version) + if e != nil { + return 0, e + } + } + return version, nil +} + +// The transaction spend journal consists of an entry for each block +// connected to the main chain which contains the transaction outputs the block spends serialized such that the order is the reverse of the order they were spent. This is required because reorganizing the chain necessarily entails disconnecting blocks to get back to the point of the fork which implies unspending all of the transaction outputs that each block previously spent. +// Since the utxo set, by definition, +// only contains unspent transaction outputs, the spent transaction outputs must be resurrected from somewhere. There is more than one way this could be done, however this is the most straight forward method that does not require having a transaction index and unpruned +// blockchain. +// NOTE: This format is NOT self describing. +// The additional details such as the number of entries (transaction inputs) are expected to come from the block itself and the utxo set (for legacy entries). The rationale in doing this is to save space. This is also the reason the spent outputs are serialized in the reverse order they are spent because later transactions are allowed to spend outputs from earlier ones in the same block. +// The reserved field below used to keep track of the version of the +// containing transaction when the height in the header code was non-zero, however the height is always non-zero now, but keeping the extra reserved field allows backwards compatibility. +// The serialized format is: +// [
],... +// Field Type Size +// header code VLQ variable +// reserved byte 1 +// compressed txout +// compressed amount VLQ variable +// compressed script []byte variable +// The serialized header code format is: +// bit 0 - containing transaction is a coinbase +// bits 1-x - height of the block that contains the spent txout +// Example 1: +// From block 170 in main blockchain. +// 1300320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c +// <><><------------------------------------------------------------------> +// | | | +// | reserved compressed txout +// header code +// - header code: 0x13 (coinbase, height 9) +// - reserved: 0x00 +// - compressed txout 0: +// - 0x32: VLQ-encoded compressed amount for 5000000000 (50 DUO) +// - 0x05: special script type pay-to-pubkey +// - 0x11...5c: x-coordinate of the pubkey +// Example 2: +// Adapted from block 100025 in main blockchain. +// 8b99700091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e868b99700086c64700b2fb57eadf61e106a100a7445a8c3f67898841ec +// <----><><----------------------------------------------><----><><----------------------------------------------> +// | | | | | | +// | reserved compressed txout | reserved +// compressed txout +// header code header code +// - Last spent output: +// - header code: 0x8b9970 (not coinbase, height 100024) +// - reserved: 0x00 +// - compressed txout: +// - 0x91f20f: VLQ-encoded compressed amount for 34405000000 (344.05 DUO) +// - 0x00: special script type pay-to-pubkey-hash +// - 0x6e...86: pubkey hash +// - Second to last spent output: +// - header code: 0x8b9970 (not coinbase, height 100024) +// - reserved: 0x00 +// - compressed txout: +// - 0x86c647: VLQ-encoded compressed amount for 13761000000 (137.61 DUO) +// - 0x00: special script type pay-to-pubkey-hash +// - 0xb2...ec: pubkey hash +// ----------------------------------------------------------------------------- + +// SpentTxOut contains a spent transaction output and potentially additional contextual information such as whether or +// not it was contained +// +// in a coinbase transaction, the version of the transaction it was contained in, and which block height the containing +// transaction was included in. +// +// As described in the comments above, the additional contextual information will only be valid when this spent txout is +// spending the last unspent output of the containing transaction. +type SpentTxOut struct { + // Amount is the amount of the output. + Amount int64 + // PkScipt is the the public key script for the output. + PkScript []byte + // Height is the height of the the block containing the creating tx. + Height int32 + // Denotes if the creating tx is a coinbase. + IsCoinBase bool +} + +// FetchSpendJournal attempts to retrieve the spend journal, or the set of outputs spent for the target block. +// +// This provides a view of all the outputs that will be consumed once the target block is connected to the end of the +// main chain. +// +// This function is safe for concurrent access. +func (b *BlockChain) FetchSpendJournal(targetBlock *block.Block) ([]SpentTxOut, error) { + b.ChainLock.RLock() + defer b.ChainLock.RUnlock() + var spendEntries []SpentTxOut + e := b.db.View( + func(dbTx database.Tx) (e error) { + spendEntries, e = dbFetchSpendJournalEntry(dbTx, targetBlock) + return e + }, + ) + if e != nil { + return nil, e + } + return spendEntries, nil +} + +// spentTxOutHeaderCode returns the calculated header code to be used when serializing the provided stxo entry. +func spentTxOutHeaderCode(stxo *SpentTxOut) uint64 { + // As described in the serialization format comments, the header code encodes the height shifted over one bit and + // the coinbase flag in the lowest bit. + headerCode := uint64(stxo.Height) << 1 + if stxo.IsCoinBase { + headerCode |= 0x01 + } + return headerCode +} + +// spentTxOutSerializeSize returns the number of bytes it would take to serialize the passed stxo according to the +// format described above. +func spentTxOutSerializeSize(stxo *SpentTxOut) int { + size := serializeSizeVLQ(spentTxOutHeaderCode(stxo)) + if stxo.Height > 0 { + // The legacy v1 spend journal format conditionally tracked the + // containing transaction version when the height was non-zero, + // so this is required for backwards compat. + size += serializeSizeVLQ(0) + } + return size + compressedTxOutSize(uint64(stxo.Amount), stxo.PkScript) +} + +// putSpentTxOut serializes the passed stxo according to the format described above directly into the passed target byte +// slice. +// +// The target byte slice must be at least large enough to handle the number of bytes returned by the +// SpentTxOutSerializeSize function or it will panic. +func putSpentTxOut(target []byte, stxo *SpentTxOut) int { + headerCode := spentTxOutHeaderCode(stxo) + offset := putVLQ(target, headerCode) + if stxo.Height > 0 { + // The legacy v1 spend journal format conditionally tracked the containing transaction version when the height + // was non-zero, so this is required for backwards compat. + offset += putVLQ(target[offset:], 0) + } + return offset + putCompressedTxOut( + target[offset:], uint64(stxo.Amount), + stxo.PkScript, + ) +} + +// decodeSpentTxOut decodes the passed serialized stxo entry, possibly followed by other data, into the passed stxo +// struct. It returns the number of bytes read. +func decodeSpentTxOut(serialized []byte, stxo *SpentTxOut) (int, error) { + // Ensure there are bytes to decode. + if len(serialized) == 0 { + return 0, errDeserialize("no serialized bytes") + } + // Deserialize the header code. + code, offset := deserializeVLQ(serialized) + if offset >= len(serialized) { + return offset, errDeserialize( + "unexpected end of data after header code", + ) + } + // Decode the header code. Bit 0 indicates containing transaction is a coinbase. Bits 1-x encode height of + // containing transaction. + stxo.IsCoinBase = code&0x01 != 0 + stxo.Height = int32(code >> 1) + if stxo.Height > 0 { + // The legacy v1 spend journal format conditionally tracked the containing transaction version when the height + // was non-zero, so this is required for backwards compat. + _, bytesRead := deserializeVLQ(serialized[offset:]) + offset += bytesRead + if offset >= len(serialized) { + return offset, errDeserialize( + "unexpected end of data after reserved", + ) + } + } + // Decode the compressed txout. + amount, pkScript, bytesRead, e := decodeCompressedTxOut( + serialized[offset:], + ) + offset += bytesRead + if e != nil { + return offset, errDeserialize( + fmt.Sprint( + "unable to decode txout: ", e, + ), + ) + } + stxo.Amount = int64(amount) + stxo.PkScript = pkScript + return offset, nil +} + +// deserializeSpendJournalEntry decodes the passed serialized byte slice into a slice of spent txouts according to the +// format described in detail above. +// +// Since the serialization format is not self describing as noted in the format comments this function also requires the +// transactions that spend the txouts. +func deserializeSpendJournalEntry(serialized []byte, txns []*wire.MsgTx) ([]SpentTxOut, error) { + // Calculate the total number of stxos. + var numStxos int + for _, tx := range txns { + numStxos += len(tx.TxIn) + } + // When a block has no spent txouts there is nothing to serialize. + if len(serialized) == 0 { + // Ensure the block actually has no stxos. This should never happen unless there is database corruption or an + // empty entry erroneously made its way into the database. + if numStxos != 0 { + return nil, AssertError( + fmt.Sprintf( + "mismatched spend journal serialization - no serialization for expected %d stxos", + numStxos, + ), + ) + } + return nil, nil + } + // Loop backwards through all transactions so everything is read in reverse order to match the serialization order. + stxoIdx := numStxos - 1 + offset := 0 + stxos := make([]SpentTxOut, numStxos) + for txIdx := len(txns) - 1; txIdx > -1; txIdx-- { + tx := txns[txIdx] + // Loop backwards through all of the transaction inputs and read the associated stxo. + for txInIdx := len(tx.TxIn) - 1; txInIdx > -1; txInIdx-- { + txIn := tx.TxIn[txInIdx] + stxo := &stxos[stxoIdx] + stxoIdx-- + n, e := decodeSpentTxOut(serialized[offset:], stxo) + offset += n + if e != nil { + return nil, errDeserialize( + fmt.Sprintf( + "unable to decode stxo for %v: %v", + txIn.PreviousOutPoint, e, + ), + ) + } + } + } + return stxos, nil +} + +// serializeSpendJournalEntry serializes all of the passed spent txouts into a single byte slice according to the format +// described in detail above. +func serializeSpendJournalEntry(stxos []SpentTxOut) []byte { + if len(stxos) == 0 { + return nil + } + // Calculate the size needed to serialize the entire journal entry. + var size int + for i := range stxos { + size += spentTxOutSerializeSize(&stxos[i]) + } + serialized := make([]byte, size) + // Serialize each individual stxo directly into the slice in reverse order one after the other. + var offset int + for i := len(stxos) - 1; i > -1; i-- { + offset += putSpentTxOut(serialized[offset:], &stxos[i]) + } + return serialized +} + +// dbFetchSpendJournalEntry fetches the spend journal entry for the passed block and deserializes it into a slice of +// spent txout entries. +// +// NOTE: Legacy entries will not have the coinbase flag or height set unless it was the final output spend in the +// containing transaction. +// +// It is up to the caller to handle this properly by looking the information up in the utxo set. +func dbFetchSpendJournalEntry(dbTx database.Tx, block *block.Block) ([]SpentTxOut, error) { + // Exclude the coinbase transaction since it can't spend anything. + spendBucket := dbTx.Metadata().Bucket(spendJournalBucketName) + serialized := spendBucket.Get(block.Hash()[:]) + blockTxns := block.WireBlock().Transactions[1:] + stxos, e := deserializeSpendJournalEntry(serialized, blockTxns) + if e != nil { + // Ensure any deserialization errors are returned as database corruption errors. + if isDeserializeErr(e) { + return nil, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "corrupt spend information for %v: %v", + block.Hash(), e, + ), + } + } + return nil, e + } + return stxos, nil +} + +// dbPutSpendJournalEntry uses an existing database transaction to update the spend journal entry for the given block +// hash using the provided slice of spent txouts. The spent txouts slice must contain an entry for every txout the +// transactions in the block spend in the order they are spent. +func dbPutSpendJournalEntry(dbTx database.Tx, blockHash *chainhash.Hash, stxos []SpentTxOut) (e error) { + spendBucket := dbTx.Metadata().Bucket(spendJournalBucketName) + serialized := serializeSpendJournalEntry(stxos) + return spendBucket.Put(blockHash[:], serialized) +} + +// dbRemoveSpendJournalEntry uses an existing database transaction to remove the spend journal entry for the passed +// block hash. +func dbRemoveSpendJournalEntry(dbTx database.Tx, blockHash *chainhash.Hash) (e error) { + spendBucket := dbTx.Metadata().Bucket(spendJournalBucketName) + return spendBucket.Delete(blockHash[:]) +} + +// The unspent transaction output ( +// utxo) set consists of an entry for each unspent output using a format that is optimized to reduce space using domain specific compression algorithms. This format is a slightly modified version of the format used in Bitcoin Core. +// Each entry is keyed by an outpoint as specified below. +// It is important to note that the key encoding uses a VLQ, which employs an MSB encoding so iteration of utxos when doing byte-wise comparisons will produce them in order. +// The serialized key format is: +// +// Field Type Size +// hash chainhash.Hash chainhash.HashSize +// output index VLQ variable +// The serialized value format is: +//
+// Field Type Size +// header code VLQ variable +// compressed txout +// compressed amount VLQ variable +// compressed script []byte variable +// The serialized header code format is: +// bit 0 - containing transaction is a coinbase +// bits 1-x - height of the block that contains the unspent txout +// Example 1: +// From tx in main blockchain: +// Blk 1, 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098:0 +// 03320496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52 +// <><------------------------------------------------------------------> +// | | +// header code compressed txout +// - header code: 0x03 (coinbase, height 1) +// - compressed txout: +// - 0x32: VLQ-encoded compressed amount for 5000000000 (50 DUO) +// - 0x04: special script type pay-to-pubkey +// - 0x96...52: x-coordinate of the pubkey +// Example 2: +// From tx in main blockchain: +// Blk 113931, +// 4a16969aa4764dd7507fc1de7f0baa4850a246de90c45e59a3207f9a26b5036f:2 +// 8cf316800900b8025be1b3efc63b0ad48e7f9f10e87544528d58 +// <----><------------------------------------------> +// | | +// header code compressed txout +// - header code: 0x8cf316 (not coinbase, height 113931) +// - compressed txout: +// - 0x8009: VLQ-encoded compressed amount for 15000000 (0.15 DUO) +// - 0x00: special script type pay-to-pubkey-hash +// - 0xb8...58: pubkey hash +// Example 3: +// From tx in main blockchain: +// Blk 338156, +// 1b02d1c8cfef60a189017b9a420c682cf4a0028175f2f563209e4ff61c8c3620:22 +// a8a2588ba5b9e763011dd46a006572d820e448e12d2bbb38640bc718e6 +// <----><--------------------------------------------------> +// | | +// header code compressed txout +// - header code: 0xa8a258 (not coinbase, height 338156) +// - compressed txout: +// - 0x8ba5b9e763: VLQ-encoded compressed amount for 366875659 (3. +// 66875659 DUO) +// - 0x01: special script type pay-to-script-hash +// - 0x1d...e6: script hash +// ----------------------------------------------------------------------------- + +// maxUint32VLQSerializeSize is the maximum number of bytes a max uint32 takes to serialize as a VLQ. +var maxUint32VLQSerializeSize = serializeSizeVLQ(1<<32 - 1) + +// outpointKeyPool defines a concurrent safe free list of byte slices used to provide temporary buffers for outpoint +// database keys. +var outpointKeyPool = sync.Pool{ + New: func() interface{} { + b := make([]byte, chainhash.HashSize+maxUint32VLQSerializeSize) + return &b // Pointer to slice to avoid boxing alloc. + }, +} + +// outpointKey returns a key suitable for use as a database key in the utxo set while making use of a free list. +// +// A new buffer is allocated if there are not already any available on the free list. The returned byte slice should be +// returned to the free list by using the recycleOutpointKey function when the caller is done with it _unless_ the slice +// will need to live for longer than the caller can calculate such as when used to write to the database. +func outpointKey(outpoint wire.OutPoint) *[]byte { + // A VLQ employs an MSB encoding, so they are useful not only to reduce the amount of storage space, but also so + // iteration of utxos when doing byte-wise comparisons will produce them in order. + key := outpointKeyPool.Get().(*[]byte) + idx := uint64(outpoint.Index) + *key = (*key)[:chainhash.HashSize+serializeSizeVLQ(idx)] + copy(*key, outpoint.Hash[:]) + putVLQ((*key)[chainhash.HashSize:], idx) + return key +} + +// recycleOutpointKey puts the provided byte slice, which should have been obtained via the outpointKey function, back +// on the free list. +func recycleOutpointKey(key *[]byte) { + outpointKeyPool.Put(key) +} + +// utxoEntryHeaderCode returns the calculated header code to be used when serializing the provided utxo entry. +func utxoEntryHeaderCode(entry *UtxoEntry) (rv uint64, e error) { + if entry.IsSpent() { + return 0, AssertError("attempt to serialize spent UXTO header") + } + // As described in the serialization format comments, the header code encodes the height shifted over one bit and + // the coinbase flag in the lowest bit. + headerCode := uint64(entry.BlockHeight()) << 1 + if entry.IsCoinBase() { + headerCode |= 0x01 + } + return headerCode, nil +} + +// serializeUtxoEntry returns the entry serialized to a format that is suitable for long-term storage. The format is +// described in detail above. +func serializeUtxoEntry(entry *UtxoEntry) ([]byte, error) { + // Spent outputs have no serialization. + if entry.IsSpent() { + return nil, nil + } + // Encode the header code. + headerCode, e := utxoEntryHeaderCode(entry) + if e != nil { + return nil, e + } + // Calculate the size needed to serialize the entry. + size := serializeSizeVLQ(headerCode) + + compressedTxOutSize(uint64(entry.Amount()), entry.PkScript()) + // Serialize the header code followed by the compressed unspent transaction output. + serialized := make([]byte, size) + offset := putVLQ(serialized, headerCode) + _ = putCompressedTxOut( + serialized[offset:], uint64(entry.Amount()), + entry.PkScript(), + ) + return serialized, nil +} + +// deserializeUtxoEntry decodes a utxo entry from the passed serialized byte slice into a new UtxoEntry using a format +// that is suitable for long -term storage. The format is described in detail above. +func deserializeUtxoEntry(serialized []byte) (entry *UtxoEntry, e error) { + // Deserialize the header code. + code, offset := deserializeVLQ(serialized) + if offset >= len(serialized) { + return nil, errDeserialize("unexpected end of data after header") + } + // Decode the header code. Bit 0 indicates whether the containing transaction is a coinbase. Bits 1-x encode height + // of containing transaction. + isCoinBase := code&0x01 != 0 + blockHeight := int32(code >> 1) + // Decode the compressed unspent transaction output. + var amount uint64 + var pkScript []byte + amount, pkScript, _, e = decodeCompressedTxOut(serialized[offset:]) + if e != nil { + return nil, errDeserialize( + fmt.Sprint( + "unable to decode utxo:", e, + ), + ) + } + entry = &UtxoEntry{ + amount: int64(amount), + pkScript: pkScript, + blockHeight: blockHeight, + packedFlags: 0, + } + if isCoinBase { + entry.packedFlags |= tfCoinBase + } + return entry, nil +} + +// dbFetchUtxoEntryByHash attempts to find and fetch a utxo for the given hash. It uses a cursor and seek to try and do +// this as efficiently as possible. When there are no entries for the provided hash, nil will be returned for the both +// the entry and the error. +func dbFetchUtxoEntryByHash(dbTx database.Tx, hash *chainhash.Hash) (*UtxoEntry, error) { + // Attempt to find an entry by seeking for the hash along with a zero index. Due to the fact the keys are serialized + // as , where the index uses an MSB encoding, if there are any entries for the hash at all, one will be + // found. + cursor := dbTx.Metadata().Bucket(utxoSetBucketName).Cursor() + key := outpointKey(wire.OutPoint{Hash: *hash, Index: 0}) + ok := cursor.Seek(*key) + recycleOutpointKey(key) + if !ok { + return nil, nil + } + // An entry was found, but it could just be an entry with the next highest hash after the requested one, so make + // sure the hashes actually match. + cursorKey := cursor.Key() + if len(cursorKey) < chainhash.HashSize { + return nil, nil + } + if !bytes.Equal(hash[:], cursorKey[:chainhash.HashSize]) { + return nil, nil + } + return deserializeUtxoEntry(cursor.Value()) +} + +// dbFetchUtxoEntry uses an existing database transaction to fetch the specified transaction output from the utxo set. +// When there is no entry for the provided output, nil will be returned for both the entry and the error. +func dbFetchUtxoEntry(dbTx database.Tx, outpoint wire.OutPoint) (*UtxoEntry, error) { + // Fetch the unspent transaction output information for the passed transaction output. Return now when there is no + // entry. + key := outpointKey(outpoint) + utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName) + serializedUtxo := utxoBucket.Get(*key) + recycleOutpointKey(key) + if serializedUtxo == nil { + return nil, nil + } + // A non-nil zero-length entry means there is an entry in the database for a spent transaction output which should + // never be the case. + if len(serializedUtxo) == 0 { + return nil, AssertError( + fmt.Sprint( + "database contains entry for spent tx output ", + outpoint, + ), + ) + } + // Deserialize the utxo entry and return it. + entry, e := deserializeUtxoEntry(serializedUtxo) + if e != nil { + // Ensure any deserialization errors are returned as database corruption errors. + if isDeserializeErr(e) { + return nil, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "corrupt utxo entry for %v: %v", + outpoint, e, + ), + } + } + return nil, e + } + return entry, nil +} + +// dbPutUtxoView uses an existing database transaction to update the utxo set in the database based on the provided utxo +// view contents and state. +// +// In particular, only the entries that have been marked as modified are written to the database. +func dbPutUtxoView(dbTx database.Tx, view *UtxoViewpoint) (e error) { + utxoBucket := dbTx.Metadata().Bucket(utxoSetBucketName) + for outpoint, entry := range view.entries { + // No need to update the database if the entry was not modified. + if entry == nil || !entry.isModified() { + continue + } + // Remove the utxo entry if it is spent. + if entry.IsSpent() { + key := outpointKey(outpoint) + e := utxoBucket.Delete(*key) + recycleOutpointKey(key) + if e != nil { + return e + } + continue + } + // Serialize and store the utxo entry. + serialized, e := serializeUtxoEntry(entry) + if e != nil { + return e + } + key := outpointKey(outpoint) + e = utxoBucket.Put(*key, serialized) + // NOTE: The key is intentionally not recycled here since the database interface contract prohibits + // modifications. It will be garbage collected normally when the database is done with it. + if e != nil { + return e + } + } + return nil +} + +// The block index consists of two buckets with an entry for every block in +// the main chain. One bucket is for the hash to height mapping and the other is for the height to hash mapping. +// The serialized format for values in the hash to height bucket is: +// +// Field Type Size +// height uint32 4 bytes +// The serialized format for values in the height to hash bucket is: +// +// Field Type Size +// hash chainhash.Hash chainhash.HashSize +// ----------------------------------------------------------------------------- + +// dbPutBlockIndex uses an existing database transaction to update or add the block index entries for the hash to height +// and height to hash mappings for the provided values. +func dbPutBlockIndex(dbTx database.Tx, hash *chainhash.Hash, height int32) (e error) { + // Serialize the height for use in the index entries. + var serializedHeight [4]byte + byteOrder.PutUint32(serializedHeight[:], uint32(height)) + // Add the block hash to height mapping to the index. + meta := dbTx.Metadata() + hashIndex := meta.Bucket(hashIndexBucketName) + if e := hashIndex.Put(hash[:], serializedHeight[:]); E.Chk(e) { + return e + } + // Add the block height to hash mapping to the index. + heightIndex := meta.Bucket(heightIndexBucketName) + return heightIndex.Put(serializedHeight[:], hash[:]) +} + +// dbRemoveBlockIndex uses an existing database transaction remove block index entries from the hash to height and +// height to hash mappings for the provided values. +func dbRemoveBlockIndex(dbTx database.Tx, hash *chainhash.Hash, height int32) (e error) { + // Remove the block hash to height mapping. + meta := dbTx.Metadata() + hashIndex := meta.Bucket(hashIndexBucketName) + if e := hashIndex.Delete(hash[:]); E.Chk(e) { + return e + } + // Remove the block height to hash mapping. + var serializedHeight [4]byte + byteOrder.PutUint32(serializedHeight[:], uint32(height)) + heightIndex := meta.Bucket(heightIndexBucketName) + return heightIndex.Delete(serializedHeight[:]) +} + +// dbFetchHeightByHash uses an existing database transaction to retrieve the height for the provided hash from the +// index. +func dbFetchHeightByHash(dbTx database.Tx, hash *chainhash.Hash) (int32, error) { + meta := dbTx.Metadata() + hashIndex := meta.Bucket(hashIndexBucketName) + serializedHeight := hashIndex.Get(hash[:]) + if serializedHeight == nil { + str := fmt.Sprintf( + "dbFetchHeightByHash: block %s is not in the main chain", hash, + ) + return 0, errNotInMainChain(str) + } + return int32(byteOrder.Uint32(serializedHeight)), nil +} + +// dbFetchHashByHeight uses an existing database transaction to retrieve the hash for the provided height from the +// index. +func dbFetchHashByHeight(dbTx database.Tx, height int32) (*chainhash.Hash, error) { + var serializedHeight [4]byte + byteOrder.PutUint32(serializedHeight[:], uint32(height)) + meta := dbTx.Metadata() + heightIndex := meta.Bucket(heightIndexBucketName) + hashBytes := heightIndex.Get(serializedHeight[:]) + if hashBytes == nil { + str := fmt.Sprintf( + "no block at height %d exists", height, + ) + return nil, errNotInMainChain(str) + } + var hash chainhash.Hash + copy(hash[:], hashBytes) + return &hash, nil +} + +// The best chain state consists of the best block hash and height, the total number of transactions up to and including +// those in the best block, and the accumulated work sum up to and including the best block. +// +// The serialized format is: +// +// +// Field Type Size +// block hash chainhash.Hash chainhash.HashSize +// block height uint32 4 bytes +// total txns uint64 8 bytes +// work sum length uint32 4 bytes +// work sum big.Int work sum length +// ----------------------------------------------------------------------------- + +// bestChainState represents the data to be stored the database for the current best chain state. +type bestChainState struct { + hash chainhash.Hash + height uint32 + totalTxns uint64 + workSum *big.Int +} + +// serializeBestChainState returns the serialization of the passed block best chain state. This is data to be stored in +// the chain state bucket. +func serializeBestChainState(state bestChainState) []byte { + // Calculate the full size needed to serialize the chain state. + workSumBytes := state.workSum.Bytes() + workSumBytesLen := uint32(len(workSumBytes)) + serializedLen := chainhash.HashSize + 4 + 8 + 4 + workSumBytesLen + // Serialize the chain state. + serializedData := make([]byte, serializedLen) + copy(serializedData[0:chainhash.HashSize], state.hash[:]) + offset := uint32(chainhash.HashSize) + byteOrder.PutUint32(serializedData[offset:], state.height) + offset += 4 + byteOrder.PutUint64(serializedData[offset:], state.totalTxns) + offset += 8 + byteOrder.PutUint32(serializedData[offset:], workSumBytesLen) + offset += 4 + copy(serializedData[offset:], workSumBytes) + return serializedData[:] +} + +// deserializeBestChainState deserializes the passed serialized best chain state. This is data stored in the chain state +// bucket and is updated after every block is connected or disconnected form the main chain. block. +func deserializeBestChainState(serializedData []byte) (bestChainState, error) { + // Ensure the serialized data has enough bytes to properly deserialize the hash, height, total transactions, and + // work sum length. + if len(serializedData) < chainhash.HashSize+16 { + return bestChainState{}, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: "corrupt best chain state", + } + } + state := bestChainState{} + copy(state.hash[:], serializedData[0:chainhash.HashSize]) + offset := uint32(chainhash.HashSize) + state.height = byteOrder.Uint32(serializedData[offset : offset+4]) + offset += 4 + state.totalTxns = byteOrder.Uint64(serializedData[offset : offset+8]) + offset += 8 + workSumBytesLen := byteOrder.Uint32(serializedData[offset : offset+4]) + offset += 4 + // Ensure the serialized data has enough bytes to deserialize the work sum. + if uint32(len(serializedData[offset:])) < workSumBytesLen { + return bestChainState{}, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: "corrupt best chain state", + } + } + workSumBytes := serializedData[offset : offset+workSumBytesLen] + state.workSum = new(big.Int).SetBytes(workSumBytes) + return state, nil +} + +// dbPutBestState uses an existing database transaction to update the best chain state with the given parameters. +func dbPutBestState(dbTx database.Tx, snapshot *BestState, workSum *big.Int) (e error) { + // Serialize the current best chain state. + serializedData := serializeBestChainState( + bestChainState{ + hash: snapshot.Hash, + height: uint32(snapshot.Height), + totalTxns: snapshot.TotalTxns, + workSum: workSum, + }, + ) + // Store the current best chain state into the database. + return dbTx.Metadata().Put(chainStateKeyName, serializedData) +} + +// createChainState initializes both the database and the chain state to the genesis block. This includes creating the +// necessary buckets and inserting the genesis block so it must only be called on an uninitialized database. +func (b *BlockChain) createChainState() (e error) { + // Create a new node from the genesis block and set it as the best node. + genesisBlock := block.NewBlock(b.params.GenesisBlock) + // Tracec(func() string { + // xx, _ := genesisBlock.Bytes() + // return hex.EncodeToString(xx) + // }) + genesisBlock.SetHeight(0) + header := &genesisBlock.WireBlock().Header + node := NewBlockNode(header, nil) + node.status = statusDataStored | statusValid + var df Diffs + df, e = b.CalcNextRequiredDifficultyPlan9Controller(node) + node.Diffs.Store(df) + if e != nil { + } + b.BestChain.SetTip(node) + // Add the new node to the index which is used for faster lookups. + b.Index.addNode(node) + // Initialize the state related to the best block. Since it is the genesis block, use its timestamp for the median + // time. + numTxns := uint64(len(genesisBlock.WireBlock().Transactions)) + blockSize := uint64(genesisBlock.WireBlock().SerializeSize()) + blockWeight := uint64(GetBlockWeight(genesisBlock)) + b.stateSnapshot = newBestState( + node, blockSize, blockWeight, numTxns, + numTxns, time.Unix(node.timestamp, 0), + ) + // Create the initial the database chain state including creating the necessary index buckets and inserting the + // genesis block. + e = b.db.Update( + func(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + // Create the bucket that houses the block index data. + _, e = meta.CreateBucket(blockIndexBucketName) + if e != nil { + return e + } + // Create the bucket that houses the chain block hash to height index. + _, e = meta.CreateBucket(hashIndexBucketName) + if e != nil { + return e + } + // Create the bucket that houses the chain block height to hash index. + _, e = meta.CreateBucket(heightIndexBucketName) + if e != nil { + return e + } + // Create the bucket that houses the spend journal data and store its + // version. + _, e = meta.CreateBucket(spendJournalBucketName) + if e != nil { + return e + } + e = dbPutVersion( + dbTx, utxoSetVersionKeyName, + latestUtxoSetBucketVersion, + ) + if e != nil { + return e + } + // Create the bucket that houses the utxo set and store its version. Note that the genesis block coinbase + // transaction is intentionally not inserted here since it is not spendable by consensus rules. + _, e = meta.CreateBucket(utxoSetBucketName) + if e != nil { + return e + } + e = dbPutVersion( + dbTx, spendJournalVersionKeyName, + latestSpendJournalBucketVersion, + ) + if e != nil { + return e + } + // Save the genesis block to the block index database. + e = dbStoreBlockNode(dbTx, node) + if e != nil { + return e + } + // Add the genesis block hash to height and height to hash mappings to the index. + e = dbPutBlockIndex(dbTx, &node.hash, node.height) + if e != nil { + return e + } + // Store the current best chain state into the database. + node.workSum = CalcWork(node.bits, node.height, node.version) + e = dbPutBestState(dbTx, b.stateSnapshot, node.workSum) + if e != nil { + return e + } + // Store the genesis block into the database. + return dbStoreBlock(dbTx, genesisBlock) + }, + ) + return e +} + +// initChainState attempts to load and initialize the chain state from the database. When the db does not yet contain +// any chain state, both it and the chain state are initialized to the genesis block. +func (b *BlockChain) initChainState() (e error) { + // Determine the state of the chain database. We may need to initialize everything from scratch or upgrade certain + // buckets. + var initialized, hasBlockIndex bool + e = b.db.View( + func(dbTx database.Tx) (e error) { + initialized = dbTx.Metadata().Get(chainStateKeyName) != nil + hasBlockIndex = dbTx.Metadata().Bucket(blockIndexBucketName) != nil + return nil + }, + ) + if e != nil { + return e + } + if !initialized { + // At this point the database has not already been initialized, so initialize both it and the chain state to the + // genesis block. + return b.createChainState() + } + if !hasBlockIndex { + e = migrateBlockIndex(b.db) + if e != nil { + return nil + } + } + // Attempt to load the chain state from the database. + e = b.db.View( + func(dbTx database.Tx) (e error) { + // Fetch the stored chain state from the database metadata. When it doesn't exist, it means the database hasn't + // been initialized for use with chain yet, so break out now to allow that to happen under a writable database + // transaction. + serializedData := dbTx.Metadata().Get(chainStateKeyName) + T.F("serialized chain state: %0x", serializedData) + state, e := deserializeBestChainState(serializedData) + if e != nil { + return e + } + // Load all of the headers from the data for the known best chain and construct the blk index accordingly. + // Since the number of nodes are already known, perform a single alloc for them versus a whole bunch of little + // ones to reduce pressure on the GC. + T.Ln("loading blk index...") + blockIndexBucket := dbTx.Metadata().Bucket(blockIndexBucketName) + // Determine how many blocks will be loaded into the index so we can allocate the right amount. + var blockCount int32 + cursor := blockIndexBucket.Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() { + blockCount++ + } + blockNodes := make([]BlockNode, blockCount) + var i int32 + var lastNode *BlockNode + cursor = blockIndexBucket.Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() { + var header *wire.BlockHeader + var status blockStatus + header, status, e = deserializeBlockRow(cursor.Value()) + if e != nil { + return e + } + // Determine the parent blk node. Since we iterate blk headers in order of height, if the blocks are + // mostly linear there is a very good chance the previous header processed is the parent. + var parent *BlockNode + if lastNode == nil { + blockHash := header.BlockHash() + if !blockHash.IsEqual(b.params.GenesisHash) { + return AssertError( + fmt.Sprintf( + "initChainState: expected first entry in blk index"+ + " to be genesis blk, found %s", + blockHash, + ), + ) + } + } else if header.PrevBlock == lastNode.hash { + // Since we iterate blk headers in order of height, if the blocks are mostly linear there is a very + // good chance the previous header processed is the parent. + parent = lastNode + } else { + parent = b.Index.LookupNode(&header.PrevBlock) + if parent == nil { + return AssertError( + fmt.Sprint( + "initChainState: Could not find parent for blk ", + header.BlockHash(), + ), + ) + } + } + // Initialize the blk node for the blk, connect it, and add it to the blk index. + node := &blockNodes[i] + initBlockNode(node, header, parent) + node.status = status + b.Index.addNode(node) + lastNode = node + i++ + } + // Set the best chain view to the stored best state. + tip := b.Index.LookupNode(&state.hash) + if tip == nil { + return AssertError( + fmt.Sprintf( + "initChainState: cannot find chain tip %s in blk index", + state.hash, + ), + ) + } + b.BestChain.SetTip(tip) + // Load the raw blk bytes for the best blk. + blockBytes, e := dbTx.FetchBlock(&state.hash) + if e != nil { + return e + } + var blk wire.Block + e = blk.Deserialize(bytes.NewReader(blockBytes)) + if e != nil { + return e + } + // As a final consistency check, we'll run through all the nodes which are ancestors of the current chain tip, + // and mark them as valid if they aren't already marked as such. This is a safe assumption as all the blk + // before the current tip are valid by definition. + for iterNode := tip; iterNode != nil; iterNode = iterNode.parent { + // If this isn't already marked as valid in the index, then we'll mark it as valid now to ensure consistency + // once we 're up and running. + if !iterNode.status.KnownValid() { + I.F( + "Block %v (height=%v) ancestor of chain tip not"+ + " marked as valid, upgrading to valid for consistency", + iterNode.hash, iterNode.height, + ) + b.Index.SetStatusFlags(iterNode, statusValid) + } + } + // Initialize the state related to the best blk. + blockSize := uint64(len(blockBytes)) + blockWeight := uint64(GetBlockWeight(block.NewBlock(&blk))) + numTxns := uint64(len(blk.Transactions)) + b.stateSnapshot = newBestState( + tip, blockSize, blockWeight, + numTxns, state.totalTxns, tip.CalcPastMedianTime(), + ) + return nil + }, + ) + if e != nil { + return e + } + // As we might have updated the index after it was loaded, we'll attempt to flush the index to the DB. This will + // only result in a write if the elements are dirty, so it'll usually be a noop. + return b.Index.flushToDB() +} + +// deserializeBlockRow parses a value in the block index bucket into a block header and block status bitfield. +func deserializeBlockRow(blockRow []byte) (*wire.BlockHeader, blockStatus, error) { + buffer := bytes.NewReader(blockRow) + var header wire.BlockHeader + e := header.Deserialize(buffer) + if e != nil { + return nil, statusNone, e + } + statusByte, e := buffer.ReadByte() + if e != nil { + return nil, statusNone, e + } + return &header, blockStatus(statusByte), nil +} + +// dbFetchHeaderByHash uses an existing database transaction to retrieve the block header for the provided hash. +func dbFetchHeaderByHash(dbTx database.Tx, hash *chainhash.Hash) (*wire.BlockHeader, error) { + headerBytes, e := dbTx.FetchBlockHeader(hash) + if e != nil { + return nil, e + } + var header wire.BlockHeader + e = header.Deserialize(bytes.NewReader(headerBytes)) + if e != nil { + return nil, e + } + return &header, nil +} + +/*// dbFetchHeaderByHeight uses an existing database transaction to retrieve +the block header for the provided height. +func dbFetchHeaderByHeight(dbTx database.Tx, +height int32) (*wire.BlockHeader, error) { + hash, e := dbFetchHashByHeight(dbTx, height) + if e != nil { + return nil, e + } + return dbFetchHeaderByHash(dbTx, hash) +} */ + +// dbFetchBlockByNode uses an existing database transaction to retrieve the raw block for the provided node, deserialize +// it, and return a util.Block with the height set. +func dbFetchBlockByNode(dbTx database.Tx, node *BlockNode) (*block.Block, error) { + // Load the raw block bytes from the database. + blockBytes, e := dbTx.FetchBlock(&node.hash) + if e != nil { + return nil, e + } + // Create the encapsulated block and set the height appropriately. + block, e := block.NewFromBytes(blockBytes) + if e != nil { + return nil, e + } + block.SetHeight(node.height) + return block, nil +} + +// dbStoreBlockNode stores the block header and validation status to the block index bucket. This overwrites the current +// entry if there exists one. +func dbStoreBlockNode(dbTx database.Tx, node *BlockNode) (e error) { + // Serialize block data to be stored. + w := bytes.NewBuffer(make([]byte, 0, blockHdrSize+1)) + header := node.Header() + e = header.Serialize(w) + if e != nil { + return e + } + e = w.WriteByte(byte(node.status)) + if e != nil { + return e + } + value := w.Bytes() + // Write block header data to block index bucket. + blockIndexBucket := dbTx.Metadata().Bucket(blockIndexBucketName) + key := blockIndexKey(&node.hash, uint32(node.height)) + return blockIndexBucket.Put(key, value) +} + +// dbStoreBlock stores the provided block in the database if it is not already there. The full block data is written to +// ffldb. +func dbStoreBlock(dbTx database.Tx, block *block.Block) (e error) { + hasBlock, e := dbTx.HasBlock(block.Hash()) + if e != nil { + return e + } + if hasBlock { + return nil + } + return dbTx.StoreBlock(block) +} + +// blockIndexKey generates the binary key for an entry in the block index bucket. The key is composed of the block +// height encoded as a big-endian 32 -bit unsigned int followed by the 32 byte block hash. +func blockIndexKey(blockHash *chainhash.Hash, blockHeight uint32) []byte { + indexKey := make([]byte, chainhash.HashSize+4) + binary.BigEndian.PutUint32(indexKey[0:4], blockHeight) + copy(indexKey[4:chainhash.HashSize+4], blockHash[:]) + return indexKey +} + +// BlockByHeight returns the block at the given height in the main chain. This function is safe for concurrent access. +func (b *BlockChain) BlockByHeight(blockHeight int32) (*block.Block, error) { + // Lookup the block height in the best chain. + node := b.BestChain.NodeByHeight(blockHeight) + if node == nil { + str := fmt.Sprintf("no block at height %d exists", blockHeight) + return nil, errNotInMainChain(str) + } + // Load the block from the database and return it. + var block *block.Block + e := b.db.View( + func(dbTx database.Tx) (e error) { + block, e = dbFetchBlockByNode(dbTx, node) + return e + }, + ) + return block, e +} + +// BlockByHash returns the block from the main chain with the given hash with the appropriate chain height set. +// +// This function is safe for concurrent access. +func (b *BlockChain) BlockByHash(hash *chainhash.Hash) (block *block.Block, e error) { + // Lookup the block hash in block index and ensure it is in the best chain. + node := b.Index.LookupNode(hash) + if node == nil || !b.BestChain.Contains(node) { + str := fmt.Sprintf("blockByHash: block %s is not in the main chain", hash) + return nil, errNotInMainChain(str) + } + // Load the block from the database and return it. + e = b.db.View( + func(dbTx database.Tx) (er error) { + block, e = dbFetchBlockByNode(dbTx, node) + return er + }, + ) + return block, e +} diff --git a/pkg/blockchain/chainio_test.go b/pkg/blockchain/chainio_test.go new file mode 100644 index 0000000..459349b --- /dev/null +++ b/pkg/blockchain/chainio_test.go @@ -0,0 +1,719 @@ +package blockchain + +import ( + "bytes" + "errors" + "reflect" + "testing" + + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/wire" +) + +// TestErrNotInMainChain ensures the functions related to errNotInMainChain work as expected. +func TestErrNotInMainChain(t *testing.T) { + errStr := "no block at height 1 exists" + e := error(errNotInMainChain(errStr)) + // Ensure the stringized output for the error is as expected. + if e != nil && e.Error() != errStr { + t.Fatalf("errNotInMainChain retuned unexpected error string - "+ + "got %q, want %q", e.Error(), errStr, + ) + } + // Ensure error is detected as the correct type. + if !isNotInMainChainErr(e) { + t.Fatalf("isNotInMainChainErr did not detect as expected type") + } + e = errors.New("something else") + if isNotInMainChainErr(e) { + t.Fatalf("isNotInMainChainErr detected incorrect type") + } +} + +// TestStxoSerialization ensures serializing and deserializing spent transaction output entries works as expected. +func TestStxoSerialization(t *testing.T) { + t.Parallel() + tests := []struct { + name string + stxo SpentTxOut + serialized []byte + }{ + // From block 170 in main blockchain. + { + name: "Spends last output of coinbase", + stxo: SpentTxOut{ + Amount: 5000000000, + PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), + IsCoinBase: true, + Height: 9, + }, + serialized: hexToBytes("1300320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), + }, + // Adapted from block 100025 in main blockchain. + { + name: "Spends last output of non coinbase", + stxo: SpentTxOut{ + Amount: 13761000000, + PkScript: hexToBytes("76a914b2fb57eadf61e106a100a7445a8c3f67898841ec88ac"), + IsCoinBase: false, + Height: 100024, + }, + serialized: hexToBytes("8b99700086c64700b2fb57eadf61e106a100a7445a8c3f67898841ec"), + }, + // Adapted from block 100025 in main blockchain. + { + name: "Does not spend last output, legacy format", + stxo: SpentTxOut{ + Amount: 34405000000, + PkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), + }, + serialized: hexToBytes("0091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e86"), + }, + } + for _, test := range tests { + // Ensure the function to calculate the serialized size without actually serializing it is calculated properly. + gotSize := spentTxOutSerializeSize(&test.stxo) + if gotSize != len(test.serialized) { + t.Errorf("SpentTxOutSerializeSize (%s): did not get "+ + "expected size - got %d, want %d", test.name, + gotSize, len(test.serialized), + ) + continue + } + // Ensure the stxo serializes to the expected value. + gotSerialized := make([]byte, gotSize) + gotBytesWritten := putSpentTxOut(gotSerialized, &test.stxo) + if !bytes.Equal(gotSerialized, test.serialized) { + t.Errorf("putSpentTxOut (%s): did not get expected "+ + "bytes - got %x, want %x", test.name, + gotSerialized, test.serialized, + ) + continue + } + if gotBytesWritten != len(test.serialized) { + t.Errorf("putSpentTxOut (%s): did not get expected "+ + "number of bytes written - got %d, want %d", + test.name, gotBytesWritten, + len(test.serialized), + ) + continue + } + // Ensure the serialized bytes are decoded back to the expected stxo. + var gotStxo SpentTxOut + gotBytesRead, e := decodeSpentTxOut(test.serialized, &gotStxo) + if e != nil { + t.Errorf("decodeSpentTxOut (%s): unexpected error: %v", + test.name, e, + ) + continue + } + if !reflect.DeepEqual(gotStxo, test.stxo) { + t.Errorf("decodeSpentTxOut (%s) mismatched entries - "+ + "got %v, want %v", test.name, gotStxo, test.stxo, + ) + continue + } + if gotBytesRead != len(test.serialized) { + t.Errorf("decodeSpentTxOut (%s): did not get expected "+ + "number of bytes read - got %d, want %d", + test.name, gotBytesRead, len(test.serialized), + ) + continue + } + } +} + +// TestStxoDecodeErrors performs negative tests against decoding spent transaction outputs to ensure error paths work as +// expected. +func TestStxoDecodeErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + stxo SpentTxOut + serialized []byte + bytesRead int + errType error + }{ + { + name: "nothing serialized", + stxo: SpentTxOut{}, + serialized: hexToBytes(""), + errType: errDeserialize(""), + bytesRead: 0, + }, + { + name: "no data after header code w/o reserved", + stxo: SpentTxOut{}, + serialized: hexToBytes("00"), + errType: errDeserialize(""), + bytesRead: 1, + }, + { + name: "no data after header code with reserved", + stxo: SpentTxOut{}, + serialized: hexToBytes("13"), + errType: errDeserialize(""), + bytesRead: 1, + }, + { + name: "no data after reserved", + stxo: SpentTxOut{}, + serialized: hexToBytes("1300"), + errType: errDeserialize(""), + bytesRead: 2, + }, + { + name: "incomplete compressed txout", + stxo: SpentTxOut{}, + serialized: hexToBytes("1332"), + errType: errDeserialize(""), + bytesRead: 2, + }, + } + for _, test := range tests { + // Ensure the expected error type is returned. + gotBytesRead, e := decodeSpentTxOut(test.serialized, + &test.stxo, + ) + if reflect.TypeOf(e) != reflect.TypeOf(test.errType) { + t.Errorf("decodeSpentTxOut (%s): expected error type "+ + "does not match - got %T, want %T", test.name, + e, test.errType, + ) + continue + } + // Ensure the expected number of bytes read is returned. + if gotBytesRead != test.bytesRead { + t.Errorf("decodeSpentTxOut (%s): unexpected number of "+ + "bytes read - got %d, want %d", test.name, + gotBytesRead, test.bytesRead, + ) + continue + } + } +} + +// TestSpendJournalSerialization ensures serializing and deserializing spend journal entries works as expected. +func TestSpendJournalSerialization(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry []SpentTxOut + blockTxns []*wire.MsgTx + serialized []byte + }{ + // From block 2 in main blockchain. + { + name: "No spends", + entry: nil, + blockTxns: nil, + serialized: nil, + }, + // From block 170 in main blockchain. + { + name: "One tx with one input spends last output of coinbase", + entry: []SpentTxOut{{ + Amount: 5000000000, + PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), + IsCoinBase: true, + Height: 9, + }, + }, + blockTxns: []*wire.MsgTx{{ // Coinbase omitted. + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), + Index: 0, + }, + SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{{ + Value: 1000000000, + PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac"), + }, + { + Value: 4000000000, + PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac"), + }, + }, + LockTime: 0, + }, + }, + serialized: hexToBytes("1300320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5c"), + }, + // Adapted from block 100025 in main blockchain. + { + name: "Two txns when one spends last output, one doesn't", + entry: []SpentTxOut{{ + Amount: 34405000000, + PkScript: hexToBytes("76a9146edbc6c4d31bae9f1ccc38538a114bf42de65e8688ac"), + IsCoinBase: false, + Height: 100024, + }, + { + Amount: 13761000000, + PkScript: hexToBytes("76a914b2fb57eadf61e106a100a7445a8c3f67898841ec88ac"), + IsCoinBase: false, + Height: 100024, + }, + }, + blockTxns: []*wire.MsgTx{{ // Coinbase omitted. + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *newHashFromStr("c0ed017828e59ad5ed3cf70ee7c6fb0f426433047462477dc7a5d470f987a537"), + Index: 1, + }, + SignatureScript: hexToBytes("493046022100c167eead9840da4a033c9a56470d7794a9bb1605b377ebe5688499b39f94be59022100fb6345cab4324f9ea0b9ee9169337534834638d818129778370f7d378ee4a325014104d962cac5390f12ddb7539507065d0def320d68c040f2e73337c3a1aaaab7195cb5c4d02e0959624d534f3c10c3cf3d73ca5065ebd62ae986b04c6d090d32627c"), + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{{ + Value: 5000000, + PkScript: hexToBytes("76a914f419b8db4ba65f3b6fcc233acb762ca6f51c23d488ac"), + }, + { + Value: 34400000000, + PkScript: hexToBytes("76a914cadf4fc336ab3c6a4610b75f31ba0676b7f663d288ac"), + }, + }, + LockTime: 0, + }, + { + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *newHashFromStr("92fbe1d4be82f765dfabc9559d4620864b05cc897c4db0e29adac92d294e52b7"), + Index: 0, + }, + SignatureScript: hexToBytes("483045022100e256743154c097465cf13e89955e1c9ff2e55c46051b627751dee0144183157e02201d8d4f02cde8496aae66768f94d35ce54465bd4ae8836004992d3216a93a13f00141049d23ce8686fe9b802a7a938e8952174d35dd2c2089d4112001ed8089023ab4f93a3c9fcd5bfeaa9727858bf640dc1b1c05ec3b434bb59837f8640e8810e87742"), + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{{ + Value: 5000000, + PkScript: hexToBytes("76a914a983ad7c92c38fc0e2025212e9f972204c6e687088ac"), + }, + { + Value: 13756000000, + PkScript: hexToBytes("76a914a6ebd69952ab486a7a300bfffdcb395dc7d47c2388ac"), + }, + }, + LockTime: 0, + }, + }, + serialized: hexToBytes("8b99700086c64700b2fb57eadf61e106a100a7445a8c3f67898841ec8b99700091f20f006edbc6c4d31bae9f1ccc38538a114bf42de65e86"), + }, + } + for i, test := range tests { + // Ensure the journal entry serializes to the expected value. + gotBytes := serializeSpendJournalEntry(test.entry) + if !bytes.Equal(gotBytes, test.serialized) { + t.Errorf("serializeSpendJournalEntry #%d (%s): "+ + "mismatched bytes - got %x, want %x", i, + test.name, gotBytes, test.serialized, + ) + continue + } + // Deserialize to a spend journal entry. + gotEntry, e := deserializeSpendJournalEntry(test.serialized, + test.blockTxns, + ) + if e != nil { + t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ + "unexpected error: %v", i, test.name, e, + ) + continue + } + // Ensure that the deserialized spend journal entry has the + // correct properties. + if !reflect.DeepEqual(gotEntry, test.entry) { + t.Errorf("deserializeSpendJournalEntry #%d (%s) "+ + "mismatched entries - got %v, want %v", + i, test.name, gotEntry, test.entry, + ) + continue + } + } +} + +// TestSpendJournalErrors performs negative tests against deserializing spend journal entries to ensure error paths work +// as expected. +func TestSpendJournalErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + blockTxns []*wire.MsgTx + serialized []byte + errType error + }{ + // Adapted from block 170 in main blockchain. + { + name: "Force assertion due to missing stxos", + blockTxns: []*wire.MsgTx{{ // Coinbase omitted. + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), + Index: 0, + }, + SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), + Sequence: 0xffffffff, + }, + }, + LockTime: 0, + }, + }, + serialized: hexToBytes(""), + errType: AssertError(""), + }, + { + name: "Force deserialization error in stxos", + blockTxns: []*wire.MsgTx{{ // Coinbase omitted. + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *newHashFromStr("0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9"), + Index: 0, + }, + SignatureScript: hexToBytes("47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901"), + Sequence: 0xffffffff, + }, + }, + LockTime: 0, + }, + }, + serialized: hexToBytes("1301320511db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a"), + errType: errDeserialize(""), + }, + } + for _, test := range tests { + // Ensure the expected error type is returned and the returned slice is nil. + stxos, e := deserializeSpendJournalEntry(test.serialized, + test.blockTxns, + ) + if reflect.TypeOf(e) != reflect.TypeOf(test.errType) { + t.Errorf("deserializeSpendJournalEntry (%s): expected "+ + "error type does not match - got %T, want %T", + test.name, e, test.errType, + ) + continue + } + if stxos != nil { + t.Errorf("deserializeSpendJournalEntry (%s): returned "+ + "slice of spent transaction outputs is not nil", + test.name, + ) + continue + } + } +} + +// TestUtxoSerialization ensures serializing and deserializing unspent trasaction output entries works as expected. +func TestUtxoSerialization(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry *UtxoEntry + serialized []byte + }{ + // From tx in main blockchain: 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098:0 + { + name: "height 1, coinbase", + entry: &UtxoEntry{ + amount: 5000000000, + pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), + blockHeight: 1, + packedFlags: tfCoinBase, + }, + serialized: hexToBytes("03320496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52"), + }, + // From tx in main blockchain: 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098:0 + { + name: "height 1, coinbase, spent", + entry: &UtxoEntry{ + amount: 5000000000, + pkScript: hexToBytes("410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac"), + blockHeight: 1, + packedFlags: tfCoinBase | tfSpent, + }, + serialized: nil, + }, + // From tx in main blockchain: 8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb:1 + { + name: "height 100001, not coinbase", + entry: &UtxoEntry{ + amount: 1000000, + pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), + blockHeight: 100001, + packedFlags: 0, + }, + serialized: hexToBytes("8b99420700ee8bd501094a7d5ca318da2506de35e1cb025ddc"), + }, + // From tx in main blockchain: 8131ffb0a2c945ecaf9b9063e59558784f9c3a74741ce6ae2a18d0571dac15bb:1 + { + name: "height 100001, not coinbase, spent", + entry: &UtxoEntry{ + amount: 1000000, + pkScript: hexToBytes("76a914ee8bd501094a7d5ca318da2506de35e1cb025ddc88ac"), + blockHeight: 100001, + packedFlags: tfSpent, + }, + serialized: nil, + }, + } + for i, test := range tests { + // Ensure the utxo entry serializes to the expected value. + gotBytes, e := serializeUtxoEntry(test.entry) + if e != nil { + t.Errorf("serializeUtxoEntry #%d (%s) unexpected "+ + "error: %v", i, test.name, e, + ) + continue + } + if !bytes.Equal(gotBytes, test.serialized) { + t.Errorf("serializeUtxoEntry #%d (%s): mismatched "+ + "bytes - got %x, want %x", i, test.name, + gotBytes, test.serialized, + ) + continue + } + // Don't try to deserialize if the test entry was spent since it will have a nil serialization. + if test.entry.IsSpent() { + continue + } + // Deserialize to a utxo entry. + utxoEntry, e := deserializeUtxoEntry(test.serialized) + if e != nil { + t.Errorf("deserializeUtxoEntry #%d (%s) unexpected "+ + "error: %v", i, test.name, e, + ) + continue + } + // The deserialized entry must not be marked spent since unspent entries are not serialized. + if utxoEntry.IsSpent() { + t.Errorf("deserializeUtxoEntry #%d (%s) output should "+ + "not be marked spent", i, test.name, + ) + continue + } + // Ensure the deserialized entry has the same properties as the ones in the test entry. + if utxoEntry.Amount() != test.entry.Amount() { + t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + "amounts: got %d, want %d", i, test.name, + utxoEntry.Amount(), test.entry.Amount(), + ) + continue + } + if !bytes.Equal(utxoEntry.PkScript(), test.entry.PkScript()) { + t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + "scripts: got %x, want %x", i, test.name, + utxoEntry.PkScript(), test.entry.PkScript(), + ) + continue + } + if utxoEntry.BlockHeight() != test.entry.BlockHeight() { + t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + "block height: got %d, want %d", i, test.name, + utxoEntry.BlockHeight(), test.entry.BlockHeight(), + ) + continue + } + if utxoEntry.IsCoinBase() != test.entry.IsCoinBase() { + t.Errorf("deserializeUtxoEntry #%d (%s) mismatched "+ + "coinbase flag: got %v, want %v", i, test.name, + utxoEntry.IsCoinBase(), test.entry.IsCoinBase(), + ) + continue + } + } +} + +// TestUtxoEntryHeaderCodeErrors performs negative tests against unspent transaction output header codes to ensure error +// paths work as expected. +func TestUtxoEntryHeaderCodeErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry *UtxoEntry + // code uint64 + errType error + }{ + { + name: "Force assertion due to spent output", + entry: &UtxoEntry{packedFlags: tfSpent}, + errType: AssertError(""), + }, + } + for _, test := range tests { + // Ensure the expected error type is returned and the code is 0. + code, e := utxoEntryHeaderCode(test.entry) + if reflect.TypeOf(e) != reflect.TypeOf(test.errType) { + t.Errorf("utxoEntryHeaderCode (%s): expected error "+ + "type does not match - got %T, want %T", + test.name, e, test.errType, + ) + continue + } + if code != 0 { + t.Errorf("utxoEntryHeaderCode (%s): unexpected code "+ + "on error - got %d, want 0", test.name, code, + ) + continue + } + } +} + +// TestUtxoEntryDeserializeErrors performs negative tests against deserializing unspent transaction outputs to ensure +// error paths work as expected. +func TestUtxoEntryDeserializeErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + serialized []byte + errType error + }{ + { + name: "no data after header code", + serialized: hexToBytes("02"), + errType: errDeserialize(""), + }, + { + name: "incomplete compressed txout", + serialized: hexToBytes("0232"), + errType: errDeserialize(""), + }, + } + for _, test := range tests { + // Ensure the expected error type is returned and the returned entry is nil. + entry, e := deserializeUtxoEntry(test.serialized) + if reflect.TypeOf(e) != reflect.TypeOf(test.errType) { + t.Errorf("deserializeUtxoEntry (%s): expected error "+ + "type does not match - got %T, want %T", + test.name, e, test.errType, + ) + continue + } + if entry != nil { + t.Errorf("deserializeUtxoEntry (%s): returned entry "+ + "is not nil", test.name, + ) + continue + } + } +} + +// // TestBestChainStateSerialization ensures serializing and deserializing the best chain state works as expected. +// func TestBestChainStateSerialization(// t *testing.T) { +// t.Parallel() +// workSum := new(big.Int) +// tests := []struct { +// name string +// state bestChainState +// serialized []byte +// }{ +// { +// name: "genesis", +// state: bestChainState{ +// hash: *newHashFromStr("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"), +// height: 0, +// totalTxns: 1, +// workSum: func() *big.Int { +// workSum.Add(workSum, CalcWork(486604799, 0, 2)) +// return new(big.Int).Set(workSum) +// }(), +// // 0x0100010001 +// }, +// serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000000000000100000000000000050000000100010001"), +// }, +// { +// name: "block 1", +// state: bestChainState{ +// hash: *newHashFromStr("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"), +// height: 1, +// totalTxns: 2, +// workSum: func() *big.Int { +// workSum.Add(workSum, CalcWork(486604799, 1, 2)) +// return new(big.Int).Set(workSum) +// }(), +// // 0x0200020002 +// }, +// serialized: hexToBytes("4860eb18bf1b1620e37e9490fc8a427514416fd75159ab86688e9a8300000000010000000200000000000000050000000200020002"), +// }, +// } +// for i, test := range tests { +// // Ensure the state serializes to the expected value. +// gotBytes := serializeBestChainState(test.state) +// if !bytes.Equal(gotBytes, test.serialized) { +// t.Errorf("serializeBestChainState #%d (%s): mismatched "+ +// "bytes - got %x, want %x", i, test.name, +// gotBytes, test.serialized) +// continue +// } +// // Ensure the serialized bytes are decoded back to the expected state. +// state, e := deserializeBestChainState(test.serialized) +// if e != nil { +// t.Errorf("deserializeBestChainState #%d (%s) "+ +// "unexpected error: %v", i, test.name, e) +// continue +// } +// if !reflect.DeepEqual(state, test.state) { +// t.Errorf("deserializeBestChainState #%d (%s) "+ +// "mismatched state - got %v, want %v", i, +// test.name, state, test.state) +// continue +// } +// } +// } + +// TestBestChainStateDeserializeErrors performs negative tests against deserializing the chain state to ensure error +// paths work as expected. +func TestBestChainStateDeserializeErrors(t *testing.T) { + t.Parallel() + tests := []struct { + name string + serialized []byte + errType error + }{ + { + name: "nothing serialized", + serialized: hexToBytes(""), + errType: database.DBError{ErrorCode: database.ErrCorruption}, + }, + { + name: "short data in hash", + serialized: hexToBytes("0000"), + errType: database.DBError{ErrorCode: database.ErrCorruption}, + }, + { + name: "short data in work sum", + serialized: hexToBytes("6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000001000000000000000500000001000100"), + errType: database.DBError{ErrorCode: database.ErrCorruption}, + }, + } + for _, test := range tests { + // Ensure the expected error type and code is returned. + _, e := deserializeBestChainState(test.serialized) + if reflect.TypeOf(e) != reflect.TypeOf(test.errType) { + t.Errorf("deserializeBestChainState (%s): expected "+ + "error type does not match - got %T, want %T", + test.name, e, test.errType, + ) + continue + } + if derr, ok := e.(database.DBError); ok { + tderr := test.errType.(database.DBError) + if derr.ErrorCode != tderr.ErrorCode { + t.Errorf("deserializeBestChainState (%s): "+ + "wrong error code got: %v, want: %v", + test.name, derr.ErrorCode, + tderr.ErrorCode, + ) + continue + } + } + } +} diff --git a/pkg/blockchain/chainview.go b/pkg/blockchain/chainview.go new file mode 100644 index 0000000..1118ea6 --- /dev/null +++ b/pkg/blockchain/chainview.go @@ -0,0 +1,335 @@ +package blockchain + +import ( + "sync" +) + +// approxNodesPerWeek is an approximation of the number of new blocks there are in a week on average. +const approxNodesPerWeek = 7 * 24 * 60 * 60 / 12 + +// log2FloorMasks defines the masks to use when quickly calculating floor(log2(x)) in a constant log2(32) = 5 steps, +// where x is a uint32, using shifts. They are derived from (2^(2^x) - 1) * (2^(2^x)), for x in 4..0. +var log2FloorMasks = []uint32{0xffff0000, 0xff00, 0xf0, 0xc, 0x2} + +// fastLog2Floor calculates and returns floor(log2(x)) in a constant 5 steps. +func fastLog2Floor(n uint32) uint8 { + rv := uint8(0) + exponent := uint8(16) + for i := 0; i < 5; i++ { + if n&log2FloorMasks[i] != 0 { + rv += exponent + n >>= exponent + } + exponent >>= 1 + } + return rv +} + +// chainView provides a flat view of a specific branch of the block chain from its tip back to the genesis block and +// provides various convenience functions for comparing chains. For example, assume a block chain with a side chain as +// depicted below: +// +// genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 +// \-> 4a -> 5a -> 6a +// +// The chain view for the branch ending in 6a consists of: +// +// genesis -> 1 -> 2 -> 3 -> 4a -> 5a -> 6a +type chainView struct { + mtx sync.Mutex + nodes []*BlockNode +} + +// newChainView returns a new chain view for the given tip block node. Passing nil as the tip will result in a chain +// view that is not initialized. The tip can be updated at any time via the setTip function. +func newChainView(tip *BlockNode) *chainView { + // The mutex is intentionally not held since this is a constructor. + var c chainView + c.setTip(tip) + return &c +} + +// genesis returns the genesis block for the chain view. This only differs from the exported version in that it is up to +// the caller to ensure the lock is held. This function MUST be called with the view mutex locked (for reads). +func (c *chainView) genesis() *BlockNode { + if len(c.nodes) == 0 { + return nil + } + return c.nodes[0] +} + +// Genesis returns the genesis block for the chain view. This function is safe for concurrent access. +func (c *chainView) Genesis() *BlockNode { + c.mtx.Lock() + genesis := c.genesis() + c.mtx.Unlock() + return genesis +} + +// tip returns the current tip block node for the chain view. It will return nil if there is no tip. This only differs +// from the exported version in that it is up to the caller to ensure the lock is held. This function MUST be called +// with the view mutex locked (for reads). +func (c *chainView) tip() *BlockNode { + if len(c.nodes) == 0 { + return nil + } + return c.nodes[len(c.nodes)-1] +} + +// Tip returns the current tip block node for the chain view. It will return nil if there is no tip. This function is +// safe for concurrent access. +func (c *chainView) Tip() *BlockNode { + c.mtx.Lock() + tip := c.tip() + c.mtx.Unlock() + return tip +} + +// setTip sets the chain view to use the provided block node as the current tip and ensures the view is consistent by +// populating it with the nodes obtained by walking backwards all the way to genesis block as necessary. Further calls +// will only perform the minimum work needed, so switching between chain tips is efficient. This only differs from the +// exported version in that it is up to the caller to ensure the lock is held. This function MUST be called with the +// view mutex locked (for writes). +func (c *chainView) setTip(node *BlockNode) { + if node == nil { + // Keep the backing array around for potential future use. + c.nodes = c.nodes[:0] + return + } + // Create or resize the slice that will hold the block nodes to the provided tip height. When creating the slice, it + // is created with some additional capacity for the underlying array as append would do in order to reduce overhead + // when extending the chain later. As long as the underlying array already has enough capacity, simply expand or + // contract the slice accordingly. The additional capacity is chosen such that the array should only have to be + // extended about once a week. + needed := node.height + 1 + if int32(cap(c.nodes)) < needed { + nodes := make([]*BlockNode, needed, needed+approxNodesPerWeek) + copy(nodes, c.nodes) + c.nodes = nodes + } else { + prevLen := int32(len(c.nodes)) + c.nodes = c.nodes[0:needed] + for i := prevLen; i < needed; i++ { + c.nodes[i] = nil + } + } + for c.nodes[node.height] != node { + c.nodes[node.height] = node + node = node.parent + if node == nil { + break + } + } +} + +// SetTip sets the chain view to use the provided block node as the current tip and ensures the view is consistent by +// populating it with the nodes obtained by walking backwards all the way to genesis block as necessary. Further calls +// will only perform the minimum work needed, so switching between chain tips is efficient. This function is safe for +// concurrent access. +func (c *chainView) SetTip(node *BlockNode) { + c.mtx.Lock() + c.setTip(node) + c.mtx.Unlock() +} + +// height returns the height of the tip of the chain view. It will return -1 if there is no tip (which only happens if +// the chain view has not been initialized). This only differs from the exported version in that it is up to the caller +// to ensure the lock is held. This function MUST be called with the view mutex locked (for reads). +func (c *chainView) height() int32 { + return int32(len(c.nodes) - 1) +} + +// Height returns the height of the tip of the chain view. It will return -1 if there is no tip (which only happens if +// the chain view has not been initialized). This function is safe for concurrent access. +func (c *chainView) Height() int32 { + c.mtx.Lock() + height := c.height() + c.mtx.Unlock() + return height +} + +// nodeByHeight returns the block node at the specified height. Nil will be returned if the height does not exist. This +// only differs from the exported version in that it is up to the caller to ensure the lock is held. This function MUST +// be called with the view mutex locked (for reads). +func (c *chainView) nodeByHeight(height int32) *BlockNode { + if height < 0 || height >= int32(len(c.nodes)) { + return nil + } + return c.nodes[height] +} + +// NodeByHeight returns the block node at the specified height. Nil will be returned if the height does not exist. This +// function is safe for concurrent access. +func (c *chainView) NodeByHeight(height int32) *BlockNode { + c.mtx.Lock() + node := c.nodeByHeight(height) + c.mtx.Unlock() + return node +} + +// Equals returns whether or not two chain views are the same. Uninitialized views (tip set to nil) are considered +// equal. This function is safe for concurrent access. +func (c *chainView) Equals(other *chainView) bool { + c.mtx.Lock() + other.mtx.Lock() + equals := len(c.nodes) == len(other.nodes) && c.tip() == other.tip() + other.mtx.Unlock() + c.mtx.Unlock() + return equals +} + +// contains returns whether or not the chain view contains the passed block node. This only differs from the exported +// version in that it is up to the caller to ensure the lock is held. This function MUST be called with the view mutex +// locked (for reads). +func (c *chainView) contains(node *BlockNode) bool { + return c.nodeByHeight(node.height) == node +} + +// Contains returns whether or not the chain view contains the passed block node. +// +// This function is safe for concurrent access. +func (c *chainView) Contains(node *BlockNode) bool { + c.mtx.Lock() + contains := c.contains(node) + c.mtx.Unlock() + return contains +} + +// next returns the successor to the provided node for the chain view. It will return nil if there is no successor or +// the provided node is not part of the view. This only differs from the exported version in that it is up to the caller +// to ensure the lock is held. See the comment on the exported function for more details. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) next(node *BlockNode) *BlockNode { + if node == nil || !c.contains(node) { + return nil + } + return c.nodeByHeight(node.height + 1) +} + +// Next returns the successor to the provided node for the chain view. It will return nil if there is no successfor or +// the provided node is not part of the view. For example, assume a block chain with a side chain as depicted below: +// +// genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 +// \-> 4a -> 5a -> 6a +// Further, assume the view is for the longer chain depicted above. That is to say it consists of: +// +// genesis -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 +// +// Invoking this function with block node 5 would return block node 6 while invoking it with block node 5a would return +// nil since that node is not part of the view. This function is safe for concurrent access. +func (c *chainView) Next(node *BlockNode) *BlockNode { + c.mtx.Lock() + next := c.next(node) + c.mtx.Unlock() + return next +} + +// findFork returns the final common block between the provided node and the the chain view. It will return nil if there +// is no common block. This only differs from the exported version in that it is up to the caller to ensure the lock is +// held. See the exported FindFork comments for more details. This function MUST be called with the view mutex locked +// (for reads). +func (c *chainView) findFork(node *BlockNode) *BlockNode { + // No fork point for node that doesn't exist. + if node == nil { + return nil + } + // When the height of the passed node is higher than the height of the tip of the current chain view, walk backwards + // through the nodes of the other chain until the heights match (or there or no more nodes in which case there is no + // common node between the two). NOTE: This isn't strictly necessary as the following section will find the node as + // well, however, it is more efficient to avoid the contains check since it is already known that the common node + // can't possibly be past the end of the current chain view. It also allows this code to take advantage of any + // potential future optimizations to the Ancestor function such as using an O(log n) skip list. + chainHeight := c.height() + if node.height > chainHeight { + node = node.Ancestor(chainHeight) + } + // Walk the other chain backwards as long as the current one does not contain the node or there are no more nodes in + // which case there is no common node between the two. + for node != nil && !c.contains(node) { + node = node.parent + } + return node +} + +// FindFork returns the final common block between the provided node and the the chain view. It will return nil if there +// is no common block. For example, assume a block chain with a side chain as depicted below: +// +// genesis -> 1 -> 2 -> ... -> 5 -> 6 -> 7 -> 8 +// \-> 6a -> 7a +// Further, assume the view is for the longer chain depicted above. That is to say it consists of: +// +// genesis -> 1 -> 2 -> ... -> 5 -> 6 -> 7 -> 8. +// +// Invoking this function with block node 7a would return block node 5 while invoking it with block node 7 would return +// itself since it is already part of the branch formed by the view. This function is safe for concurrent access. +func (c *chainView) FindFork(node *BlockNode) *BlockNode { + c.mtx.Lock() + fork := c.findFork(node) + c.mtx.Unlock() + return fork +} + +// blockLocator returns a block locator for the passed block node. The passed node can be nil in which case the block +// locator for the current tip associated with the view will be returned. This only differs from the exported version in +// that it is up to the caller to ensure the lock is held. See the exported BlockLocator function comments for more +// details. +// +// This function MUST be called with the view mutex locked (for reads). +func (c *chainView) blockLocator(node *BlockNode) BlockLocator { + // Use the current tip if requested. + if node == nil { + node = c.tip() + } + if node == nil { + return nil + } + // Calculate the max number of entries that will ultimately be in the block locator. See the description of the + // algorithm for how these numbers are derived. + var maxEntries uint8 + if node.height <= 12 { + maxEntries = uint8(node.height) + 1 + } else { + // Requested hash itself + previous 10 entries + genesis block. Then floor(log2(height-10)) entries for the skip + // portion. + adjustedHeight := uint32(node.height) - 10 + maxEntries = 12 + fastLog2Floor(adjustedHeight) + } + locator := make(BlockLocator, 0, maxEntries) + step := int32(1) + for node != nil { + locator = append(locator, &node.hash) + // Nothing more to add once the genesis block has been added. + if node.height == 0 { + break + } + // Calculate height of previous node to include ensuring the final node is the genesis block. + height := node.height - step + if height < 0 { + height = 0 + } + // When the node is in the current chain view, all of its ancestors must be too, so use a much faster O(1) + // lookup in that case. Otherwise, fall back to walking backwards through the nodes of the other chain to the + // correct ancestor. + if c.contains(node) { + node = c.nodes[height] + } else { + node = node.Ancestor(height) + } + // Once 11 entries have been included, start doubling the distance between included hashes. + if len(locator) > 10 { + step *= 2 + } + } + return locator +} + +// BlockLocator returns a block locator for the passed block node. The passed node can be nil in which case the block +// locator for the current tip associated with the view will be returned. See the BlockLocator type for details on the +// algorithm used to create a block locator. This function is safe for concurrent access. +func (c *chainView) BlockLocator(node *BlockNode) BlockLocator { + c.mtx.Lock() + locator := c.blockLocator(node) + c.mtx.Unlock() + return locator +} diff --git a/pkg/blockchain/chainview_test.go b/pkg/blockchain/chainview_test.go new file mode 100644 index 0000000..49ab88e --- /dev/null +++ b/pkg/blockchain/chainview_test.go @@ -0,0 +1,458 @@ +package blockchain + +import ( + "fmt" + "math/rand" + "reflect" + "testing" + + "github.com/p9c/p9/pkg/wire" +) + +// testNoncePrng provides a deterministic prng for the nonce in generated fake nodes. The ensures that the node have +// unique hashes. +var testNoncePrng = rand.New(rand.NewSource(0)) + +// chainedNodes returns the specified number of nodes constructed such that each subsequent node points to the previous +// one to create a chain. The first node will point to the passed parent which can be nil if desired. +func chainedNodes(parent *BlockNode, numNodes int) []*BlockNode { + nodes := make([]*BlockNode, numNodes) + tip := parent + for i := 0; i < numNodes; i++ { + // This is invalid, but all that is needed is enough to get the synthetic tests to work. + header := wire.BlockHeader{Nonce: testNoncePrng.Uint32()} + if tip != nil { + header.PrevBlock = tip.hash + } + nodes[i] = NewBlockNode(&header, tip) + tip = nodes[i] + } + return nodes +} + +// String returns the block node as a human-readable name. +func (node BlockNode) String() string { + return fmt.Sprintf("%s(%d)", node.hash, node.height) +} + +// tstTip is a convenience function to grab the tip of a chain of block nodes created via chainedNodes. +func tstTip(nodes []*BlockNode) *BlockNode { + return nodes[len(nodes)-1] +} + +// locatorHashes is a convenience function that returns the hashes for all of the passed indexes of the provided nodes. +// It is used to construct expected block locators in the tests. +func locatorHashes(nodes []*BlockNode, indexes ...int) BlockLocator { + hashes := make(BlockLocator, 0, len(indexes)) + for _, idx := range indexes { + hashes = append(hashes, &nodes[idx].hash) + } + return hashes +} + +// zipLocators is a convenience function that returns a single block locator given a variable number of them and is used +// in the tests. +func zipLocators(locators ...BlockLocator) BlockLocator { + var hashes BlockLocator + for _, locator := range locators { + hashes = append(hashes, locator...) + } + return hashes +} + +// TestChainView ensures all of the exported functionality of chain views works as intended with the exception of some +// special cases which are handled in other tests. +func TestChainView(t *testing.T) { + // Construct a synthetic block index consisting of the following structure. + // + // 0 -> 1 -> 2 -> 3 -> 4 + // \-> 2a -> 3a -> 4a -> 5a -> 6a -> 7a -> ... -> 26a + // \-> 3a'-> 4a' -> 5a' + branch0Nodes := chainedNodes(nil, 5) + branch1Nodes := chainedNodes(branch0Nodes[1], 25) + branch2Nodes := chainedNodes(branch1Nodes[0], 3) + tip := tstTip + tests := []struct { + name string + // active view + view *chainView + // expected genesis block of active view + genesis *BlockNode + // expected tip of active view + tip *BlockNode + // side chain view + side *chainView + // expected tip of side chain view + sideTip *BlockNode + // expected fork node + fork *BlockNode + // expected nodes in active view + contains []*BlockNode + // expected nodes NOT in active view + noContains []*BlockNode + // view expected equal to active view + equal *chainView + // view expected NOT equal to active + unequal *chainView + // expected locator for active view tip + locator BlockLocator + }{ + { + // Create a view for branch 0 as the active chain and another view for branch 1 as the side chain. + name: "chain0-chain1", + view: newChainView(tip(branch0Nodes)), + genesis: branch0Nodes[0], + tip: tip(branch0Nodes), + side: newChainView(tip(branch1Nodes)), + sideTip: tip(branch1Nodes), + fork: branch0Nodes[1], + contains: branch0Nodes, + noContains: branch1Nodes, + equal: newChainView(tip(branch0Nodes)), + unequal: newChainView(tip(branch1Nodes)), + locator: locatorHashes(branch0Nodes, 4, 3, 2, 1, 0), + }, + { + // Create a view for branch 1 as the active chain and another view for branch 2 as the side chain. + name: "chain1-chain2", + view: newChainView(tip(branch1Nodes)), + genesis: branch0Nodes[0], + tip: tip(branch1Nodes), + side: newChainView(tip(branch2Nodes)), + sideTip: tip(branch2Nodes), + fork: branch1Nodes[0], + contains: branch1Nodes, + noContains: branch2Nodes, + equal: newChainView(tip(branch1Nodes)), + unequal: newChainView(tip(branch2Nodes)), + locator: zipLocators( + locatorHashes(branch1Nodes, 24, 23, 22, 21, 20, + 19, 18, 17, 16, 15, 14, 13, 11, 7, + ), + locatorHashes(branch0Nodes, 1, 0), + ), + }, + { + // Create a view for branch 2 as the active chain and another view for branch 0 as the side chain. + name: "chain2-chain0", + view: newChainView(tip(branch2Nodes)), + genesis: branch0Nodes[0], + tip: tip(branch2Nodes), + side: newChainView(tip(branch0Nodes)), + sideTip: tip(branch0Nodes), + fork: branch0Nodes[1], + contains: branch2Nodes, + noContains: branch0Nodes[2:], + equal: newChainView(tip(branch2Nodes)), + unequal: newChainView(tip(branch0Nodes)), + locator: zipLocators( + locatorHashes(branch2Nodes, 2, 1, 0), + locatorHashes(branch1Nodes, 0), + locatorHashes(branch0Nodes, 1, 0), + ), + }, + } +testLoop: + for _, test := range tests { + // Ensure the active and side chain heights are the expected values. + if test.view.Height() != test.tip.height { + t.Errorf("%s: unexpected active view height -- got "+ + "%d, want %d", test.name, test.view.Height(), + test.tip.height, + ) + continue + } + if test.side.Height() != test.sideTip.height { + t.Errorf("%s: unexpected side view height -- got %d, "+ + "want %d", test.name, test.side.Height(), + test.sideTip.height, + ) + continue + } + // Ensure the active and side chain genesis block is the expected value. + if test.view.Genesis() != test.genesis { + t.Errorf("%s: unexpected active view genesis -- got "+ + "%v, want %v", test.name, test.view.Genesis(), + test.genesis, + ) + continue + } + if test.side.Genesis() != test.genesis { + t.Errorf("%s: unexpected side view genesis -- got %v, want %v", test.name, test.view.Genesis(), + test.genesis, + ) + continue + } + // Ensure the active and side chain tips are the expected nodes. + if test.view.Tip() != test.tip { + t.Errorf("%s: unexpected active view tip -- got %v, "+ + "want %v", test.name, test.view.Tip(), test.tip, + ) + continue + } + if test.side.Tip() != test.sideTip { + t.Errorf("%s: unexpected active view tip -- got %v, "+ + "want %v", test.name, test.side.Tip(), + test.sideTip, + ) + continue + } + // Ensure that regardless of the order the two chains are compared they both return the expected fork point. + forkNode := test.view.FindFork(test.side.Tip()) + if forkNode != test.fork { + t.Errorf("%s: unexpected fork node (view, side) -- "+ + "got %v, want %v", test.name, forkNode, + test.fork, + ) + continue + } + forkNode = test.side.FindFork(test.view.Tip()) + if forkNode != test.fork { + t.Errorf("%s: unexpected fork node (side, view) -- "+ + "got %v, want %v", test.name, forkNode, + test.fork, + ) + continue + } + // Ensure that the fork point for a node that is already part of the chain view is the node itself. + forkNode = test.view.FindFork(test.view.Tip()) + if forkNode != test.view.Tip() { + t.Errorf("%s: unexpected fork node (view, tip) -- "+ + "got %v, want %v", test.name, forkNode, + test.view.Tip(), + ) + continue + } + // Ensure all expected nodes are contained in the active view. + for _, node := range test.contains { + if !test.view.Contains(node) { + t.Errorf("%s: expected %v in active view", + test.name, node, + ) + continue testLoop + } + } + // Ensure all nodes from side chain view are NOT contained in the active view. + for _, node := range test.noContains { + if test.view.Contains(node) { + t.Errorf("%s: unexpected %v in active view", + test.name, node, + ) + continue testLoop + } + } + // Ensure equality of different views into the same chain works as intended. + if !test.view.Equals(test.equal) { + t.Errorf("%s: unexpected unequal views", test.name) + continue + } + if test.view.Equals(test.unequal) { + t.Errorf("%s: unexpected equal views", test.name) + continue + } + // Ensure all nodes contained in the view return the expected next node. + for i, node := range test.contains { + // Final node expects nil for the next node. + var expected *BlockNode + if i < len(test.contains)-1 { + expected = test.contains[i+1] + } + if next := test.view.Next(node); next != expected { + t.Errorf("%s: unexpected next node -- got %v, "+ + "want %v", test.name, next, expected, + ) + continue testLoop + } + } + // Ensure nodes that are not contained in the view do not produce a successor node. + for _, node := range test.noContains { + if next := test.view.Next(node); next != nil { + t.Errorf("%s: unexpected next node -- got %v, "+ + "want nil", test.name, next, + ) + continue testLoop + } + } + // Ensure all nodes contained in the view can be retrieved by height. + for _, wantNode := range test.contains { + node := test.view.NodeByHeight(wantNode.height) + if node != wantNode { + t.Errorf("%s: unexpected node for height %d -- "+ + "got %v, want %v", test.name, + wantNode.height, node, wantNode, + ) + continue testLoop + } + } + // Ensure the block locator for the tip of the active view consists of the expected hashes. + locator := test.view.BlockLocator(test.view.tip()) + if !reflect.DeepEqual(locator, test.locator) { + t.Errorf("%s: unexpected locator -- got %v, want %v", + test.name, locator, test.locator, + ) + continue + } + } +} + +// TestChainViewForkCorners ensures that finding the fork between two chains works in some corner cases such as when the +// two chains have completely unrelated histories. +func TestChainViewForkCorners(t *testing.T) { + // Construct two unrelated single branch synthetic block indexes. + branchNodes := chainedNodes(nil, 5) + unrelatedBranchNodes := chainedNodes(nil, 7) + // Create chain views for the two unrelated histories. + view1 := newChainView(tstTip(branchNodes)) + view2 := newChainView(tstTip(unrelatedBranchNodes)) + // Ensure attempting to find a fork point with a node that doesn't exist doesn't produce a node. + if fork := view1.FindFork(nil); fork != nil { + t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork) + } + // Ensure attempting to find a fork point in two chain views with totally unrelated histories doesn't produce a node. + for _, node := range branchNodes { + if fork := view2.FindFork(node); fork != nil { + t.Fatalf("FindFork: unexpected fork -- got %v, want nil", + fork, + ) + } + } + for _, node := range unrelatedBranchNodes { + if fork := view1.FindFork(node); fork != nil { + t.Fatalf("FindFork: unexpected fork -- got %v, want nil", + fork, + ) + } + } +} + +// TestChainViewSetTip ensures changing the tip works as intended including capacity changes. +func TestChainViewSetTip(t *testing.T) { + // Construct a synthetic block index consisting of the following structure. + // + // 0 -> 1 -> 2 -> 3 -> 4 + // \-> 2a -> 3a -> 4a -> 5a -> 6a -> 7a -> ... -> 26a + branch0Nodes := chainedNodes(nil, 5) + branch1Nodes := chainedNodes(branch0Nodes[1], 25) + tip := tstTip + tests := []struct { + name string + view *chainView // active view + tips []*BlockNode // tips to set + contains [][]*BlockNode // expected nodes in view for each tip + }{ + { + // Create an empty view and set the tip to increasingly longer chains. + name: "increasing", + view: newChainView(nil), + tips: []*BlockNode{tip(branch0Nodes), tip(branch1Nodes)}, + contains: [][]*BlockNode{branch0Nodes, branch1Nodes}, + }, + { + // Create a view with a longer chain and set the tip to increasingly shorter chains. + name: "decreasing", + view: newChainView(tip(branch1Nodes)), + tips: []*BlockNode{tip(branch0Nodes), nil}, + contains: [][]*BlockNode{branch0Nodes, nil}, + }, + { + // Create a view with a shorter chain and set the tip to a longer chain followed by setting it back to the + // shorter chain. + name: "small-large-small", + view: newChainView(tip(branch0Nodes)), + tips: []*BlockNode{tip(branch1Nodes), tip(branch0Nodes)}, + contains: [][]*BlockNode{branch1Nodes, branch0Nodes}, + }, + { + // Create a view with a longer chain and set the tip to a smaller chain followed by setting it back to the + // longer chain. + name: "large-small-large", + view: newChainView(tip(branch1Nodes)), + tips: []*BlockNode{tip(branch0Nodes), tip(branch1Nodes)}, + contains: [][]*BlockNode{branch0Nodes, branch1Nodes}, + }, + } +testLoop: + for _, test := range tests { + for i, tip := range test.tips { + // Ensure the view tip is the expected node. + test.view.SetTip(tip) + if test.view.Tip() != tip { + t.Errorf("%s: unexpected view tip -- got %v, "+ + "want %v", test.name, test.view.Tip(), + tip, + ) + continue testLoop + } + // Ensure all expected nodes are contained in the view. + for _, node := range test.contains[i] { + if !test.view.Contains(node) { + t.Errorf("%s: expected %v in active view", + test.name, node, + ) + continue testLoop + } + } + } + } +} + +// TestChainViewNil ensures that creating and accessing a nil chain view behaves as expected. +func TestChainViewNil(t *testing.T) { + // Ensure two unininitialized views are considered equal. + view := newChainView(nil) + if !view.Equals(newChainView(nil)) { + t.Fatal("uninitialized nil views unequal") + } + // Ensure the genesis of an uninitialized view does not produce a node. + if genesis := view.Genesis(); genesis != nil { + t.Fatalf("Genesis: unexpected genesis -- got %v, want nil", + genesis, + ) + } + // Ensure the tip of an uninitialized view does not produce a node. + if tip := view.Tip(); tip != nil { + t.Fatalf("Tip: unexpected tip -- got %v, want nil", tip) + } + // Ensure the height of an uninitialized view is the expected value. + if height := view.Height(); height != -1 { + t.Fatalf("Height: unexpected height -- got %d, want -1", height) + } + // Ensure attempting to get a node for a height that does not exist does not produce a node. + if node := view.NodeByHeight(10); node != nil { + t.Fatalf("NodeByHeight: unexpected node -- got %v, want nil", node) + } + // Ensure an uninitialized view does not report it contains nodes. + fakeNode := chainedNodes(nil, 1)[0] + if view.Contains(fakeNode) { + t.Fatalf("Contains: view claims it contains node %v", fakeNode) + } + // Ensure the next node for a node that does not exist does not produce a node. + if next := view.Next(nil); next != nil { + t.Fatalf("Next: unexpected next node -- got %v, want nil", next) + } + // Ensure the next node for a node that exists does not produce a node. + if next := view.Next(fakeNode); next != nil { + t.Fatalf("Next: unexpected next node -- got %v, want nil", next) + } + // Ensure attempting to find a fork point with a node that doesn't exist doesn't produce a node. + if fork := view.FindFork(nil); fork != nil { + t.Fatalf("FindFork: unexpected fork -- got %v, want nil", fork) + } + // Ensure attempting to get a block locator for the tip doesn't produce one since the tip is nil. + if locator := view.BlockLocator(nil); locator != nil { + t.Fatalf("BlockLocator: unexpected locator -- got %v, want nil", + locator, + ) + } + // Ensure attempting to get a block locator for a node that exists still works as intended. + branchNodes := chainedNodes(nil, 50) + wantLocator := locatorHashes(branchNodes, 49, 48, 47, 46, 45, 44, 43, + 42, 41, 40, 39, 38, 36, 32, 24, 8, 0, + ) + locator := view.BlockLocator(tstTip(branchNodes)) + if !reflect.DeepEqual(locator, wantLocator) { + t.Fatalf("BlockLocator: unexpected locator -- got %v, want %v", + locator, wantLocator, + ) + } +} diff --git a/pkg/blockchain/checkpoints.go b/pkg/blockchain/checkpoints.go new file mode 100644 index 0000000..2f85855 --- /dev/null +++ b/pkg/blockchain/checkpoints.go @@ -0,0 +1,225 @@ +package blockchain + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + "time" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" +) + +// CheckpointConfirmations is the number of blocks before the end of the current best block chain that a good checkpoint +// candidate must be. TODO: review this and add it to the fork spec +const CheckpointConfirmations = 2016 + +// newHashFromStr converts the passed big-endian hex string into a chainhash.Hash. +// +// It only differs from the one available in chainhash in that it ignores the error since it will only (and must only) +// be called with hard-coded, and therefore known good, hashes. +func newHashFromStr(hexStr string) *chainhash.Hash { + hash, _ := chainhash.NewHashFromStr(hexStr) + return hash +} + +// Checkpoints returns a slice of checkpoints ( regardless of whether they are already known). When there are no +// checkpoints for the chain, it will return nil. +// +// This function is safe for concurrent access. +func (b *BlockChain) Checkpoints() []chaincfg.Checkpoint { + return b.checkpoints +} + +// HasCheckpoints returns whether this BlockChain has checkpoints defined. +// +// This function is safe for concurrent access. +func (b *BlockChain) HasCheckpoints() bool { + return len(b.checkpoints) > 0 +} + +// LatestCheckpoint returns the most recent checkpoint (regardless of whether it is already known). When there are no +// defined checkpoints for the active chain instance, it will return nil. +// +// This function is safe for concurrent access. +func (b *BlockChain) LatestCheckpoint() *chaincfg.Checkpoint { + if !b.HasCheckpoints() { + return nil + } + return &b.checkpoints[len(b.checkpoints)-1] +} + +// verifyCheckpoint returns whether the passed block height and hash combination match the checkpoint data. It also +// returns true if there is no checkpoint data for the passed block height. +func (b *BlockChain) verifyCheckpoint(height int32, hash *chainhash.Hash) bool { + if !b.HasCheckpoints() { + return true + } + // Nothing to check if there is no checkpoint data for the block height. + checkpoint, exists := b.checkpointsByHeight[height] + if !exists { + return true + } + if !checkpoint.Hash.IsEqual(hash) { + return false + } + I.F("Verified checkpoint at height %d/block %s", checkpoint.Height, + checkpoint.Hash, + ) + return true +} + +// findPreviousCheckpoint finds the most recent checkpoint that is already available in the downloaded portion of the +// block chain and returns the associated block node. It returns nil if a checkpoint can't be found ( this should really +// only happen for blocks before the first checkpoint). +// +// This function MUST be called with the chain lock held (for reads). +func (b *BlockChain) findPreviousCheckpoint() (*BlockNode, error) { + if !b.HasCheckpoints() { + return nil, nil + } + // Perform the initial search to find and cache the latest known checkpoint if the best chain is not known yet or we + // haven't already previously searched. + checkpoints := b.checkpoints + numCheckpoints := len(checkpoints) + if b.checkpointNode == nil && b.nextCheckpoint == nil { + // Loop backwards through the available checkpoints to find one that is already available. + for i := numCheckpoints - 1; i >= 0; i-- { + node := b.Index.LookupNode(checkpoints[i].Hash) + if node == nil || !b.BestChain.Contains(node) { + continue + } + // Checkpoint found. Cache it for future lookups and set the next expected checkpoint accordingly. + b.checkpointNode = node + if i < numCheckpoints-1 { + b.nextCheckpoint = &checkpoints[i+1] + } + return b.checkpointNode, nil + } + // No known latest checkpoint. This will only happen on blocks before the first known checkpoint. So, set the + // next expected checkpoint to the first checkpoint and return the fact there is no latest known checkpoint + // block. + b.nextCheckpoint = &checkpoints[0] + return nil, nil + } + // At this point we've already searched for the latest known checkpoint, so when there is no next checkpoint, the + // current checkpoint lockin will always be the latest known + // checkpoint. + if b.nextCheckpoint == nil { + return b.checkpointNode, nil + } + // When there is a next checkpoint and the height of the current best chain does not exceed it, the current + // checkpoint lockin is still the latest known checkpoint. + if b.BestChain.Tip().height < b.nextCheckpoint.Height { + return b.checkpointNode, nil + } + // We've reached or exceeded the next checkpoint height. + // + // Note that once a checkpoint lockin has been reached, forks are prevented from any blocks before the checkpoint, + // so we don't have to worry about the checkpoint going away out from under us due to a chain reorganize. + // + // Cache the latest known checkpoint for future lookups. + // + // Note that if this lookup fails something is very wrong since the chain has already passed the checkpoint which + // was verified as accurate before inserting it. + checkpointNode := b.Index.LookupNode(b.nextCheckpoint.Hash) + if checkpointNode == nil { + return nil, AssertError(fmt.Sprintf("findPreviousCheckpoint "+ + "failed lookup of known good block node %s", + b.nextCheckpoint.Hash, + ), + ) + } + b.checkpointNode = checkpointNode + // Set the next expected checkpoint. + checkpointIndex := -1 + for i := numCheckpoints - 1; i >= 0; i-- { + if checkpoints[i].Hash.IsEqual(b.nextCheckpoint.Hash) { + checkpointIndex = i + break + } + } + b.nextCheckpoint = nil + if checkpointIndex != -1 && checkpointIndex < numCheckpoints-1 { + b.nextCheckpoint = &checkpoints[checkpointIndex+1] + } + return b.checkpointNode, nil +} + +// isNonstandardTransaction determines whether a transaction contains any scripts which are not one of the standard +// types. +func isNonstandardTransaction(tx *util.Tx) bool { + // Chk all of the output public key scripts for non-standard scripts. + for _, txOut := range tx.MsgTx().TxOut { + scriptClass := txscript.GetScriptClass(txOut.PkScript) + if scriptClass == txscript.NonStandardTy { + return true + } + } + return false +} + +// IsCheckpointCandidate returns whether or not the passed block is a good checkpoint candidate. +// +// The factors used to determine a good checkpoint are: +// +// - The block must be in the main chain +// +// - The block must be at least 'CheckpointConfirmations' blocks prior to the current end of the main chain +// +// - The timestamps for the blocks before and after the checkpoint must have timestamps which are also before and after +// the checkpoint, respectively (due to the median time allowance this is not always the case) +// +// - The block must not contain any strange transaction such as those with nonstandard scripts +// +// The intent is that candidates are reviewed by a developer to make the final decision and then manually added to the +// list of checkpoints for a network. This function is safe for concurrent access. +func (b *BlockChain) IsCheckpointCandidate(block *block.Block) (bool, error) { + b.ChainLock.RLock() + defer b.ChainLock.RUnlock() + // A checkpoint must be in the main chain. + node := b.Index.LookupNode(block.Hash()) + if node == nil || !b.BestChain.Contains(node) { + return false, nil + } + // Ensure the height of the passed block and the entry for the block in the main chain match. This should always be + // the case unless the caller provided an invalid block. + if node.height != block.Height() { + return false, fmt.Errorf("passed block height of %d does not "+ + "match the main chain height of %d", block.Height(), + node.height, + ) + } + // A checkpoint must be at least CheckpointConfirmations blocks before the end of the main chain. + mainChainHeight := b.BestChain.Tip().height + if node.height > (mainChainHeight - CheckpointConfirmations) { + return false, nil + } + // A checkpoint must be have at least one block after it. This should always succeed since the check above already + // made sure it is CheckpointConfirmations back, but be safe in case the constant changes. + nextNode := b.BestChain.Next(node) + if nextNode == nil { + return false, nil + } + // A checkpoint must be have at least one block before it. + if node.parent == nil { + return false, nil + } + // A checkpoint must have timestamps for the block and the blocks on either side of it in order (due to the median + // time allowance this is not always the case). + prevTime := time.Unix(node.parent.timestamp, 0) + curTime := block.WireBlock().Header.Timestamp + nextTime := time.Unix(nextNode.timestamp, 0) + if prevTime.After(curTime) || nextTime.Before(curTime) { + return false, nil + } + // A checkpoint must have transactions that only contain standard scripts. + for _, tx := range block.Transactions() { + if isNonstandardTransaction(tx) { + return false, nil + } + } + // All of the checks passed, so the block is a candidate. + return true, nil +} diff --git a/pkg/blockchain/common_test.go b/pkg/blockchain/common_test.go new file mode 100644 index 0000000..3a21bdc --- /dev/null +++ b/pkg/blockchain/common_test.go @@ -0,0 +1,349 @@ +package blockchain + +import ( + "compress/bzip2" + "encoding/binary" + "fmt" + "github.com/p9c/p9/pkg/block" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + + "github.com/p9c/p9/pkg/database" + _ "github.com/p9c/p9/pkg/database/ffldb" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // testDbType is the database backend type to use for the tests. + testDbType = "ffldb" + // testDbRoot is the root directory used to create all test databases. + testDbRoot = "testdbs" + // blockDataNet is the expected network in the test block data. + blockDataNet = wire.MainNet +) + +// filesExists returns whether or not the named file or directory exists. +func fileExists(name string) bool { + if _, e := os.Stat(name); E.Chk(e) { + if os.IsNotExist(e) { + return false + } + } + return true +} + +// isSupportedDbType returns whether or not the passed database type is currently supported. +func isSupportedDbType(dbType string) bool { + supportedDrivers := database.SupportedDrivers() + for _, driver := range supportedDrivers { + if dbType == driver { + return true + } + } + return false +} + +// loadBlocks reads files containing bitcoin block data (gzipped but otherwise in the format bitcoind writes) from disk +// and returns them as an array of util.Block. This is largely borrowed from the test code in pod. +func loadBlocks(filename string) (blocks []*block.Block, e error) { + filename = filepath.Join("tstdata/", filename) + var network = wire.MainNet + var dr io.Reader + var fi io.ReadCloser + fi, e = os.Open(filename) + if e != nil { + return + } + if strings.HasSuffix(filename, ".bz2") { + dr = bzip2.NewReader(fi) + } else { + dr = fi + } + defer func() { + if e = fi.Close(); E.Chk(e) { + } + }() + var blk *block.Block + height := int64(1) + for ; ; height++ { + var rintbuf uint32 + e = binary.Read(dr, binary.LittleEndian, &rintbuf) + if e == io.EOF { + // hit end of file at expected offset: no warning + // height-- + e = nil + } + if rintbuf != uint32(network) { + break + } + e = binary.Read(dr, binary.LittleEndian, &rintbuf) + blocklen := rintbuf + rbytes := make([]byte, blocklen) + // read block + _, e = dr.Read(rbytes) + if e != nil { + fmt.Println(e) + } + blk, e = block.NewFromBytes(rbytes) + if e != nil { + return + } + blocks = append(blocks, blk) + } + return +} + +// chainSetup is used to create a new db and chain instance with the genesis block already inserted. In addition to the +// new chain instance, it returns a teardown function the caller should invoke when done testing to clean up. +func chainSetup(dbName string, netparams *chaincfg.Params) (chain *BlockChain, teardown func(), e error) { + if !isSupportedDbType(testDbType) { + return nil, nil, fmt.Errorf("unsupported db type %v", testDbType) + } + // Handle memory database specially since it doesn't need the disk specific handling. + var db database.DB + if testDbType == "memdb" { + var ndb database.DB + ndb, e = database.Create(testDbType) + if e != nil { + return nil, nil, fmt.Errorf("error creating db: %v", e) + } + db = ndb + // Setup a teardown function for cleaning up. This function is returned to the caller to be invoked when it is + // done testing. + teardown = func() { + if e = db.Close(); E.Chk(e) { + } + } + } else { + // Create the root directory for test databases. + if !fileExists(testDbRoot) { + if e = os.MkdirAll(testDbRoot, 0700); E.Chk(e) { + e = fmt.Errorf( + "unable to create test db "+ + "root: %v", e, + ) + return nil, nil, e + } + } + // Create a new database to store the accepted blocks into. + dbPath := filepath.Join(testDbRoot, dbName) + _ = os.RemoveAll(dbPath) + var ndb database.DB + ndb, e = database.Create(testDbType, dbPath, blockDataNet) + if e != nil { + return nil, nil, fmt.Errorf("error creating db: %v", e) + } + db = ndb + // Setup a teardown function for cleaning up. This function is returned to the caller to be invoked when it is + // done testing. + teardown = func() { + if e = db.Close(); E.Chk(e) { + } + if e = os.RemoveAll(dbPath); E.Chk(e) { + } + if e = os.RemoveAll(testDbRoot); E.Chk(e) { + } + } + } + // Copy the chain netparams to ensure any modifications the tests do to the chain parameters do not affect the + // global instance. + paramsCopy := *netparams + // Create the main chain instance. + chain, e = New( + &Config{ + DB: db, + ChainParams: ¶msCopy, + Checkpoints: nil, + TimeSource: NewMedianTime(), + SigCache: txscript.NewSigCache(1000), + }, + ) + if e != nil { + teardown() + e = fmt.Errorf("failed to create chain instance: %v", e) + return nil, nil, e + } + return chain, teardown, nil +} + +// loadUtxoView returns a utxo view loaded from a file. +func loadUtxoView(filename string) (*UtxoViewpoint, error) { + // The utxostore file format is: + // + // + // + // The output index and serialized utxo len are little endian uint32s and the serialized utxo uses the format + // described in chainio.go. + filename = filepath.Join("tstdata", filename) + fi, e := os.Open(filename) + if e != nil { + return nil, e + } + // Choose read based on whether the file is compressed or not. + var r io.Reader + if strings.HasSuffix(filename, ".bz2") { + r = bzip2.NewReader(fi) + } else { + r = fi + } + defer func() { + if e := fi.Close(); E.Chk(e) { + } + }() + view := NewUtxoViewpoint() + for { + // Hash of the utxo entry. + var hash chainhash.Hash + _, e := io.ReadAtLeast(r, hash[:], len(hash[:])) + if e != nil { + // Expected EOF at the right offset. + if e == io.EOF { + break + } + return nil, e + } + // Output index of the utxo entry. + var index uint32 + e = binary.Read(r, binary.LittleEndian, &index) + if e != nil { + return nil, e + } + // Num of serialized utxo entry bytes. + var numBytes uint32 + e = binary.Read(r, binary.LittleEndian, &numBytes) + if e != nil { + return nil, e + } + // Serialized utxo entry. + serialized := make([]byte, numBytes) + _, e = io.ReadAtLeast(r, serialized, int(numBytes)) + if e != nil { + return nil, e + } + // Deserialize it and add it to the view. + entry, e := deserializeUtxoEntry(serialized) + if e != nil { + return nil, e + } + view.Entries()[wire.OutPoint{Hash: hash, Index: index}] = entry + } + return view, nil +} + +// convertUtxoStore reads a utxostore from the legacy format and writes it back out using the latest format. It is only +// useful for converting utxostore data used in the tests, which has already been done. However, the code is left +// available for future reference. +func convertUtxoStore(r io.Reader, w io.Writer) (e error) { + // The old utxostore file format was: + // + // + // + // The serialized utxo len was a little endian uint32 and the serialized utxo uses the format described in + // upgrade.go. + littleEndian := binary.LittleEndian + for { + // Hash of the utxo entry. + var hash chainhash.Hash + _, e := io.ReadAtLeast(r, hash[:], len(hash[:])) + if e != nil { + // Expected EOF at the right offset. + if e == io.EOF { + break + } + return e + } + // Num of serialized utxo entry bytes. + var numBytes uint32 + e = binary.Read(r, littleEndian, &numBytes) + if e != nil { + return e + } + // Serialized utxo entry. + serialized := make([]byte, numBytes) + _, e = io.ReadAtLeast(r, serialized, int(numBytes)) + if e != nil { + return e + } + // Deserialize the entry. + entries, e := deserializeUtxoEntryV0(serialized) + if e != nil { + return e + } + // Loop through all of the utxos and write them out in the new format. + for outputIdx, entry := range entries { + // Reserialize the entries using the new format. + serialized, e := serializeUtxoEntry(entry) + if e != nil { + return e + } + // Write the hash of the utxo entry. + _, e = w.Write(hash[:]) + if e != nil { + return e + } + // Write the output index of the utxo entry. + e = binary.Write(w, littleEndian, outputIdx) + if e != nil { + return e + } + // Write num of serialized utxo entry bytes. + e = binary.Write(w, littleEndian, uint32(len(serialized))) + if e != nil { + return e + } + // Write the serialized utxo. + _, e = w.Write(serialized) + if e != nil { + return e + } + } + } + return nil +} + +// TstSetCoinbaseMaturity makes the ability to set the coinbase maturity available when running tests. +func (b *BlockChain) TstSetCoinbaseMaturity(maturity uint16) { + b.params.CoinbaseMaturity = maturity +} + +// newFakeChain returns a chain that is usable for syntetic tests. It is important to note that this chain has no +// database associated with it, so it is not usable with all functions and the tests must take care when making use of +// it. +func newFakeChain(params *chaincfg.Params) *BlockChain { + // Create a genesis block node and block index index populated with it for use when creating the fake chain below. + node := NewBlockNode(¶ms.GenesisBlock.Header, nil) + index := newBlockIndex(nil, params) + index.AddNode(node) + targetTimespan := params.TargetTimespan + targetTimePerBlock := params.TargetTimePerBlock + adjustmentFactor := params.RetargetAdjustmentFactor + return &BlockChain{ + params: params, + timeSource: NewMedianTime(), + minRetargetTimespan: targetTimespan / adjustmentFactor, + maxRetargetTimespan: targetTimespan * adjustmentFactor, + blocksPerRetarget: int32(targetTimespan / targetTimePerBlock), + Index: index, + BestChain: newChainView(node), + } +} + +// newFakeNode creates a block node connected to the passed parent with the provided fields populated and fake values +// for the other fields. +func newFakeNode(parent *BlockNode, blockVersion int32, bits uint32, timestamp time.Time) *BlockNode { + // Make up a header and create a block node from it. + header := &wire.BlockHeader{ + Version: blockVersion, + PrevBlock: parent.hash, + Bits: bits, + Timestamp: timestamp, + } + return NewBlockNode(header, parent) +} diff --git a/pkg/blockchain/compress.go b/pkg/blockchain/compress.go new file mode 100644 index 0000000..d9ea7a9 --- /dev/null +++ b/pkg/blockchain/compress.go @@ -0,0 +1,502 @@ +package blockchain + +import ( + "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/txscript" +) + +// In order to reduce the size of stored scripts, a domain specific compression algorithm is used which recognizes +// standard scripts and stores them using less bytes than the original script. The compression algorithm used here was +// obtained from Bitcoin Core, so all credits for the algorithm go to it. +// +// The general serialized format is: +// +// + + + + +` + // jsSetGo sets the `window.go` variable. + jsSetGo = `(() => { + window.go = {argv: [], env: {}, importObject: {go: {}}}; + const argv = new URLSearchParams(location.search).get("argv"); + if (argv) { + window.go["argv"] = argv.split(" "); + } +})();` + // jsStartGo initializes the main.wasm. + jsStartGo = `(() => { + defaultGo = new Go(); + Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"])); + Object.assign(defaultGo["env"], go["env"]); + for (let key in go["importObject"]) { + if (typeof defaultGo["importObject"][key] === "undefined") { + defaultGo["importObject"][key] = {}; + } + Object.assign(defaultGo["importObject"][key], go["importObject"][key]); + } + window.go = defaultGo; + if (!WebAssembly.instantiateStreaming) { // polyfill + WebAssembly.instantiateStreaming = async (resp, importObject) => { + const source = await (await resp).arrayBuffer(); + return await WebAssembly.instantiate(source, importObject); + }; + } + WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => { + go.run(result.instance); + }); +})();` +) diff --git a/pkg/gel/gio/cmd/gogio/main.go b/pkg/gel/gio/cmd/gogio/main.go new file mode 100644 index 0000000..da35401 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/main.go @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "image" + "image/color" + "image/png" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "golang.org/x/image/draw" + "golang.org/x/sync/errgroup" +) + +var ( + target = flag.String("target", "", "specify target (ios, tvos, android, js).\n") + archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).") + minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level") + buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)") + destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.") + appID = flag.String("appid", "", "app identifier (for -buildmode=exe)") + version = flag.Int("version", 1, "app version (for -buildmode=exe)") + printCommands = flag.Bool("x", false, "print the commands") + keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.") + linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool") + extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker") + extraTags = flag.String("tags", "", "extra tags to the Go tool") + iconPath = flag.String("icon", "", "specify an icon for iOS and Android") + signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.") + signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.") + noStrip = flag.Bool("nostrip", false, "leave debugging symbols in produced .so files") +) + +func main() { + flag.Usage = func() { + fmt.Fprint(os.Stderr, mainUsage) + } + flag.Parse() + if err := flagValidate(); err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + buildInfo, err := newBuildInfo(flag.Arg(0)) + if err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + if err := build(buildInfo); err != nil { + fmt.Fprintf(os.Stderr, "gogio: %v\n", err) + os.Exit(1) + } + os.Exit(0) +} + +func flagValidate() error { + pkgPathArg := flag.Arg(0) + if pkgPathArg == "" { + return errors.New("specify a package") + } + if *target == "" { + return errors.New("please specify -target") + } + switch *target { + case "ios", "tvos", "android", "js", "windows": + default: + return fmt.Errorf("invalid -target %s", *target) + } + switch *buildMode { + case "archive", "exe": + default: + return fmt.Errorf("invalid -buildmode %s", *buildMode) + } + return nil +} + +func build(bi *buildInfo) error { + tmpDir, err := ioutil.TempDir("", "gogio-") + if err != nil { + return err + } + if *keepWorkdir { + fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir) + } else { + defer os.RemoveAll(tmpDir) + } + switch *target { + case "js": + return buildJS(bi) + case "ios", "tvos": + return buildIOS(tmpDir, *target, bi) + case "android": + return buildAndroid(tmpDir, bi) + case "windows": + return buildWindows(tmpDir, bi) + default: + panic("unreachable") + } +} + +func runCmdRaw(cmd *exec.Cmd) ([]byte, error) { + if *printCommands { + fmt.Printf("%s\n", strings.Join(cmd.Args, " ")) + } + out, err := cmd.Output() + if err == nil { + return out, nil + } + if err, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr) + } + return nil, err +} + +func runCmd(cmd *exec.Cmd) (string, error) { + out, err := runCmdRaw(cmd) + return string(bytes.TrimSpace(out)), err +} + +func copyFile(dst, src string) (err error) { + r, err := os.Open(src) + if err != nil { + return err + } + defer r.Close() + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err == nil { + err = cerr + } + }() + _, err = io.Copy(w, r) + return err +} + +type arch struct { + iosArch string + jniArch string + clangArch string +} + +var allArchs = map[string]arch{ + "arm": { + iosArch: "armv7", + jniArch: "armeabi-v7a", + clangArch: "armv7a-linux-androideabi", + }, + "arm64": { + iosArch: "arm64", + jniArch: "arm64-v8a", + clangArch: "aarch64-linux-android", + }, + "386": { + iosArch: "i386", + jniArch: "x86", + clangArch: "i686-linux-android", + }, + "amd64": { + iosArch: "x86_64", + jniArch: "x86_64", + clangArch: "x86_64-linux-android", + }, +} + +type iconVariant struct { + path string + size int + fill bool +} + +func buildIcons(baseDir, icon string, variants []iconVariant) error { + f, err := os.Open(icon) + if err != nil { + return err + } + defer f.Close() + img, _, err := image.Decode(f) + if err != nil { + return err + } + var resizes errgroup.Group + for _, v := range variants { + v := v + resizes.Go(func() (err error) { + path := filepath.Join(baseDir, v.path) + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if cerr := f.Close(); err == nil { + err = cerr + } + }() + return png.Encode(f, resizeIcon(v, img)) + }) + } + return resizes.Wait() +} + +func resizeIcon(v iconVariant, img image.Image) *image.NRGBA { + scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}}) + op := draw.Src + if v.fill { + op = draw.Over + draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src) + } + draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil) + + return scaled +} diff --git a/pkg/gel/gio/cmd/gogio/main_test.go b/pkg/gel/gio/cmd/gogio/main_test.go new file mode 100644 index 0000000..98dcb27 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/main_test.go @@ -0,0 +1,17 @@ +package main + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + if os.Getenv("RUN_GOGIO") != "" { + // Allow the end-to-end tests to call the gogio tool without + // having to build it from scratch, nor having to refactor the + // main function to avoid using global variables. + main() + os.Exit(0) // main already exits, but just in case. + } + os.Exit(m.Run()) +} diff --git a/pkg/gel/gio/cmd/gogio/permission.go b/pkg/gel/gio/cmd/gogio/permission.go new file mode 100644 index 0000000..b22fcef --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/permission.go @@ -0,0 +1,33 @@ +package main + +var AndroidPermissions = map[string][]string{ + "network": { + "android.permission.INTERNET", + }, + "networkstate": { + "android.permission.ACCESS_NETWORK_STATE", + }, + "bluetooth": { + "android.permission.BLUETOOTH", + "android.permission.BLUETOOTH_ADMIN", + "android.permission.ACCESS_FINE_LOCATION", + }, + "camera": { + "android.permission.CAMERA", + }, + "storage": { + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + }, +} + +var AndroidFeatures = map[string][]string{ + "default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`}, + "bluetooth": { + `name="android.hardware.bluetooth"`, + `name="android.hardware.bluetooth_le"`, + }, + "camera": { + `name="android.hardware.camera"`, + }, +} diff --git a/pkg/gel/gio/cmd/gogio/race_test.go b/pkg/gel/gio/cmd/gogio/race_test.go new file mode 100644 index 0000000..0749936 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/race_test.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build race + +package main_test + +func init() { raceEnabled = true } diff --git a/pkg/gel/gio/cmd/gogio/testdata/testdata.go b/pkg/gel/gio/cmd/gogio/testdata/testdata.go new file mode 100644 index 0000000..305f8d8 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/testdata/testdata.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// A simple app used for gogio's end-to-end tests. +package main + +import ( + "fmt" + "image" + "image/color" + "log" + + "github.com/p9c/p9/pkg/gel/gio/app" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/io/system" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +func main() { + go func() { + w := app.NewWindow() + if err := loop(w); err != nil { + log.Fatal(err) + } + }() + app.Main() +} + +type notifyFrame int + +const ( + notifyNone notifyFrame = iota + notifyInvalidate + notifyPrint +) + +// notify keeps track of whether we want to print to stdout to notify the user +// when a frame is ready. Initially we want to notify about the first frame. +var notify = notifyInvalidate + +type ( + C = layout.Context + D = layout.Dimensions +) + +func loop(w *app.Window) error { + topLeft := quarterWidget{ + color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}, + } + topRight := quarterWidget{ + color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + } + botLeft := quarterWidget{ + color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + } + botRight := quarterWidget{ + color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80}, + } + + var ops op.Ops + for { + e := <-w.Events() + switch e := e.(type) { + case system.DestroyEvent: + return e.Err + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + // Clear background to white, even on embedded platforms such as webassembly. + paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + // r1c1 + layout.Flexed(1, func(gtx C) D { return topLeft.Layout(gtx) }), + // r1c2 + layout.Flexed(1, func(gtx C) D { return topRight.Layout(gtx) }), + ) + }), + layout.Flexed(1, func(gtx C) D { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + // r2c1 + layout.Flexed(1, func(gtx C) D { return botLeft.Layout(gtx) }), + // r2c2 + layout.Flexed(1, func(gtx C) D { return botRight.Layout(gtx) }), + ) + }), + ) + + e.Frame(gtx.Ops) + + switch notify { + case notifyInvalidate: + notify = notifyPrint + w.Invalidate() + case notifyPrint: + notify = notifyNone + fmt.Println("gio frame ready") + } + } + } +} + +// quarterWidget paints a quarter of the screen with one color. When clicked, it +// turns red, going back to its normal color when clicked again. +type quarterWidget struct { + color color.NRGBA + + clicked bool +} + +var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff} + +func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions { + var color color.NRGBA + if w.clicked { + color = red + } else { + color = w.color + } + + r := image.Rectangle{Max: gtx.Constraints.Max} + paint.FillShape(gtx.Ops, color, clip.Rect(r).Op()) + + pointer.Rect(image.Rectangle{ + Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y), + }).Add(gtx.Ops) + pointer.InputOp{ + Tag: w, + Types: pointer.Press, + }.Add(gtx.Ops) + + for _, e := range gtx.Events(w) { + if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press { + w.clicked = !w.clicked + // notify when we're done updating the frame. + notify = notifyInvalidate + } + } + return layout.Dimensions{Size: gtx.Constraints.Max} +} diff --git a/pkg/gel/gio/cmd/gogio/wayland_test.go b/pkg/gel/gio/cmd/gogio/wayland_test.go new file mode 100644 index 0000000..df10410 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/wayland_test.go @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "image" + "image/png" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "sync" + "text/template" + "time" +) + +type WaylandTestDriver struct { + driverBase + + runtimeDir string + socket string + display string +} + +// No bars or anything fancy. Just a white background with our dimensions. +var tmplSwayConfig = template.Must(template.New("").Parse(` +output * bg #FFFFFF solid_color +output * mode {{.Width}}x{{.Height}} +default_border none +`)) + +var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`) + +func (d *WaylandTestDriver) Start(path string) { + // We want os.Environ, so that it can e.g. find $DISPLAY to run within + // X11. wlroots env vars are documented at: + // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md + env := os.Environ() + if *headless { + env = append(env, "WLR_BACKENDS=headless") + } + + d.needPrograms( + "sway", // to run a wayland compositor + "grim", // to take screenshots + "swaymsg", // to send input + ) + + // First, build the app. + dir := d.tempDir("gio-endtoend-wayland") + bin := filepath.Join(dir, "red") + flags := []string{"build", "-tags", "nox11", "-o=" + bin} + if raceEnabled { + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + conf := filepath.Join(dir, "config") + f, err := os.Create(conf) + if err != nil { + d.Fatal(err) + } + defer f.Close() + if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{ + d.width, d.height, + }); err != nil { + d.Fatal(err) + } + + d.socket = filepath.Join(dir, "socket") + env = append(env, "SWAYSOCK="+d.socket) + d.runtimeDir = dir + env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir) + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + // First, start sway. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose") + cmd.Env = env + stderr, err := cmd.StderrPipe() + if err != nil { + d.Fatal(err) + } + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + d.Cleanup(func() { + // Give it a chance to exit gracefully, cleaning up + // after itself. After 10ms, the deferred cancel above + // will signal an os.Kill. + cmd.Process.Signal(os.Interrupt) + time.Sleep(10 * time.Millisecond) + }) + + // Wait for sway to be ready. We probably don't need a deadline + // here. + br := bufio.NewReader(stderr) + for { + line, err := br.ReadString('\n') + if err != nil { + d.Fatal(err) + } + if m := rxSwayReady.FindStringSubmatch(line); m != nil { + d.display = m[1] + break + } + } + + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") { + // Don't print all stderr, since we use --verbose. + // TODO(mvdan): if it's useful, probably filter + // errors and show them. + d.Error(err) + } + wg.Done() + }() + } + + // Then, start our program on the sway compositor above. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, bin) + cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *WaylandTestDriver) Screenshot() image.Image { + cmd := exec.Command("grim", "/dev/stdout") + cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *WaylandTestDriver) swaymsg(args ...interface{}) { + strs := []string{"--socket", d.socket} + for _, arg := range args { + strs = append(strs, fmt.Sprint(arg)) + } + cmd := exec.Command("swaymsg", strs...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } +} + +func (d *WaylandTestDriver) Click(x, y int) { + d.swaymsg("seat", "-", "cursor", "set", x, y) + d.swaymsg("seat", "-", "cursor", "press", "button1") + d.swaymsg("seat", "-", "cursor", "release", "button1") + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/pkg/gel/gio/cmd/gogio/windows_test.go b/pkg/gel/gio/cmd/gogio/windows_test.go new file mode 100644 index 0000000..996b511 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/windows_test.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "context" + "image" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "time" + + "golang.org/x/image/draw" +) + +// Wine is tightly coupled with X11 at the moment, and we can reuse the same +// methods to automate screenshots and clicks. The main difference is how we +// build and run the app. + +// The only quirk is that it seems impossible for the Wine window to take the +// entirety of the X server's dimensions, even if we try to resize it to take +// the entire display. It seems to want to leave some vertical space empty, +// presumably for window decorations or the "start" bar on Windows. To work +// around that, make the X server 50x50px bigger, and crop the screenshots back +// to the original size. + +type WineTestDriver struct { + X11TestDriver +} + +func (d *WineTestDriver) Start(path string) { + d.needPrograms("wine") + + // First, build the app. + bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe") + flags := []string{"build", "-o=" + bin} + if raceEnabled { + if runtime.GOOS != "windows" { + // cross-compilation disables CGo, which breaks -race. + d.Skipf("can't cross-compile -race for Windows; skipping") + } + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, "GOOS=windows") + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + // Add 50x50px to the display dimensions, as discussed earlier. + d.startServer(&wg, d.width+50, d.height+50) + + // Then, start our program via Wine on the X server above. + { + cacheDir, err := os.UserCacheDir() + if err != nil { + d.Fatal(err) + } + // Use a wine directory separate from the default ~/.wine, so + // that the user's winecfg doesn't affect our test. This will + // default to ~/.cache/gio-e2e-wine. We use the user's cache, + // to reuse a previously set up wineprefix. + wineprefix := filepath.Join(cacheDir, "gio-e2e-wine") + + // First, ensure that wineprefix is up to date with wineboot. + // Wait for this separately from the first frame, as setting up + // a new prefix might take 5s on its own. + env := []string{ + "DISPLAY=" + d.display, + "WINEDEBUG=fixme-all", // hide "fixme" noise + "WINEPREFIX=" + wineprefix, + + // Disable wine-gecko (Explorer) and wine-mono (.NET). + // Otherwise, if not installed, wineboot will get stuck + // with a prompt to install them on the virtual X + // display. Moreover, Gio doesn't need either, and wine + // is faster without them. + "WINEDLLOVERRIDES=mscoree,mshtml=", + } + { + start := time.Now() + cmd := exec.Command("wine", "wineboot", "-i") + cmd.Env = env + // Use a combined output pipe instead of CombinedOutput, + // so that we only wait for the child process to exit, + // and we don't need to wait for all of wine's + // grandchildren to exit and stop writing. This is + // relevant as wine leaves "wineserver" lingering for + // three seconds by default, to be reused later. + stdout, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + if err := cmd.Run(); err != nil { + io.Copy(os.Stderr, stdout) + d.Fatal(err) + } + d.Logf("set up WINEPREFIX in %s", time.Since(start)) + } + + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, "wine", bin) + cmd.Env = env + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + // Wait for the gio app to render. + d.waitForFrame() + + // xdotool seems to fail at actually moving the window if we use it + // immediately after Gio is ready. Why? + // We can't tell if the windowmove operation worked until we take a + // screenshot, because the getwindowgeometry op reports the 0x0 + // coordinates even if the window wasn't moved properly. + // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that. + // TODO(mvdan): revisit this, when you have a spare three hours. + time.Sleep(400 * time.Millisecond) + id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio") + d.xdotool("windowmove", "--sync", id, 0, 0) +} + +func (d *WineTestDriver) Screenshot() image.Image { + img := d.X11TestDriver.Screenshot() + // Crop the screenshot back to the original dimensions. + cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height)) + draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src) + return cropped +} diff --git a/pkg/gel/gio/cmd/gogio/windowsbuild.go b/pkg/gel/gio/cmd/gogio/windowsbuild.go new file mode 100644 index 0000000..1af8668 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/windowsbuild.go @@ -0,0 +1,412 @@ +package main + +import ( + "bytes" + "encoding/binary" + "fmt" + "image/png" + "io" + "math" + "os" + "os/exec" + "path/filepath" + "reflect" + "strconv" + "strings" + "text/template" + + "github.com/akavel/rsrc/binutil" + "github.com/akavel/rsrc/coff" + "golang.org/x/text/encoding/unicode" +) + +func buildWindows(tmpDir string, bi *buildInfo) error { + builder := &windowsBuilder{TempDir: tmpDir} + builder.DestDir = *destPath + if builder.DestDir == "" { + builder.DestDir = bi.pkgPath + } + + name := bi.name + if *destPath != "" { + if filepath.Ext(*destPath) != ".exe" { + return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath) + } + name = filepath.Base(*destPath) + } + name = strings.TrimSuffix(name, ".exe") + sdk := bi.minsdk + if sdk > 10 { + return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk) + } + version := strconv.Itoa(bi.version) + if bi.version > math.MaxUint16 { + return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16) + } + + for _, arch := range bi.archs { + builder.Coff = coff.NewRSRC() + builder.Coff.Arch(arch) + + if err := builder.embedIcon(bi.iconPath); err != nil { + return err + } + + if err := builder.embedManifest(windowsManifest{ + Version: "1.0.0." + version, + WindowsVersion: sdk, + Name: name, + }); err != nil { + return fmt.Errorf("can't create manifest: %v", err) + } + + if err := builder.embedInfo(windowsResources{ + Version: [2]uint32{uint32(1) << 16, uint32(bi.version)}, + VersionHuman: "1.0.0." + version, + Name: name, + Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10) + }); err != nil { + return fmt.Errorf("can't create info: %v", err) + } + + if err := builder.buildResource(bi, name, arch); err != nil { + return fmt.Errorf("can't build the resources: %v", err) + } + + if err := builder.buildProgram(bi, name, arch); err != nil { + return err + } + } + + return nil +} + +type ( + windowsResources struct { + Version [2]uint32 + VersionHuman string + Language uint16 + Name string + } + windowsManifest struct { + Version string + WindowsVersion int + Name string + } + windowsBuilder struct { + TempDir string + DestDir string + Coff *coff.Coff + } +) + +const ( + // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types + windowsResourceIcon = 3 + windowsResourceIconGroup = windowsResourceIcon + 11 + windowsResourceManifest = 24 + windowsResourceVersion = 16 +) + +type bufferCoff struct { + bytes.Buffer +} + +func (b *bufferCoff) Size() int64 { + return int64(b.Len()) +} + +func (b *windowsBuilder) embedIcon(path string) (err error) { + iconFile, err := os.Open(path) + if err != nil { + return fmt.Errorf("can't read the icon located at %s: %v", path, err) + } + defer iconFile.Close() + + iconImage, err := png.Decode(iconFile) + if err != nil { + return fmt.Errorf("can't decode the PNG file (%s): %v", path, err) + } + + sizes := []int{16, 32, 48, 64, 128, 256} + var iconHeader bufferCoff + + // GRPICONDIR structure. + if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil { + return err + } + + for _, size := range sizes { + var iconBuffer bufferCoff + + if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil { + return fmt.Errorf("can't encode image: %v", err) + } + + b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer) + + if err := binary.Write(&iconHeader, binary.LittleEndian, struct { + Size [2]uint8 + Color [2]uint8 + Planes uint16 + BitCount uint16 + Length uint32 + Id uint16 + }{ + Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px. + Planes: 1, + BitCount: 32, + Length: uint32(iconBuffer.Len()), + Id: uint16(size), + }); err != nil { + return err + } + } + + b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader) + + return nil +} + +func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error { + out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso")) + if err != nil { + return err + } + defer out.Close() + b.Coff.Freeze() + + // See https://github.com/akavel/rsrc/internal/write.go#L13. + w := binutil.Writer{W: out} + binutil.Walk(b.Coff, func(v reflect.Value, path string) error { + if binutil.Plain(v.Kind()) { + w.WriteLE(v.Interface()) + return nil + } + vv, ok := v.Interface().(binutil.SizedReader) + if ok { + w.WriteFromSized(vv) + return binutil.WALK_SKIP + } + return nil + }) + + if w.Err != nil { + return fmt.Errorf("error writing output file: %s", w.Err) + } + + return nil +} + +func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error { + dest := b.DestDir + if len(buildInfo.archs) > 1 { + dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe") + } + + cmd := exec.Command( + "go", + "build", + "-ldflags=-H=windowsgui "+buildInfo.ldflags, + "-tags="+buildInfo.tags, + "-o", dest, + buildInfo.pkgPath, + ) + cmd.Env = append( + os.Environ(), + "GOOS=windows", + "GOARCH="+arch, + ) + _, err := runCmd(cmd) + return err +} + +func (b *windowsBuilder) embedManifest(v windowsManifest) error { + t, err := template.New("manifest").Parse(` + + + {{.Name}} + + + {{if (le .WindowsVersion 10)}} +{{end}} + {{if (le .WindowsVersion 9)}} +{{end}} + {{if (le .WindowsVersion 8)}} +{{end}} + {{if (le .WindowsVersion 7)}} +{{end}} + {{if (le .WindowsVersion 6)}} +{{end}} + + + + + + + + + + + + true + + +`) + if err != nil { + return err + } + + var manifest bufferCoff + if err := t.Execute(&manifest, v); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceManifest, 1, &manifest) + + return nil +} + +func (b *windowsBuilder) embedInfo(v windowsResources) error { + page := uint16(1) + + // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo + t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo + windowsInfoValueFixed{ + Signature: 0xFEEF04BD, + StructVersion: 0x00010000, + FileVersion: v.Version, + ProductVersion: v.Version, + FileFlagMask: 0x3F, + FileFlags: 0, + FileOS: 0x40004, + FileType: 0x1, + FileSubType: 0, + }, + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo + newValue(valueText, "StringFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable + newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str + newValue(valueText, "ProductVersion", v.VersionHuman), + newValue(valueText, "FileVersion", v.VersionHuman), + newValue(valueText, "FileDescription", v.Name), + newValue(valueText, "ProductName", v.Name), + // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...) + }), + }), + // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo + newValue(valueBinary, "VarFileInfo", []io.WriterTo{ + // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str + newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)), + }), + }) + + // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`: + t.ValueLength = 52 + + var verrsrc bufferCoff + if _, err := t.WriteTo(&verrsrc); err != nil { + return err + } + + b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc) + + return nil +} + +type windowsInfoValueFixed struct { + Signature uint32 + StructVersion uint32 + FileVersion [2]uint32 + ProductVersion [2]uint32 + FileFlagMask uint32 + FileFlags uint32 + FileOS uint32 + FileType uint32 + FileSubType uint32 + FileDate [2]uint32 +} + +func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) { + return 0, binary.Write(w, binary.LittleEndian, v) +} + +type windowsInfoValue struct { + Length uint16 + ValueLength uint16 + Type uint16 + Key []byte + Value []byte +} + +func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) { + // binary.Write doesn't support []byte inside struct. + if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil { + return 0, err + } + if _, err = w.Write(v.Key); err != nil { + return 0, err + } + if _, err = w.Write(v.Value); err != nil { + return 0, err + } + return 0, nil +} + +const ( + valueBinary uint16 = 0 + valueText uint16 = 1 +) + +func newValue(valueType uint16, key string, input interface{}) windowsInfoValue { + v := windowsInfoValue{ + Type: valueType, + Length: 6, + } + + padding := func(in []byte) []byte { + if l := uint16(len(in)) + v.Length; l%4 != 0 { + return append(in, make([]byte, 4-l%4)...) + } + return in + } + + v.Key = padding(utf16Encode(key)) + v.Length += uint16(len(v.Key)) + + switch in := input.(type) { + case string: + v.Value = padding(utf16Encode(in)) + v.ValueLength = uint16(len(v.Value) / 2) + case []io.WriterTo: + var buff bytes.Buffer + for k := range in { + if _, err := in[k].WriteTo(&buff); err != nil { + panic(err) + } + } + v.Value = buff.Bytes() + default: + var buff bytes.Buffer + if err := binary.Write(&buff, binary.LittleEndian, in); err != nil { + panic(err) + } + v.ValueLength = uint16(buff.Len()) + v.Value = buff.Bytes() + } + + v.Length += uint16(len(v.Value)) + + return v +} + +// utf16Encode encodes the string to UTF16 with null-termination. +func utf16Encode(s string) []byte { + b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s)) + if err != nil { + panic(err) + } + return append(b, 0x00, 0x00) // null-termination. +} diff --git a/pkg/gel/gio/cmd/gogio/x11_test.go b/pkg/gel/gio/cmd/gogio/x11_test.go new file mode 100644 index 0000000..9bb3174 --- /dev/null +++ b/pkg/gel/gio/cmd/gogio/x11_test.go @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main_test + +import ( + "bytes" + "context" + "fmt" + "image" + "image/png" + "io" + "math/rand" + "os" + "os/exec" + "path/filepath" + "sync" + "time" +) + +type X11TestDriver struct { + driverBase + + display string +} + +func (d *X11TestDriver) Start(path string) { + // First, build the app. + bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red") + flags := []string{"build", "-tags", "nowayland", "-o=" + bin} + if raceEnabled { + flags = append(flags, "-race") + } + flags = append(flags, path) + cmd := exec.Command("go", flags...) + if out, err := cmd.CombinedOutput(); err != nil { + d.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + d.Cleanup(wg.Wait) + + d.startServer(&wg, d.width, d.height) + + // Then, start our program on the X server above. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, bin) + cmd.Env = []string{"DISPLAY=" + d.display} + output, err := cmd.StdoutPipe() + if err != nil { + d.Fatal(err) + } + cmd.Stderr = cmd.Stdout + d.output = output + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + d.Error(err) + } + wg.Done() + }() + } + + // Wait for the gio app to render. + d.waitForFrame() +} + +func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) { + // Pick a random display number between 1 and 100,000. Most machines + // will only be using :0, so there's only a 0.001% chance of two + // concurrent test runs to run into a conflict. + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1) + + var xprog string + xflags := []string{ + "-wr", // we want a white background; the default is black + } + if *headless { + xprog = "Xvfb" // virtual X server + xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height)) + } else { + xprog = "Xephyr" // nested X server as a window + xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height)) + } + xflags = append(xflags, d.display) + + d.needPrograms( + xprog, // to run the X server + "scrot", // to take screenshots + "xdotool", // to send input + ) + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, xprog, xflags...) + combined := &bytes.Buffer{} + cmd.Stdout = combined + cmd.Stderr = combined + if err := cmd.Start(); err != nil { + d.Fatal(err) + } + d.Cleanup(cancel) + d.Cleanup(func() { + // Give it a chance to exit gracefully, cleaning up + // after itself. After 10ms, the deferred cancel above + // will signal an os.Kill. + cmd.Process.Signal(os.Interrupt) + time.Sleep(10 * time.Millisecond) + }) + + // Wait for the X server to be ready. The socket path isn't + // terribly portable, but that's okay for now. + withRetries(d.T, time.Second, func() error { + socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:]) + _, err := os.Stat(socket) + return err + }) + + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + // Print all output and error. + io.Copy(os.Stdout, combined) + d.Error(err) + } + wg.Done() + }() +} + +func (d *X11TestDriver) Screenshot() image.Image { + cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout") + cmd.Env = []string{"DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + img, err := png.Decode(bytes.NewReader(out)) + if err != nil { + d.Fatal(err) + } + return img +} + +func (d *X11TestDriver) xdotool(args ...interface{}) string { + d.Helper() + strs := make([]string, len(args)) + for i, arg := range args { + strs[i] = fmt.Sprint(arg) + } + cmd := exec.Command("xdotool", strs...) + cmd.Env = []string{"DISPLAY=" + d.display} + out, err := cmd.CombinedOutput() + if err != nil { + d.Errorf("%s", out) + d.Fatal(err) + } + return string(bytes.TrimSpace(out)) +} + +func (d *X11TestDriver) Click(x, y int) { + d.xdotool("mousemove", "--sync", x, y) + d.xdotool("click", "1") + + // Wait for the gio app to render after this click. + d.waitForFrame() +} diff --git a/pkg/gel/gio/f32/affine.go b/pkg/gel/gio/f32/affine.go new file mode 100644 index 0000000..667f7e9 --- /dev/null +++ b/pkg/gel/gio/f32/affine.go @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "fmt" + "math" +) + +// Affine2D represents an affine 2D transformation. The zero value if Affine2D +// represents the identity transform. +type Affine2D struct { + // in order to make the zero value of Affine2D represent the identity + // transform we store it with the identity matrix subtracted, that is + // if the actual transformation matrix is: + // [sx, hx, ox] + // [hy, sy, oy] + // [ 0, 0, 1] + // we store a = sx-1 and e = sy-1 + a, b, c float32 + d, e, f float32 +} + +// NewAffine2D creates a new Affine2D transform from the matrix elements +// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D { + return Affine2D{ + a: sx - 1, b: hx, c: ox, + d: hy, e: sy - 1, f: oy, + } +} + +// Offset the transformation. +func (a Affine2D) Offset(offset Point) Affine2D { + return Affine2D{ + a.a, a.b, a.c + offset.X, + a.d, a.e, a.f + offset.Y, + } +} + +// Scale the transformation around the given origin. +func (a Affine2D) Scale(origin, factor Point) Affine2D { + if origin == (Point{}) { + return a.scale(factor) + } + a = a.Offset(origin.Mul(-1)) + a = a.scale(factor) + return a.Offset(origin) +} + +// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin. +func (a Affine2D) Rotate(origin Point, radians float32) Affine2D { + if origin == (Point{}) { + return a.rotate(radians) + } + a = a.Offset(origin.Mul(-1)) + a = a.rotate(radians) + return a.Offset(origin) +} + +// Shear the transformation by the given angle (in radians) around the given origin. +func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D { + if origin == (Point{}) { + return a.shear(radiansX, radiansY) + } + a = a.Offset(origin.Mul(-1)) + a = a.shear(radiansX, radiansY) + return a.Offset(origin) +} + +// Mul returns A*B. +func (A Affine2D) Mul(B Affine2D) (r Affine2D) { + r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1 + r.b = (A.a+1)*B.b + A.b*(B.e+1) + r.c = (A.a+1)*B.c + A.b*B.f + A.c + r.d = A.d*(B.a+1) + (A.e+1)*B.d + r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1 + r.f = A.d*B.c + (A.e+1)*B.f + A.f + return r +} + +// Invert the transformation. Note that if the matrix is close to singular +// numerical errors may become large or infinity. +func (a Affine2D) Invert() Affine2D { + if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 { + return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f} + } + a.a += 1 + a.e += 1 + det := a.a*a.e - a.b*a.d + a.a, a.e = a.e/det, a.a/det + a.b, a.d = -a.b/det, -a.d/det + temp := a.c + a.c = -a.a*a.c - a.b*a.f + a.f = -a.d*temp - a.e*a.f + a.a -= 1 + a.e -= 1 + return a +} + +// Transform p by returning a*p. +func (a Affine2D) Transform(p Point) Point { + return Point{ + X: p.X*(a.a+1) + p.Y*a.b + a.c, + Y: p.X*a.d + p.Y*(a.e+1) + a.f, + } +} + +// Elems returns the matrix elements of the transform in row-major order. The +// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. +func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) { + return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f +} + +func (a Affine2D) scale(factor Point) Affine2D { + return Affine2D{ + (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, + a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y, + } +} + +func (a Affine2D) rotate(radians float32) Affine2D { + sin, cos := math.Sincos(float64(radians)) + s, c := float32(sin), float32(cos) + return Affine2D{ + (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s, + (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c, + } +} + +func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { + tx := float32(math.Tan(float64(radiansX))) + ty := float32(math.Tan(float64(radiansY))) + return Affine2D{ + (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx, + (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f, + } +} + +func (a Affine2D) String() string { + sx, hx, ox, hy, sy, oy := a.Elems() + return fmt.Sprintf("[[%f %f %f] [%f %f %f]]", sx, hx, ox, hy, sy, oy) +} diff --git a/pkg/gel/gio/f32/affine_test.go b/pkg/gel/gio/f32/affine_test.go new file mode 100644 index 0000000..4077b8d --- /dev/null +++ b/pkg/gel/gio/f32/affine_test.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +import ( + "math" + "testing" +) + +func eq(p1, p2 Point) bool { + tol := 1e-5 + dx, dy := p2.X-p1.X, p2.Y-p1.Y + return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol +} + +func eqaff(x, y Affine2D) bool { + tol := 1e-5 + return math.Abs(float64(x.a-y.a)) < tol && + math.Abs(float64(x.b-y.b)) < tol && + math.Abs(float64(x.c-y.c)) < tol && + math.Abs(float64(x.d-y.d)) < tol && + math.Abs(float64(x.e-y.e)) < tol && + math.Abs(float64(x.f-y.f)) < tol +} + +func TestTransformOffset(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + + r := Affine2D{}.Offset(o).Transform(p) + if !eq(r, Pt(3, -1)) { + t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r) + } + i := Affine2D{}.Offset(o).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformScale(t *testing.T) { + p := Point{X: 1, Y: 2} + s := Point{X: -1, Y: 2} + + r := Affine2D{}.Scale(Point{}, s).Transform(p) + if !eq(r, Pt(-1, 4)) { + t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r) + } + i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformRotate(t *testing.T) { + p := Point{X: 1, Y: 0} + a := float32(math.Pi / 2) + + r := Affine2D{}.Rotate(Point{}, a).Transform(p) + if !eq(r, Pt(0, 1)) { + t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r) + } + i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformShear(t *testing.T) { + p := Point{X: 1, Y: 1} + + r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(2, 1)) { + t.Errorf("shear transformation mismatch: have %v, want {2 1}", r) + } + i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestTransformMultiply(t *testing.T) { + p := Point{X: 1, Y: 2} + o := Point{X: 2, Y: -3} + s := Point{X: -1, Y: 2} + a := float32(-math.Pi / 2) + + r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p) + if !eq(r, Pt(1, 3)) { + t.Errorf("complex transformation mismatch: have %v, want {1 3}", r) + } + i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) + if !eq(i, p) { + t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p) + } +} + +func TestPrimes(t *testing.T) { + xa := NewAffine2D(9, 11, 13, 17, 19, 23) + xb := NewAffine2D(29, 31, 37, 43, 47, 53) + + pa := Point{X: 2, Y: 3} + pb := Point{X: 5, Y: 7} + + for _, test := range []struct { + x Affine2D + p Point + exp Point + }{ + {x: xa, p: pa, exp: Pt(64, 114)}, + {x: xa, p: pb, exp: Pt(135, 241)}, + {x: xb, p: pa, exp: Pt(188, 280)}, + {x: xb, p: pb, exp: Pt(399, 597)}, + } { + got := test.x.Transform(test.p) + if !eq(got, test.exp) { + t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp) + } + } + + for _, test := range []struct { + x Affine2D + exp Affine2D + }{ + {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)}, + {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)}, + } { + got := test.x.Invert() + if !eqaff(got, test.exp) { + t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp) + } + } + + got := xa.Mul(xb) + exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659) + if !eqaff(got, exp) { + t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp) + } +} + +func TestTransformScaleAround(t *testing.T) { + p := Pt(-1, -1) + target := Pt(-6, -13) + pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Scale not as expected") + } +} + +func TestTransformRotateAround(t *testing.T) { + p := Pt(-1, -1) + pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p) + target := Pt(-1, 3) + if !eq(pt, target) { + t.Log(pt, "!=", target) + t.Error("Rotate not as expected") + } +} + +func TestMulOrder(t *testing.T) { + A := Affine2D{}.Offset(Pt(100, 100)) + B := Affine2D{}.Scale(Point{}, Pt(2, 2)) + _ = A + _ = B + + T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2)) + T2 := B.Mul(A) + + if T1 != T2 { + t.Log(T1) + t.Log(T2) + t.Error("multiplication / transform order not as expected") + } +} + +func BenchmarkTransformOffset(b *testing.B) { + p := Point{X: 1, Y: 2} + o := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Offset(o) + + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformScale(b *testing.B) { + p := Point{X: 1, Y: 2} + s := Point{X: 0.5, Y: 0.5} + aff := Affine2D{}.Scale(Point{}, s) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformRotate(b *testing.B) { + p := Point{X: 1, Y: 2} + a := float32(math.Pi / 2) + aff := Affine2D{}.Rotate(Point{}, a) + for i := 0; i < b.N; i++ { + p = aff.Transform(p) + } + _ = p +} + +func BenchmarkTransformTranslateMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformScaleMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5}) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} + +func BenchmarkTransformMultiply(b *testing.B) { + a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) + t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7) + + for i := 0; i < b.N; i++ { + a = a.Mul(t) + } +} diff --git a/pkg/gel/gio/f32/f32.go b/pkg/gel/gio/f32/f32.go new file mode 100644 index 0000000..69745ba --- /dev/null +++ b/pkg/gel/gio/f32/f32.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package f32 is a float32 implementation of package image's +Point and Rectangle. + +The coordinate space has the origin in the top left +corner with the axes extending right and down. +*/ +package f32 + +import "strconv" + +// A Point is a two dimensional point. +type Point struct { + X, Y float32 +} + +// String return a string representation of p. +func (p Point) String() string { + return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) + + "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")" +} + +// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X, +// Min.Y <= Y < Max.Y. +type Rectangle struct { + Min, Max Point +} + +// String return a string representation of r. +func (r Rectangle) String() string { + return r.Min.String() + "-" + r.Max.String() +} + +// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}. +// The returned Rectangle has x0 and y0 swapped if necessary so that +// it's correctly formed. +func Rect(x0, y0, x1, y1 float32) Rectangle { + if x0 > x1 { + x0, x1 = x1, x0 + } + if y0 > y1 { + y0, y1 = y1, y0 + } + return Rectangle{Point{x0, y0}, Point{x1, y1}} +} + +// Pt is shorthand for Point{X: x, Y: y}. +func Pt(x, y float32) Point { + return Point{X: x, Y: y} +} + +// Add return the point p+p2. +func (p Point) Add(p2 Point) Point { + return Point{X: p.X + p2.X, Y: p.Y + p2.Y} +} + +// Sub returns the vector p-p2. +func (p Point) Sub(p2 Point) Point { + return Point{X: p.X - p2.X, Y: p.Y - p2.Y} +} + +// Mul returns p scaled by s. +func (p Point) Mul(s float32) Point { + return Point{X: p.X * s, Y: p.Y * s} +} + +// In reports whether p is in r. +func (p Point) In(r Rectangle) bool { + return r.Min.X <= p.X && p.X < r.Max.X && + r.Min.Y <= p.Y && p.Y < r.Max.Y +} + +// Size returns r's width and height. +func (r Rectangle) Size() Point { + return Point{X: r.Dx(), Y: r.Dy()} +} + +// Dx returns r's width. +func (r Rectangle) Dx() float32 { + return r.Max.X - r.Min.X +} + +// Dy returns r's Height. +func (r Rectangle) Dy() float32 { + return r.Max.Y - r.Min.Y +} + +// Intersect returns the intersection of r and s. +func (r Rectangle) Intersect(s Rectangle) Rectangle { + if r.Min.X < s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y < s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X > s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y > s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +// Union returns the union of r and s. +func (r Rectangle) Union(s Rectangle) Rectangle { + if r.Min.X > s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y > s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X < s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y < s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +// Canon returns the canonical version of r, where Min is to +// the upper left of Max. +func (r Rectangle) Canon() Rectangle { + if r.Max.X < r.Min.X { + r.Min.X, r.Max.X = r.Max.X, r.Min.X + } + if r.Max.Y < r.Min.Y { + r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y + } + return r +} + +// Empty reports whether r represents the empty area. +func (r Rectangle) Empty() bool { + return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y +} + +// Add offsets r with the vector p. +func (r Rectangle) Add(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X + p.X, r.Min.Y + p.Y}, + Point{r.Max.X + p.X, r.Max.Y + p.Y}, + } +} + +// Sub offsets r with the vector -p. +func (r Rectangle) Sub(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X - p.X, r.Min.Y - p.Y}, + Point{r.Max.X - p.X, r.Max.Y - p.Y}, + } +} diff --git a/pkg/gel/gio/font/gofont/gofont.go b/pkg/gel/gio/font/gofont/gofont.go new file mode 100644 index 0000000..066b8c9 --- /dev/null +++ b/pkg/gel/gio/font/gofont/gofont.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package gofont exports the Go fonts as a text.Collection. +// +// See https://blog.golang.org/go-fonts for a description of the +// fonts, and the golang.org/x/image/font/gofont packages for the +// font data. +package gofont + +import ( + "fmt" + "sync" + + "golang.org/x/image/font/gofont/gobold" + "golang.org/x/image/font/gofont/gobolditalic" + "golang.org/x/image/font/gofont/goitalic" + "golang.org/x/image/font/gofont/gomedium" + "golang.org/x/image/font/gofont/gomediumitalic" + "golang.org/x/image/font/gofont/gomono" + "golang.org/x/image/font/gofont/gomonobold" + "golang.org/x/image/font/gofont/gomonobolditalic" + "golang.org/x/image/font/gofont/gomonoitalic" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/gofont/gosmallcaps" + "golang.org/x/image/font/gofont/gosmallcapsitalic" + + "github.com/p9c/p9/pkg/gel/gio/font/opentype" + "github.com/p9c/p9/pkg/gel/gio/text" +) + +var ( + once sync.Once + collection []text.FontFace +) + +func Collection() []text.FontFace { + once.Do(func() { + register(text.Font{}, goregular.TTF) + register(text.Font{Style: text.Italic}, goitalic.TTF) + register(text.Font{Weight: text.Bold}, gobold.TTF) + register(text.Font{Style: text.Italic, Weight: text.Bold}, gobolditalic.TTF) + register(text.Font{Weight: text.Medium}, gomedium.TTF) + register(text.Font{Weight: text.Medium, Style: text.Italic}, gomediumitalic.TTF) + register(text.Font{Variant: "Mono"}, gomono.TTF) + register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF) + register(text.Font{Variant: "Mono", Weight: text.Bold, Style: text.Italic}, gomonobolditalic.TTF) + register(text.Font{Variant: "Mono", Style: text.Italic}, gomonoitalic.TTF) + register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF) + register(text.Font{Variant: "Smallcaps", Style: text.Italic}, gosmallcapsitalic.TTF) + // Ensure that any outside appends will not reuse the backing store. + n := len(collection) + collection = collection[:n:n] + }) + return collection +} + +func register(fnt text.Font, ttf []byte) { + face, err := opentype.Parse(ttf) + if err != nil { + panic(fmt.Errorf("failed to parse font: %v", err)) + } + fnt.Typeface = "Go" + collection = append(collection, text.FontFace{Font: fnt, Face: face}) +} diff --git a/pkg/gel/gio/font/opentype/opentype.go b/pkg/gel/gio/font/opentype/opentype.go new file mode 100644 index 0000000..1473043 --- /dev/null +++ b/pkg/gel/gio/font/opentype/opentype.go @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package opentype implements text layout and shaping for OpenType +// files. +package opentype + +import ( + "bytes" + "io" + "unicode" + "unicode/utf8" + + "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/text" +) + +// Font implements text.Face. Its methods are safe to use +// concurrently. +type Font struct { + font *sfnt.Font +} + +// Collection is a collection of one or more fonts. When used as a text.Face, +// each rune will be assigned a glyph from the first font in the collection +// that supports it. +type Collection struct { + fonts []*opentype +} + +type opentype struct { + Font *sfnt.Font + Hinting font.Hinting +} + +// a glyph represents a rune and its advance according to a Font. +// TODO: remove this type and work on io.Readers directly. +type glyph struct { + Rune rune + Advance fixed.Int26_6 +} + +// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte +// data source. +func Parse(src []byte) (*Font, error) { + fnt, err := sfnt.Parse(src) + if err != nil { + return nil, err + } + return &Font{font: fnt}, nil +} + +// ParseCollection parses an SFNT font collection, such as TTC or OTC data, +// from a []byte data source. +// +// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, +// it will return a collection containing 1 font. +func ParseCollection(src []byte) (*Collection, error) { + c, err := sfnt.ParseCollection(src) + if err != nil { + return nil, err + } + return newCollectionFrom(c) +} + +// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data, +// from an io.ReaderAt data source. +// +// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it +// will return a collection containing 1 font. +func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) { + c, err := sfnt.ParseCollectionReaderAt(src) + if err != nil { + return nil, err + } + return newCollectionFrom(c) +} + +func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) { + fonts := make([]*opentype, coll.NumFonts()) + for i := range fonts { + fnt, err := coll.Font(i) + if err != nil { + return nil, err + } + fonts[i] = &opentype{ + Font: fnt, + Hinting: font.HintingFull, + } + } + return &Collection{fonts: fonts}, nil +} + +// NumFonts returns the number of fonts in the collection. +func (c *Collection) NumFonts() int { + return len(c.fonts) +} + +// Font returns the i'th font in the collection. +func (c *Collection) Font(i int) (*Font, error) { + if i < 0 || len(c.fonts) <= i { + return nil, sfnt.ErrNotFound + } + return &Font{font: c.fonts[i].Font}, nil +} + +func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.Line, error) { + glyphs, err := readGlyphs(txt) + if err != nil { + return nil, err + } + fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}} + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, fonts, glyphs) +} + +func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp { + var buf sfnt.Buffer + return textPath(&buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str) +} + +func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { + o := &opentype{Font: f.font, Hinting: font.HintingFull} + var buf sfnt.Buffer + return o.Metrics(&buf, ppem) +} + +func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.Line, error) { + glyphs, err := readGlyphs(txt) + if err != nil { + return nil, err + } + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs) +} + +func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp { + var buf sfnt.Buffer + return textPath(&buf, ppem, c.fonts, str) +} + +func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype { + if len(fonts) < 1 { + return nil + } + for _, f := range fonts { + if f.HasGlyph(buf, r) { + return f + } + } + return fonts[0] // Use replacement character from the first font if necessary +} + +func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*opentype, glyphs []glyph) ([]text.Line, error) { + var lines []text.Line + var nextLine text.Line + updateBounds := func(f *opentype) { + m := f.Metrics(sbuf, ppem) + if m.Ascent > nextLine.Ascent { + nextLine.Ascent = m.Ascent + } + // m.Height is equal to m.Ascent + m.Descent + linegap. + // Compute the descent including the linegap. + descent := m.Height - m.Ascent + if descent > nextLine.Descent { + nextLine.Descent = descent + } + b := f.Bounds(sbuf, ppem) + nextLine.Bounds = nextLine.Bounds.Union(b) + } + maxDotX := fixed.I(maxWidth) + type state struct { + r rune + f *opentype + adv fixed.Int26_6 + x fixed.Int26_6 + idx int + len int + valid bool + } + var prev, word state + endLine := func() { + if prev.f == nil && len(fonts) > 0 { + prev.f = fonts[0] + } + updateBounds(prev.f) + nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx]) + nextLine.Width = prev.x + prev.adv + nextLine.Bounds.Max.X += prev.x + lines = append(lines, nextLine) + glyphs = glyphs[prev.idx:] + nextLine = text.Line{} + prev = state{} + word = state{} + } + for prev.idx < len(glyphs) { + g := &glyphs[prev.idx] + next := state{ + r: g.Rune, + f: fontForGlyph(sbuf, fonts, g.Rune), + idx: prev.idx + 1, + len: prev.len + utf8.RuneLen(g.Rune), + x: prev.x + prev.adv, + } + if next.f != nil { + if next.f != prev.f { + updateBounds(next.f) + } + next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune) + } + if g.Rune == '\n' { + // The newline is zero width; use the previous + // character for line measurements. + prev.idx = next.idx + prev.len = next.len + endLine() + continue + } + var k fixed.Int26_6 + if prev.valid && next.f != nil { + k = next.f.Kern(sbuf, ppem, prev.r, next.r) + } + // Break the line if we're out of space. + if prev.idx > 0 && next.x+next.adv+k > maxDotX { + // If the line contains no word breaks, break off the last rune. + if word.idx == 0 { + word = prev + } + next.x -= word.x + word.adv + next.idx -= word.idx + next.len -= word.len + prev = word + endLine() + } else if k != 0 { + glyphs[prev.idx-1].Advance += k + next.x += k + } + g.Advance = next.adv + if unicode.IsSpace(g.Rune) { + word = next + } + prev = next + } + endLine() + return lines, nil +} + +// toLayout converts a slice of glyphs to a text.Layout. +func toLayout(glyphs []glyph) text.Layout { + var buf bytes.Buffer + advs := make([]fixed.Int26_6, len(glyphs)) + for i, g := range glyphs { + buf.WriteRune(g.Rune) + advs[i] = glyphs[i].Advance + } + return text.Layout{Text: buf.String(), Advances: advs} +} + +func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str text.Layout) op.CallOp { + var lastPos f32.Point + var builder clip.Path + ops := new(op.Ops) + m := op.Record(ops) + var x fixed.Int26_6 + builder.Begin(ops) + rune := 0 + for _, r := range str.Text { + if !unicode.IsSpace(r) { + f := fontForGlyph(buf, fonts, r) + if f == nil { + continue + } + segs, ok := f.LoadGlyph(buf, ppem, r) + if !ok { + continue + } + // Move to glyph position. + pos := f32.Point{ + X: float32(x) / 64, + } + builder.Move(pos.Sub(lastPos)) + lastPos = pos + var lastArg f32.Point + // Convert sfnt.Segments to relative segments. + for _, fseg := range segs { + nargs := 1 + switch fseg.Op { + case sfnt.SegmentOpQuadTo: + nargs = 2 + case sfnt.SegmentOpCubeTo: + nargs = 3 + } + var args [3]f32.Point + for i := 0; i < nargs; i++ { + a := f32.Point{ + X: float32(fseg.Args[i].X) / 64, + Y: float32(fseg.Args[i].Y) / 64, + } + args[i] = a.Sub(lastArg) + if i == nargs-1 { + lastArg = a + } + } + switch fseg.Op { + case sfnt.SegmentOpMoveTo: + builder.Move(args[0]) + case sfnt.SegmentOpLineTo: + builder.Line(args[0]) + case sfnt.SegmentOpQuadTo: + builder.Quad(args[0], args[1]) + case sfnt.SegmentOpCubeTo: + builder.Cube(args[0], args[1], args[2]) + default: + panic("unsupported segment op") + } + } + lastPos = lastPos.Add(lastArg) + } + x += str.Advances[rune] + rune++ + } + clip.Outline{ + Path: builder.End(), + }.Op().Add(ops) + return m.Stop() +} + +func readGlyphs(r io.Reader) ([]glyph, error) { + var glyphs []glyph + buf := make([]byte, 0, 1024) + for { + n, err := r.Read(buf[len(buf):cap(buf)]) + buf = buf[:len(buf)+n] + lim := len(buf) + // Read full runes if possible. + if err != io.EOF { + lim -= utf8.UTFMax - 1 + } + i := 0 + for i < lim { + c, s := utf8.DecodeRune(buf[i:]) + i += s + glyphs = append(glyphs, glyph{Rune: c}) + } + n = copy(buf, buf[i:]) + buf = buf[:n] + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + } + return glyphs, nil +} + +func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool { + g, err := f.Font.GlyphIndex(buf, r) + return g != 0 && err == nil +} + +func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return 0, false + } + adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting) + return adv, err == nil +} + +func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, r0, r1 rune) fixed.Int26_6 { + g0, err := f.Font.GlyphIndex(buf, r0) + if err != nil { + return 0 + } + g1, err := f.Font.GlyphIndex(buf, r1) + if err != nil { + return 0 + } + adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting) + if err != nil { + return 0 + } + return adv +} + +func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics { + m, _ := f.Font.Metrics(buf, ppem, f.Hinting) + return m +} + +func (f *opentype) Bounds(buf *sfnt.Buffer, ppem fixed.Int26_6) fixed.Rectangle26_6 { + r, _ := f.Font.Bounds(buf, ppem, f.Hinting) + return r +} + +func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) ([]sfnt.Segment, bool) { + g, err := f.Font.GlyphIndex(buf, r) + if err != nil { + return nil, false + } + segs, err := f.Font.LoadGlyph(buf, g, ppem, nil) + if err != nil { + return nil, false + } + return segs, true +} diff --git a/pkg/gel/gio/font/opentype/opentype_test.go b/pkg/gel/gio/font/opentype/opentype_test.go new file mode 100644 index 0000000..9537d42 --- /dev/null +++ b/pkg/gel/gio/font/opentype/opentype_test.go @@ -0,0 +1,213 @@ +package opentype + +import ( + "bytes" + "compress/gzip" + "encoding/binary" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "golang.org/x/image/font" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/text" +) + +func TestCollectionAsFace(t *testing.T) { + // Load two fonts with disjoint glyphs. Font 1 supports only '1', and font 2 supports only '2'. + // The fonts have different glyphs for the replacement character (".notdef"). + font1, ttf1, err := decompressFontFile("testdata/only1.ttf.gz") + if err != nil { + t.Fatalf("failed to load test font 1: %v", err) + } + font2, ttf2, err := decompressFontFile("testdata/only2.ttf.gz") + if err != nil { + t.Fatalf("failed to load test font 2: %v", err) + } + + otc := mergeFonts(ttf1, ttf2) + coll, err := ParseCollection(otc) + if err != nil { + t.Fatalf("failed to load merged test font: %v", err) + } + + shapeValid1, err := shapeRune(font1, '1') + if err != nil { + t.Fatalf("failed shaping valid glyph with font 1: %v", err) + } + shapeInvalid1, err := shapeRune(font1, '3') + if err != nil { + t.Fatalf("failed shaping invalid glyph with font 1: %v", err) + } + shapeValid2, err := shapeRune(font2, '2') + if err != nil { + t.Fatalf("failed shaping valid glyph with font 2: %v", err) + } + shapeInvalid2, err := shapeRune(font2, '3') // Same invalid glyph as before to test replacement glyph difference + if err != nil { + t.Fatalf("failed shaping invalid glyph with font 2: %v", err) + } + shapeCollValid1, err := shapeRune(coll, '1') + if err != nil { + t.Fatalf("failed shaping valid glyph for font 1 with font collection: %v", err) + } + shapeCollValid2, err := shapeRune(coll, '2') + if err != nil { + t.Fatalf("failed shaping valid glyph for font 2 with font collection: %v", err) + } + shapeCollInvalid, err := shapeRune(coll, '4') // Different invalid glyph to confirm use of the replacement glyph + if err != nil { + t.Fatalf("failed shaping invalid glyph with font collection: %v", err) + } + + // All shapes from the original fonts should be distinct because the glyphs are distinct, including the replacement + // glyphs. + distinctShapes := []op.CallOp{shapeValid1, shapeInvalid1, shapeValid2, shapeInvalid2} + for i := 0; i < len(distinctShapes); i++ { + for j := i + 1; j < len(distinctShapes); j++ { + if areShapesEqual(distinctShapes[i], distinctShapes[j]) { + t.Errorf("font shapes %d and %d are not distinct", i, j) + } + } + } + + // Font collections should render glyphs from the first supported font. Replacement glyphs should come from the + // first font in all cases. + if !areShapesEqual(shapeCollValid1, shapeValid1) { + t.Error("font collection did not render the valid glyph using font 1") + } + if !areShapesEqual(shapeCollValid2, shapeValid2) { + t.Error("font collection did not render the valid glyph using font 2") + } + if !areShapesEqual(shapeCollInvalid, shapeInvalid1) { + t.Error("font collection did not render the invalid glyph using the replacement from font 1") + } +} + +func TestEmptyString(t *testing.T) { + face, err := Parse(goregular.TTF) + if err != nil { + t.Fatal(err) + } + + ppem := fixed.I(200) + + lines, err := face.Layout(ppem, 2000, strings.NewReader("")) + if err != nil { + t.Fatal(err) + } + if len(lines) == 0 { + t.Fatalf("Layout returned no lines for empty string; expected 1") + } + l := lines[0] + exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull) + if err != nil { + t.Fatal(err) + } + if got := l.Bounds; got != exp { + t.Errorf("got bounds %+v for empty string; expected %+v", got, exp) + } +} + +func decompressFontFile(name string) (*Font, []byte, error) { + f, err := os.Open(name) + if err != nil { + return nil, nil, fmt.Errorf("could not open file for reading: %s: %v", name, err) + } + defer f.Close() + gz, err := gzip.NewReader(f) + if err != nil { + return nil, nil, fmt.Errorf("font file contains invalid gzip data: %v", err) + } + src, err := ioutil.ReadAll(gz) + if err != nil { + return nil, nil, fmt.Errorf("failed to decompress font file: %v", err) + } + fnt, err := Parse(src) + if err != nil { + return nil, nil, fmt.Errorf("file did not contain a valid font: %v", err) + } + return fnt, src, nil +} + +// mergeFonts produces a trivial OpenType Collection (OTC) file for two source fonts. +// It makes many assumptions and is not meant for general use. +// For file format details, see https://docs.microsoft.com/en-us/typography/opentype/spec/otff +// For a robust tool to generate these files, see https://pypi.org/project/afdko/ +func mergeFonts(ttf1, ttf2 []byte) []byte { + // Locations to place the two embedded fonts. All of the offsets to the fonts' internal tables will need to be + // shifted from the start of the file by the appropriate amount, and then everything will work as expected. + offset1 := uint32(20) // Length of OpenType collection headers + offset2 := offset1 + uint32(len(ttf1)) + + var buf bytes.Buffer + _, _ = buf.Write([]byte("ttcf\x00\x01\x00\x00\x00\x00\x00\x02")) + _ = binary.Write(&buf, binary.BigEndian, offset1) + _ = binary.Write(&buf, binary.BigEndian, offset2) + + // Inline function to copy a font into the collection verbatim, except for adding an offset to all of the font's + // table positions. + copyOffsetTTF := func(ttf []byte, offset uint32) { + _, _ = buf.Write(ttf[:12]) + numTables := binary.BigEndian.Uint16(ttf[4:6]) + for i := uint16(0); i < numTables; i++ { + p := 12 + 16*i + _, _ = buf.Write(ttf[p : p+8]) + tblLoc := binary.BigEndian.Uint32(ttf[p+8:p+12]) + offset + _ = binary.Write(&buf, binary.BigEndian, tblLoc) + _, _ = buf.Write(ttf[p+12 : p+16]) + } + _, _ = buf.Write(ttf[12+16*numTables:]) + } + copyOffsetTTF(ttf1, offset1) + copyOffsetTTF(ttf2, offset2) + + return buf.Bytes() +} + +// shapeRune uses a given Face to shape exactly one rune at a fixed size, then returns the resulting shape data. +func shapeRune(f text.Face, r rune) (op.CallOp, error) { + ppem := fixed.I(200) + lines, err := f.Layout(ppem, 2000, strings.NewReader(string(r))) + if err != nil { + return op.CallOp{}, err + } + if len(lines) != 1 { + return op.CallOp{}, fmt.Errorf("unexpected rendering for \"U+%08X\": got %d lines (expected: 1)", r, len(lines)) + } + return f.Shape(ppem, lines[0].Layout), nil +} + +// areShapesEqual returns true iff both given text shapes are produced with identical operations. +func areShapesEqual(shape1, shape2 op.CallOp) bool { + var ops1, ops2 op.Ops + shape1.Add(&ops1) + shape2.Add(&ops2) + var r1, r2 ops.Reader + r1.Reset(&ops1) + r2.Reset(&ops2) + for { + encOp1, ok1 := r1.Decode() + encOp2, ok2 := r2.Decode() + if ok1 != ok2 { + return false + } + if !ok1 { + break + } + if len(encOp1.Refs) > 0 || len(encOp2.Refs) > 0 { + panic("unexpected ops with refs in font shaping test") + } + if !bytes.Equal(encOp1.Data, encOp2.Data) { + return false + } + } + return true +} diff --git a/pkg/gel/gio/font/opentype/testdata/only1.ttf.gz b/pkg/gel/gio/font/opentype/testdata/only1.ttf.gz new file mode 100644 index 0000000..544159d Binary files /dev/null and b/pkg/gel/gio/font/opentype/testdata/only1.ttf.gz differ diff --git a/pkg/gel/gio/font/opentype/testdata/only2.ttf.gz b/pkg/gel/gio/font/opentype/testdata/only2.ttf.gz new file mode 100644 index 0000000..87a3e68 Binary files /dev/null and b/pkg/gel/gio/font/opentype/testdata/only2.ttf.gz differ diff --git a/pkg/gel/gio/gesture/gesture.go b/pkg/gel/gio/gesture/gesture.go new file mode 100644 index 0000000..5b25edf --- /dev/null +++ b/pkg/gel/gio/gesture/gesture.go @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package gesture implements common pointer gestures. + +Gestures accept low level pointer Events from an event +Queue and detect higher level actions such as clicks +and scrolling. +*/ +package gesture + +import ( + "image" + "math" + "runtime" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "github.com/p9c/p9/pkg/gel/gio/internal/fling" +) + +// The duration is somewhat arbitrary. +const doubleClickDuration = 300 * time.Millisecond + +// Click detects click gestures in the form +// of ClickEvents. +type Click struct { + // clickedAt is the timestamp at which + // the last click occurred. + clickedAt time.Duration + // clicks is incremented if successive clicks + // are performed within a fixed duration. + clicks int + // pressed tracks whether the pointer is pressed. + pressed bool + // entered tracks whether the pointer is inside the gesture. + entered bool + // pid is the pointer.ID. + pid pointer.ID + Button pointer.Buttons +} + +type ClickState uint8 + +// ClickEvent represent a click action, either a +// TypePress for the beginning of a click or a +// TypeClick for a completed click. +type ClickEvent struct { + Type ClickType + Position f32.Point + Source pointer.Source + Modifiers key.Modifiers + // NumClicks records successive clicks occurring + // within a short duration of each other. + NumClicks int + Button pointer.Buttons +} + +type ClickType uint8 + +// Drag detects drag gestures in the form of pointer.Drag events. +type Drag struct { + dragging bool + pid pointer.ID + start f32.Point + grab bool +} + +// Scroll detects scroll gestures and reduces them to +// scroll distances. Scroll recognizes mouse wheel +// movements as well as drag and fling touch gestures. +type Scroll struct { + dragging bool + axis Axis + estimator fling.Extrapolation + flinger fling.Animation + pid pointer.ID + grab bool + last int + // Leftover scroll. + scroll float32 +} + +type ScrollState uint8 + +type Axis uint8 + +const ( + Horizontal Axis = iota + Vertical + Both +) + +const ( + // TypePress is reported for the first pointer + // press. + TypePress ClickType = iota + // TypeClick is reported when a click action + // is complete. + TypeClick + // TypeCancel is reported when the gesture is + // cancelled. + TypeCancel +) + +const ( + // StateIdle is the default scroll state. + StateIdle ScrollState = iota + // StateDrag is reported during drag gestures. + StateDragging + // StateFlinging is reported when a fling is + // in progress. + StateFlinging +) + +var touchSlop = unit.Dp(3) + +// Add the handler to the operation list to receive click events. +func (c *Click) Add(ops *op.Ops) { + op := pointer.InputOp{ + Tag: c, + Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave, + } + op.Add(ops) +} + +// Hovered returns whether a pointer is inside the area. +func (c *Click) Hovered() bool { + return c.entered +} + +// Pressed returns whether a pointer is pressing. +func (c *Click) Pressed() bool { + return c.pressed +} + +// Events returns the next click event, if any. +func (c *Click) Events(q event.Queue) []ClickEvent { + var events []ClickEvent + for _, evt := range q.Events(c) { + // I.S(evt) + e, ok := evt.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Release: + if !c.pressed || c.pid != e.PointerID { + break + } + c.pressed = false + if c.entered { + if e.Time-c.clickedAt < doubleClickDuration || + (c.clicks == 2 && e.Time-c.clickedAt < doubleClickDuration*2) { + c.clicks++ + } else { + c.clicks = 1 + } + c.clickedAt = e.Time + events = append(events, ClickEvent{ + Type: TypeClick, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers, + Button: e.Buttons, NumClicks: c.clicks, + }) + } else { + events = append(events, ClickEvent{Type: TypeCancel}) + } + case pointer.Cancel: + wasPressed := c.pressed + c.pressed = false + c.entered = false + if wasPressed { + events = append(events, ClickEvent{Type: TypeCancel}) + } + case pointer.Press: + if c.pressed { + break + } + // if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary { + // break + // } + if !c.entered { + c.pid = e.PointerID + } + if c.pid != e.PointerID { + break + } + c.pressed = true + events = append(events, ClickEvent{ + Type: TypePress, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers, Button: e.Buttons, + }) + case pointer.Leave: + if !c.pressed { + c.pid = e.PointerID + } + if c.pid == e.PointerID { + c.entered = false + } + case pointer.Enter: + if !c.pressed { + c.pid = e.PointerID + } + if c.pid == e.PointerID { + c.entered = true + } + } + } + return events +} + +func (ClickEvent) ImplementsEvent() {} + +// Add the handler to the operation list to receive scroll events. +func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) { + oph := pointer.InputOp{ + Tag: s, + Grab: s.grab, + Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll, + ScrollBounds: bounds, + } + oph.Add(ops) + if s.flinger.Active() { + op.InvalidateOp{}.Add(ops) + } +} + +// Stop any remaining fling movement. +func (s *Scroll) Stop() { + s.flinger = fling.Animation{} +} + +// Scroll detects the scrolling distance from the available events and +// ongoing fling gestures. +func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int { + if s.axis != axis { + s.axis = axis + return 0 + } + total := 0 + for _, evt := range q.Events(s) { + e, ok := evt.(pointer.Event) + if !ok { + continue + } + switch e.Type { + case pointer.Press: + if s.dragging { + break + } + // Only scroll on touch drags, or on Android where mice + // drags also scroll by convention. + if e.Source != pointer.Touch && runtime.GOOS != "android" { + break + } + s.Stop() + s.estimator = fling.Extrapolation{} + v := s.val(e.Position) + s.last = int(math.Round(float64(v))) + s.estimator.Sample(e.Time, v) + s.dragging = true + s.pid = e.PointerID + case pointer.Release: + if s.pid != e.PointerID { + break + } + fling := s.estimator.Estimate() + if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop { + s.flinger.Start(cfg, t, fling.Velocity) + } + fallthrough + case pointer.Cancel: + s.dragging = false + s.grab = false + case pointer.Scroll: + switch s.axis { + case Horizontal: + s.scroll += e.Scroll.X + case Vertical: + s.scroll += e.Scroll.Y + } + iscroll := int(s.scroll) + s.scroll -= float32(iscroll) + total += iscroll + case pointer.Drag: + if !s.dragging || s.pid != e.PointerID { + continue + } + val := s.val(e.Position) + s.estimator.Sample(e.Time, val) + v := int(math.Round(float64(val))) + dist := s.last - v + if e.Priority < pointer.Grabbed { + slop := cfg.Px(touchSlop) + if dist := dist; dist >= slop || -slop >= dist { + s.grab = true + } + } else { + s.last = v + total += dist + } + } + } + total += s.flinger.Tick(t) + return total +} + +func (s *Scroll) val(p f32.Point) float32 { + if s.axis == Horizontal { + return p.X + } else { + return p.Y + } +} + +// State reports the scroll state. +func (s *Scroll) State() ScrollState { + switch { + case s.flinger.Active(): + return StateFlinging + case s.dragging: + return StateDragging + default: + return StateIdle + } +} + +// Add the handler to the operation list to receive drag events. +func (d *Drag) Add(ops *op.Ops) { + op := pointer.InputOp{ + Tag: d, + Grab: d.grab, + Types: pointer.Press | pointer.Drag | pointer.Release, + } + op.Add(ops) +} + +// Events returns the next drag events, if any. +func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event { + var events []pointer.Event + for _, e := range q.Events(d) { + e, ok := e.(pointer.Event) + if !ok { + continue + } + + switch e.Type { + case pointer.Press: + if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) { + continue + } + if d.dragging { + continue + } + d.dragging = true + d.pid = e.PointerID + d.start = e.Position + case pointer.Drag: + if !d.dragging || e.PointerID != d.pid { + continue + } + switch axis { + case Horizontal: + e.Position.Y = d.start.Y + case Vertical: + e.Position.X = d.start.X + case Both: + // Do nothing + } + if e.Priority < pointer.Grabbed { + diff := e.Position.Sub(d.start) + slop := cfg.Px(touchSlop) + if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { + d.grab = true + } + } + case pointer.Release, pointer.Cancel: + if !d.dragging || e.PointerID != d.pid { + continue + } + d.dragging = false + d.grab = false + } + + events = append(events, e) + } + + return events +} + +// Dragging reports whether it's currently in use. +func (d *Drag) Dragging() bool { return d.dragging } + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("invalid Axis") + } +} + +func (ct ClickType) String() string { + switch ct { + case TypePress: + return "TypePress" + case TypeClick: + return "TypeClick" + case TypeCancel: + return "TypeCancel" + default: + panic("invalid ClickType") + } +} + +func (s ScrollState) String() string { + switch s { + case StateIdle: + return "StateIdle" + case StateDragging: + return "StateDragging" + case StateFlinging: + return "StateFlinging" + default: + panic("unreachable") + } +} diff --git a/pkg/gel/gio/gesture/gesture_test.go b/pkg/gel/gio/gesture/gesture_test.go new file mode 100644 index 0000000..65bf871 --- /dev/null +++ b/pkg/gel/gio/gesture/gesture_test.go @@ -0,0 +1,87 @@ +package gesture + +import ( + "testing" + "time" + + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/io/router" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestMouseClicks(t *testing.T) { + for _, tc := range []struct { + label string + events []event.Event + clicks []int // number of combined clicks per click (single, double...) + }{ + { + label: "single click", + events: mouseClickEvents(200 * time.Millisecond), + clicks: []int{1}, + }, + { + label: "double click", + events: mouseClickEvents( + 100*time.Millisecond, + 100*time.Millisecond+doubleClickDuration-1), + clicks: []int{1, 2}, + }, + { + label: "two single clicks", + events: mouseClickEvents( + 100*time.Millisecond, + 100*time.Millisecond+doubleClickDuration+1), + clicks: []int{1, 1}, + }, + } { + t.Run(tc.label, func(t *testing.T) { + var click Click + var ops op.Ops + click.Add(&ops) + + var r router.Router + r.Frame(&ops) + r.Queue(tc.events...) + + events := click.Events(&r) + clicks := filterMouseClicks(events) + if got, want := len(clicks), len(tc.clicks); got != want { + t.Fatalf("got %d mouse clicks, expected %d", got, want) + } + + for i, click := range clicks { + if got, want := click.NumClicks, tc.clicks[i]; got != want { + t.Errorf("got %d combined mouse clicks, expected %d", got, want) + } + } + }) + } +} + +func mouseClickEvents(times ...time.Duration) []event.Event { + press := pointer.Event{ + Type: pointer.Press, + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + } + events := make([]event.Event, 0, 2*len(times)) + for _, t := range times { + release := press + release.Type = pointer.Release + release.Time = t + events = append(events, press, release) + } + return events +} + +func filterMouseClicks(events []ClickEvent) []ClickEvent { + var clicks []ClickEvent + for _, ev := range events { + if ev.Type == TypeClick { + clicks = append(clicks, ev) + } + } + return clicks +} diff --git a/pkg/gel/gio/gesture/log.go b/pkg/gel/gio/gesture/log.go new file mode 100644 index 0000000..9e79319 --- /dev/null +++ b/pkg/gel/gio/gesture/log.go @@ -0,0 +1,9 @@ +package gesture + +// import ( +// "github.com/p9c/log" +// +// "github.com/p9c/gel/version" +// ) +// +// var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/pkg/gel/gio/gpu/api.go b/pkg/gel/gio/gpu/api.go new file mode 100644 index 0000000..f977407 --- /dev/null +++ b/pkg/gel/gio/gpu/api.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + +// An API carries the necessary GPU API specific resources to create a Device. +// There is an API type for each supported GPU API such as OpenGL and Direct3D. +type API = driver.API + +// OpenGL denotes the OpenGL or OpenGL ES API. +type OpenGL = driver.OpenGL + +// Direct3D11 denotes the Direct3D API. +type Direct3D11 = driver.Direct3D11 diff --git a/pkg/gel/gio/gpu/caches.go b/pkg/gel/gio/gpu/caches.go new file mode 100644 index 0000000..8ca696a --- /dev/null +++ b/pkg/gel/gio/gpu/caches.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "fmt" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" +) + +type resourceCache struct { + res map[interface{}]resource + newRes map[interface{}]resource +} + +// opCache is like a resourceCache but using concrete types and a +// freelist instead of two maps to avoid runtime.mapaccess2 calls +// since benchmarking showed them as a bottleneck. +type opCache struct { + // store the index + 1 in cache this key is stored in + index map[ops.Key]int + // list of indexes in cache that are free and can be used + freelist []int + cache []opCacheValue +} + +type opCacheValue struct { + data pathData + // computePath is the encoded path for compute. + computePath encoder + + bounds f32.Rectangle + // the fields below are handled by opCache + key ops.Key + keep bool +} + +func newResourceCache() *resourceCache { + return &resourceCache{ + res: make(map[interface{}]resource), + newRes: make(map[interface{}]resource), + } +} + +func (r *resourceCache) get(key interface{}) (resource, bool) { + v, exists := r.res[key] + if exists { + r.newRes[key] = v + } + return v, exists +} + +func (r *resourceCache) put(key interface{}, val resource) { + if _, exists := r.newRes[key]; exists { + panic(fmt.Errorf("key exists, %p", key)) + } + r.res[key] = val + r.newRes[key] = val +} + +func (r *resourceCache) frame() { + for k, v := range r.res { + if _, exists := r.newRes[k]; !exists { + delete(r.res, k) + v.release() + } + } + for k, v := range r.newRes { + delete(r.newRes, k) + r.res[k] = v + } +} + +func (r *resourceCache) release() { + for _, v := range r.newRes { + v.release() + } + r.newRes = nil + r.res = nil +} + +func newOpCache() *opCache { + return &opCache{ + index: make(map[ops.Key]int), + freelist: make([]int, 0), + cache: make([]opCacheValue, 0), + } +} + +func (r *opCache) get(key ops.Key) (o opCacheValue, exist bool) { + v := r.index[key] + if v == 0 { + return + } + r.cache[v-1].keep = true + return r.cache[v-1], true +} + +func (r *opCache) put(key ops.Key, val opCacheValue) { + v := r.index[key] + val.keep = true + val.key = key + if v == 0 { + // not in cache + i := len(r.cache) + if len(r.freelist) > 0 { + i = r.freelist[len(r.freelist)-1] + r.freelist = r.freelist[:len(r.freelist)-1] + r.cache[i] = val + } else { + r.cache = append(r.cache, val) + } + r.index[key] = i + 1 + } else { + r.cache[v-1] = val + } +} + +func (r *opCache) frame() { + r.freelist = r.freelist[:0] + for i, v := range r.cache { + r.cache[i].keep = false + if v.keep { + continue + } + if v.data.data != nil { + v.data.release() + r.cache[i].data.data = nil + } + delete(r.index, v.key) + r.freelist = append(r.freelist, i) + } +} + +func (r *opCache) release() { + for i := range r.cache { + r.cache[i].keep = false + } + r.frame() + r.index = nil + r.freelist = nil + r.cache = nil +} diff --git a/pkg/gel/gio/gpu/clip.go b/pkg/gel/gio/gpu/clip.go new file mode 100644 index 0000000..8236025 --- /dev/null +++ b/pkg/gel/gio/gpu/clip.go @@ -0,0 +1,97 @@ +package gpu + +import ( + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/stroke" +) + +type quadSplitter struct { + bounds f32.Rectangle + contour uint32 + d *drawOps +} + +func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) { + // NW. + encodeVertex(data, meta, -1, 1, from, ctrl, to) + // NE. + encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to) + // SW. + encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to) + // SE. + encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to) +} + +func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) { + var corner float32 + if cornerx == 1 { + corner += .5 + } + if cornery == 1 { + corner += .25 + } + v := vertex{ + Corner: corner, + FromX: from.X, + FromY: from.Y, + CtrlX: ctrl.X, + CtrlY: ctrl.Y, + ToX: to.X, + ToY: to.Y, + } + v.encode(data, meta) +} + +func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) { + data := qs.d.writeVertCache(vertStride * 4) + encodeQuadTo(data, qs.contour, from, ctrl, to) +} + +func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) { + cbnd := f32.Rectangle{ + Min: quad.From, + Max: quad.To, + }.Canon() + from, ctrl, to := quad.From, quad.Ctrl, quad.To + + // If the curve contain areas where a vertical line + // intersects it twice, split the curve in two x monotone + // lower and upper curves. The stencil fragment program + // expects only one intersection per curve. + + // Find the t where the derivative in x is 0. + v0 := ctrl.Sub(from) + v1 := to.Sub(ctrl) + d := v0.X - v1.X + // t = v0 / d. Split if t is in ]0;1[. + if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { + t := v0.X / d + ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t)) + ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) + mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) + qs.encodeQuadTo(from, ctrl0, mid) + qs.encodeQuadTo(mid, ctrl1, to) + if mid.X > cbnd.Max.X { + cbnd.Max.X = mid.X + } + if mid.X < cbnd.Min.X { + cbnd.Min.X = mid.X + } + } else { + qs.encodeQuadTo(from, ctrl, to) + } + // Find the y extremum, if any. + d = v0.Y - v1.Y + if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { + t := v0.Y / d + y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y + if y > cbnd.Max.Y { + cbnd.Max.Y = y + } + if y < cbnd.Min.Y { + cbnd.Min.Y = y + } + } + + qs.bounds = qs.bounds.Union(cbnd) +} diff --git a/pkg/gel/gio/gpu/compute.go b/pkg/gel/gio/gpu/compute.go new file mode 100644 index 0000000..b3a581e --- /dev/null +++ b/pkg/gel/gio/gpu/compute.go @@ -0,0 +1,1063 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "math/bits" + "time" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/internal/scene" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +type compute struct { + ctx driver.Device + enc encoder + + drawOps drawOps + texOps []textureOp + cache *resourceCache + maxTextureDim int + + programs struct { + elements driver.Program + tileAlloc driver.Program + pathCoarse driver.Program + backdrop driver.Program + binning driver.Program + coarse driver.Program + kernel4 driver.Program + } + buffers struct { + config driver.Buffer + scene sizedBuffer + state sizedBuffer + memory sizedBuffer + } + output struct { + size image.Point + // image is the output texture. Note that it is in RGBA format, + // but contains data in sRGB. See blitOutput for more detail. + image driver.Texture + blitProg driver.Program + } + // images contains ImageOp images packed into a texture atlas. + images struct { + packer packer + // positions maps imageOpData.handles to positions inside tex. + positions map[interface{}]image.Point + tex driver.Texture + } + // materials contains the pre-processed materials (transformed images for + // now, gradients etc. later) packed in a texture atlas. The atlas is used + // as source in kernel4. + materials struct { + // offsets maps texture ops to the offsets to put in their FillImage commands. + offsets map[textureKey]image.Point + + prog driver.Program + layout driver.InputLayout + + packer packer + + tex driver.Texture + fbo driver.Framebuffer + quads []materialVertex + + bufSize int + buffer driver.Buffer + } + timers struct { + profile string + t *timers + elements *timer + tileAlloc *timer + pathCoarse *timer + backdropBinning *timer + coarse *timer + kernel4 *timer + } + + // The following fields hold scratch space to avoid garbage. + zeroSlice []byte + memHeader *memoryHeader + conf *config +} + +// materialVertex describes a vertex of a quad used to render a transformed +// material. +type materialVertex struct { + posX, posY float32 + u, v float32 +} + +// textureKey identifies textureOp. +type textureKey struct { + handle interface{} + transform f32.Affine2D +} + +// textureOp represents an imageOp that requires texture space. +type textureOp struct { + // sceneIdx is the index in the scene that contains the fill image command + // that corresponds to the operation. + sceneIdx int + key textureKey + img imageOpData + + // pos is the position of the untransformed image in the images texture. + pos image.Point +} + +type encoder struct { + scene []scene.Command + npath int + npathseg int + ntrans int +} + +type encodeState struct { + trans f32.Affine2D + clip f32.Rectangle +} + +type sizedBuffer struct { + size int + buffer driver.Buffer +} + +// config matches Config in setup.h +type config struct { + n_elements uint32 // paths + n_pathseg uint32 + width_in_tiles uint32 + height_in_tiles uint32 + tile_alloc memAlloc + bin_alloc memAlloc + ptcl_alloc memAlloc + pathseg_alloc memAlloc + anno_alloc memAlloc + trans_alloc memAlloc +} + +// memAlloc matches Alloc in mem.h +type memAlloc struct { + offset uint32 + //size uint32 +} + +// memoryHeader matches the header of Memory in mem.h. +type memoryHeader struct { + mem_offset uint32 + mem_error uint32 +} + +// GPU structure sizes and constants. +const ( + tileWidthPx = 32 + tileHeightPx = 32 + ptclInitialAlloc = 1024 + kernel4OutputUnit = 2 + kernel4AtlasUnit = 3 + + pathSize = 12 + binSize = 8 + pathsegSize = 52 + annoSize = 32 + transSize = 24 + stateSize = 60 + stateStride = 4 + 2*stateSize +) + +// mem.h constants. +const ( + memNoError = 0 // NO_ERROR + memMallocFailed = 1 // ERR_MALLOC_FAILED +) + +func newCompute(ctx driver.Device) (*compute, error) { + maxDim := ctx.Caps().MaxTextureSize + // Large atlas textures cause artifacts due to precision loss in + // shaders. + if cap := 8192; maxDim > cap { + maxDim = cap + } + g := &compute{ + ctx: ctx, + cache: newResourceCache(), + maxTextureDim: maxDim, + conf: new(config), + memHeader: new(memoryHeader), + } + + blitProg, err := ctx.NewProgram(shader_copy_vert, shader_copy_frag) + if err != nil { + g.Release() + return nil, err + } + g.output.blitProg = blitProg + + materialProg, err := ctx.NewProgram(shader_material_vert, shader_material_frag) + if err != nil { + g.Release() + return nil, err + } + g.materials.prog = materialProg + progLayout, err := ctx.NewInputLayout(shader_material_vert, []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + g.Release() + return nil, err + } + g.materials.layout = progLayout + + g.drawOps.pathCache = newOpCache() + g.drawOps.compute = true + + buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, int(unsafe.Sizeof(config{}))) + if err != nil { + g.Release() + return nil, err + } + g.buffers.config = buf + + shaders := []struct { + prog *driver.Program + src driver.ShaderSources + }{ + {&g.programs.elements, shader_elements_comp}, + {&g.programs.tileAlloc, shader_tile_alloc_comp}, + {&g.programs.pathCoarse, shader_path_coarse_comp}, + {&g.programs.backdrop, shader_backdrop_comp}, + {&g.programs.binning, shader_binning_comp}, + {&g.programs.coarse, shader_coarse_comp}, + {&g.programs.kernel4, shader_kernel4_comp}, + } + for _, shader := range shaders { + p, err := ctx.NewComputeProgram(shader.src) + if err != nil { + g.Release() + return nil, err + } + *shader.prog = p + } + return g, nil +} + +func (g *compute) Collect(viewport image.Point, ops *op.Ops) { + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.ctx, g.cache, ops, viewport) + for _, img := range g.drawOps.allImageOps { + expandPathOp(img.path, img.clip) + } + if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { + t := &g.timers + t.t = newTimers(g.ctx) + t.elements = g.timers.t.newTimer() + t.tileAlloc = g.timers.t.newTimer() + t.pathCoarse = g.timers.t.newTimer() + t.backdropBinning = g.timers.t.newTimer() + t.coarse = g.timers.t.newTimer() + t.kernel4 = g.timers.t.newTimer() + } +} + +func (g *compute) Clear(col color.NRGBA) { + g.drawOps.clear = true + g.drawOps.clearColor = f32color.LinearFromSRGB(col) +} + +func (g *compute) Frame() error { + viewport := g.drawOps.viewport + tileDims := image.Point{ + X: (viewport.X + tileWidthPx - 1) / tileWidthPx, + Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx, + } + + defFBO := g.ctx.BeginFrame() + defer g.ctx.EndFrame() + + if err := g.encode(viewport); err != nil { + return err + } + if err := g.uploadImages(); err != nil { + return err + } + if err := g.renderMaterials(); err != nil { + return err + } + if err := g.render(tileDims); err != nil { + return err + } + g.ctx.BindFramebuffer(defFBO) + g.blitOutput(viewport) + g.cache.frame() + g.drawOps.pathCache.frame() + t := &g.timers + if g.drawOps.profile && t.t.ready() { + et, tat, pct, bbt := t.elements.Elapsed, t.tileAlloc.Elapsed, t.pathCoarse.Elapsed, t.backdropBinning.Elapsed + ct, k4t := t.coarse.Elapsed, t.kernel4.Elapsed + ft := et + tat + pct + bbt + ct + k4t + q := 100 * time.Microsecond + ft = ft.Round(q) + et, tat, pct, bbt = et.Round(q), tat.Round(q), pct.Round(q), bbt.Round(q) + ct, k4t = ct.Round(q), k4t.Round(q) + t.profile = fmt.Sprintf("ft:%7s et:%7s tat:%7s pct:%7s bbt:%7s ct:%7s k4t:%7s", ft, et, tat, pct, bbt, ct, k4t) + } + g.drawOps.clear = false + return nil +} + +func (g *compute) Profile() string { + return g.timers.profile +} + +// blitOutput copies the compute render output to the output FBO. We need to +// copy because compute shaders can only write to textures, not FBOs. Compute +// shader can only write to RGBA textures, but since we actually render in sRGB +// format we can't use glBlitFramebuffer, because it does sRGB conversion. +func (g *compute) blitOutput(viewport image.Point) { + if !g.drawOps.clear { + g.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha) + g.ctx.SetBlend(true) + defer g.ctx.SetBlend(false) + } + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.ctx.BindTexture(0, g.output.image) + g.ctx.BindProgram(g.output.blitProg) + g.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func (g *compute) encode(viewport image.Point) error { + g.texOps = g.texOps[:0] + g.enc.reset() + + // Flip Y-axis. + flipY := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(1, -1)).Offset(f32.Pt(0, float32(viewport.Y))) + g.enc.transform(flipY) + if g.drawOps.clear { + g.enc.rect(f32.Rectangle{Max: layout.FPt(viewport)}) + g.enc.fillColor(f32color.NRGBAToRGBA(g.drawOps.clearColor.SRGB())) + } + return g.encodeOps(flipY, viewport, g.drawOps.allImageOps) +} + +func (g *compute) renderMaterials() error { + m := &g.materials + m.quads = m.quads[:0] + resize := false + reclaimed := false +restart: + for { + for _, op := range g.texOps { + if off, exists := m.offsets[op.key]; exists { + g.enc.setFillImageOffset(op.sceneIdx, off) + continue + } + quad, bounds := g.materialQuad(op.key.transform, op.img, op.pos) + + // A material is clipped to avoid drawing outside its bounds inside the atlas. However, + // imprecision in the clipping may cause a single pixel overflow. Be safe. + size := bounds.Size().Add(image.Pt(1, 1)) + place, fits := m.packer.tryAdd(size) + if !fits { + m.offsets = nil + m.quads = m.quads[:0] + m.packer.clear() + if !reclaimed { + // Some images may no longer be in use, try again + // after clearing existing maps. + reclaimed = true + } else { + m.packer.maxDim += 256 + resize = true + if m.packer.maxDim > g.maxTextureDim { + return errors.New("compute: no space left in material atlas") + } + } + m.packer.newPage() + continue restart + } + // Position quad to match place. + offset := place.Pos.Sub(bounds.Min) + offsetf := layout.FPt(offset) + for i := range quad { + quad[i].posX += offsetf.X + quad[i].posY += offsetf.Y + } + // Draw quad as two triangles. + m.quads = append(m.quads, quad[0], quad[1], quad[3], quad[3], quad[1], quad[2]) + if m.offsets == nil { + m.offsets = make(map[textureKey]image.Point) + } + m.offsets[op.key] = offset + g.enc.setFillImageOffset(op.sceneIdx, offset) + } + break + } + if len(m.quads) == 0 { + return nil + } + texSize := m.packer.maxDim + if resize { + if m.fbo != nil { + m.fbo.Release() + m.fbo = nil + } + if m.tex != nil { + m.tex.Release() + m.tex = nil + } + handle, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, texSize, texSize, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingShaderStorage|driver.BufferBindingFramebuffer) + if err != nil { + return fmt.Errorf("compute: failed to create material atlas: %v", err) + } + m.tex = handle + fbo, err := g.ctx.NewFramebuffer(handle, 0) + if err != nil { + return fmt.Errorf("compute: failed to create material framebuffer: %v", err) + } + m.fbo = fbo + } + // TODO: move to shaders. + // Transform to clip space: [-1, -1] - [1, 1]. + clip := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(2/float32(texSize), 2/float32(texSize))).Offset(f32.Pt(-1, -1)) + for i, v := range m.quads { + p := clip.Transform(f32.Pt(v.posX, v.posY)) + m.quads[i].posX = p.X + m.quads[i].posY = p.Y + } + vertexData := byteslice.Slice(m.quads) + if len(vertexData) > m.bufSize { + if m.buffer != nil { + m.buffer.Release() + m.buffer = nil + } + n := pow2Ceil(len(vertexData)) + buf, err := g.ctx.NewBuffer(driver.BufferBindingVertices, n) + if err != nil { + return err + } + m.bufSize = n + m.buffer = buf + } + m.buffer.Upload(vertexData) + g.ctx.BindTexture(0, g.images.tex) + g.ctx.BindFramebuffer(m.fbo) + g.ctx.Viewport(0, 0, texSize, texSize) + if reclaimed { + g.ctx.Clear(0, 0, 0, 0) + } + g.ctx.BindProgram(m.prog) + g.ctx.BindVertexBuffer(m.buffer, int(unsafe.Sizeof(m.quads[0])), 0) + g.ctx.BindInputLayout(m.layout) + g.ctx.DrawArrays(driver.DrawModeTriangles, 0, len(m.quads)) + return nil +} + +func (g *compute) uploadImages() error { + // padding is the number of pixels added to the right and below + // images, to avoid atlas filtering artifacts. + const padding = 1 + + a := &g.images + var uploads map[interface{}]*image.RGBA + resize := false + reclaimed := false +restart: + for { + for i, op := range g.texOps { + if pos, exists := a.positions[op.img.handle]; exists { + g.texOps[i].pos = pos + continue + } + size := op.img.src.Bounds().Size().Add(image.Pt(padding, padding)) + place, fits := a.packer.tryAdd(size) + if !fits { + a.positions = nil + uploads = nil + a.packer.clear() + if !reclaimed { + // Some images may no longer be in use, try again + // after clearing existing maps. + reclaimed = true + } else { + a.packer.maxDim += 256 + resize = true + if a.packer.maxDim > g.maxTextureDim { + return errors.New("compute: no space left in image atlas") + } + } + a.packer.newPage() + continue restart + } + if a.positions == nil { + a.positions = make(map[interface{}]image.Point) + } + a.positions[op.img.handle] = place.Pos + g.texOps[i].pos = place.Pos + if uploads == nil { + uploads = make(map[interface{}]*image.RGBA) + } + uploads[op.img.handle] = op.img.src + } + break + } + if len(uploads) == 0 { + return nil + } + if resize { + if a.tex != nil { + a.tex.Release() + a.tex = nil + } + sz := a.packer.maxDim + handle, err := g.ctx.NewTexture(driver.TextureFormatSRGB, sz, sz, driver.FilterLinear, driver.FilterLinear, driver.BufferBindingTexture) + if err != nil { + return fmt.Errorf("compute: failed to create image atlas: %v", err) + } + a.tex = handle + } + for h, img := range uploads { + pos, ok := a.positions[h] + if !ok { + panic("compute: internal error: image not placed") + } + size := img.Bounds().Size() + driver.UploadImage(a.tex, pos, img) + rightPadding := image.Pt(padding, size.Y) + a.tex.Upload(image.Pt(pos.X+size.X, pos.Y), rightPadding, g.zeros(rightPadding.X*rightPadding.Y*4)) + bottomPadding := image.Pt(size.X, padding) + a.tex.Upload(image.Pt(pos.X, pos.Y+size.Y), bottomPadding, g.zeros(bottomPadding.X*bottomPadding.Y*4)) + } + return nil +} + +func pow2Ceil(v int) int { + exp := bits.Len(uint(v)) + if bits.OnesCount(uint(v)) == 1 { + exp-- + } + return 1 << exp +} + +// materialQuad constructs a quad that represents the transformed image. It returns the quad +// and its bounds. +func (g *compute) materialQuad(M f32.Affine2D, img imageOpData, uvPos image.Point) ([4]materialVertex, image.Rectangle) { + imgSize := layout.FPt(img.src.Bounds().Size()) + sx, hx, ox, hy, sy, oy := M.Elems() + transOff := f32.Pt(ox, oy) + // The 4 corners of the image rectangle transformed by M, excluding its offset, are: + // + // q0: M * (0, 0) q3: M * (w, 0) + // q1: M * (0, h) q2: M * (w, h) + // + // Note that q0 = M*0 = 0, q2 = q1 + q3. + q0 := f32.Pt(0, 0) + q1 := f32.Pt(hx*imgSize.Y, sy*imgSize.Y) + q3 := f32.Pt(sx*imgSize.X, hy*imgSize.X) + q2 := q1.Add(q3) + q0 = q0.Add(transOff) + q1 = q1.Add(transOff) + q2 = q2.Add(transOff) + q3 = q3.Add(transOff) + + boundsf := f32.Rectangle{ + Min: min(min(q0, q1), min(q2, q3)), + Max: max(max(q0, q1), max(q2, q3)), + } + + bounds := boundRectF(boundsf) + uvPosf := layout.FPt(uvPos) + atlasScale := 1 / float32(g.images.packer.maxDim) + uvBounds := f32.Rectangle{ + Min: uvPosf.Mul(atlasScale), + Max: uvPosf.Add(imgSize).Mul(atlasScale), + } + quad := [4]materialVertex{ + {posX: q0.X, posY: q0.Y, u: uvBounds.Min.X, v: uvBounds.Min.Y}, + {posX: q1.X, posY: q1.Y, u: uvBounds.Min.X, v: uvBounds.Max.Y}, + {posX: q2.X, posY: q2.Y, u: uvBounds.Max.X, v: uvBounds.Max.Y}, + {posX: q3.X, posY: q3.Y, u: uvBounds.Max.X, v: uvBounds.Min.Y}, + } + return quad, bounds +} + +func max(p1, p2 f32.Point) f32.Point { + p := p1 + if p2.X > p.X { + p.X = p2.X + } + if p2.Y > p.Y { + p.Y = p2.Y + } + return p +} + +func min(p1, p2 f32.Point) f32.Point { + p := p1 + if p2.X < p.X { + p.X = p2.X + } + if p2.Y < p.Y { + p.Y = p2.Y + } + return p +} + +func (g *compute) encodeOps(trans f32.Affine2D, viewport image.Point, ops []imageOp) error { + for _, op := range ops { + bounds := layout.FRect(op.clip) + // clip is the union of all drawing affected by the clipping + // operation. TODO: tighten. + clip := f32.Rect(0, 0, float32(viewport.X), float32(viewport.Y)) + nclips := g.encodeClipStack(clip, bounds, op.path, false) + m := op.material + switch m.material { + case materialTexture: + t := trans.Mul(m.trans) + g.texOps = append(g.texOps, textureOp{ + sceneIdx: len(g.enc.scene), + img: m.data, + key: textureKey{ + transform: t, + handle: m.data.handle, + }, + }) + // Add fill command, its offset is resolved and filled in renderMaterials. + g.enc.fillImage(0) + case materialColor: + g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color.SRGB())) + case materialLinearGradient: + // TODO: implement. + g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color1.SRGB())) + default: + panic("not implemented") + } + if op.path != nil && op.path.path { + g.enc.fillMode(scene.FillModeNonzero) + g.enc.transform(op.path.trans.Invert()) + } + // Pop the clip stack. + for i := 0; i < nclips; i++ { + g.enc.endClip(clip) + } + } + return nil +} + +// encodeClips encodes a stack of clip paths and return the stack depth. +func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin bool) int { + nclips := 0 + if p != nil && p.parent != nil { + nclips += g.encodeClipStack(clip, bounds, p.parent, true) + nclips += 1 + } + isStroke := p.stroke.Width > 0 + if p != nil && p.path { + if isStroke { + g.enc.fillMode(scene.FillModeStroke) + g.enc.lineWidth(p.stroke.Width) + } + pathData, _ := g.drawOps.pathCache.get(p.pathKey) + g.enc.transform(p.trans) + g.enc.append(pathData.computePath) + } else { + g.enc.rect(bounds) + } + if begin { + g.enc.beginClip(clip) + if isStroke { + g.enc.fillMode(scene.FillModeNonzero) + } + if p != nil && p.path { + g.enc.transform(p.trans.Invert()) + } + } + return nclips +} + +func encodePath(verts []byte) encoder { + var enc encoder + for len(verts) >= scene.CommandSize+4 { + cmd := ops.DecodeCommand(verts[4:]) + enc.scene = append(enc.scene, cmd) + enc.npathseg++ + verts = verts[scene.CommandSize+4:] + } + return enc +} + +func (g *compute) render(tileDims image.Point) error { + const ( + // wgSize is the largest and most common workgroup size. + wgSize = 128 + // PARTITION_SIZE from elements.comp + partitionSize = 32 * 4 + ) + widthInBins := (tileDims.X + 15) / 16 + heightInBins := (tileDims.Y + 7) / 8 + if widthInBins*heightInBins > wgSize { + return fmt.Errorf("gpu: output too large (%dx%d)", tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx) + } + + // Pad scene with zeroes to avoid reading garbage in elements.comp. + scenePadding := partitionSize - len(g.enc.scene)%partitionSize + g.enc.scene = append(g.enc.scene, make([]scene.Command, scenePadding)...) + + realloced := false + scene := byteslice.Slice(g.enc.scene) + if s := len(scene); s > g.buffers.scene.size { + realloced = true + paddedCap := s * 11 / 10 + if err := g.buffers.scene.ensureCapacity(g.ctx, paddedCap); err != nil { + return err + } + } + g.buffers.scene.buffer.Upload(scene) + + w, h := tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx + if g.output.size.X != w || g.output.size.Y != h { + if err := g.resizeOutput(image.Pt(w, h)); err != nil { + return err + } + } + g.ctx.BindImageTexture(kernel4OutputUnit, g.output.image, driver.AccessWrite, driver.TextureFormatRGBA8) + if t := g.materials.tex; t != nil { + g.ctx.BindImageTexture(kernel4AtlasUnit, t, driver.AccessRead, driver.TextureFormatRGBA8) + } + + // alloc is the number of allocated bytes for static buffers. + var alloc uint32 + round := func(v, quantum int) int { + return (v + quantum - 1) &^ (quantum - 1) + } + malloc := func(size int) memAlloc { + size = round(size, 4) + offset := alloc + alloc += uint32(size) + return memAlloc{offset /*, uint32(size)*/} + } + + *g.conf = config{ + n_elements: uint32(g.enc.npath), + n_pathseg: uint32(g.enc.npathseg), + width_in_tiles: uint32(tileDims.X), + height_in_tiles: uint32(tileDims.Y), + tile_alloc: malloc(g.enc.npath * pathSize), + bin_alloc: malloc(round(g.enc.npath, wgSize) * binSize), + ptcl_alloc: malloc(tileDims.X * tileDims.Y * ptclInitialAlloc), + pathseg_alloc: malloc(g.enc.npathseg * pathsegSize), + anno_alloc: malloc(g.enc.npath * annoSize), + trans_alloc: malloc(g.enc.ntrans * transSize), + } + + numPartitions := (g.enc.numElements() + 127) / 128 + // clearSize is the atomic partition counter plus flag and 2 states per partition. + clearSize := 4 + numPartitions*stateStride + if clearSize > g.buffers.state.size { + realloced = true + paddedCap := clearSize * 11 / 10 + if err := g.buffers.state.ensureCapacity(g.ctx, paddedCap); err != nil { + return err + } + } + + g.buffers.config.Upload(byteslice.Struct(g.conf)) + + minSize := int(unsafe.Sizeof(memoryHeader{})) + int(alloc) + if minSize > g.buffers.memory.size { + realloced = true + // Add space for dynamic GPU allocations. + const sizeBump = 4 * 1024 * 1024 + minSize += sizeBump + if err := g.buffers.memory.ensureCapacity(g.ctx, minSize); err != nil { + return err + } + } + for { + *g.memHeader = memoryHeader{ + mem_offset: alloc, + } + g.buffers.memory.buffer.Upload(byteslice.Struct(g.memHeader)) + g.buffers.state.buffer.Upload(g.zeros(clearSize)) + + if realloced { + realloced = false + g.bindBuffers() + } + t := &g.timers + g.ctx.MemoryBarrier() + t.elements.begin() + g.ctx.BindProgram(g.programs.elements) + g.ctx.DispatchCompute(numPartitions, 1, 1) + g.ctx.MemoryBarrier() + t.elements.end() + t.tileAlloc.begin() + g.ctx.BindProgram(g.programs.tileAlloc) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + g.ctx.MemoryBarrier() + t.tileAlloc.end() + t.pathCoarse.begin() + g.ctx.BindProgram(g.programs.pathCoarse) + g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1) + g.ctx.MemoryBarrier() + t.pathCoarse.end() + t.backdropBinning.begin() + g.ctx.BindProgram(g.programs.backdrop) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + // No barrier needed between backdrop and binning. + g.ctx.BindProgram(g.programs.binning) + g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1) + g.ctx.MemoryBarrier() + t.backdropBinning.end() + t.coarse.begin() + g.ctx.BindProgram(g.programs.coarse) + g.ctx.DispatchCompute(widthInBins, heightInBins, 1) + g.ctx.MemoryBarrier() + t.coarse.end() + t.kernel4.begin() + g.ctx.BindProgram(g.programs.kernel4) + g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1) + g.ctx.MemoryBarrier() + t.kernel4.end() + + if err := g.buffers.memory.buffer.Download(byteslice.Struct(g.memHeader)); err != nil { + if err == driver.ErrContentLost { + continue + } + return err + } + switch errCode := g.memHeader.mem_error; errCode { + case memNoError: + return nil + case memMallocFailed: + // Resize memory and try again. + realloced = true + sz := g.buffers.memory.size * 15 / 10 + if err := g.buffers.memory.ensureCapacity(g.ctx, sz); err != nil { + return err + } + continue + default: + return fmt.Errorf("compute: shader program failed with error %d", errCode) + } + } +} + +// zeros returns a byte slice with size bytes of zeros. +func (g *compute) zeros(size int) []byte { + if cap(g.zeroSlice) < size { + g.zeroSlice = append(g.zeroSlice, make([]byte, size)...) + } + return g.zeroSlice[:size] +} + +func (g *compute) resizeOutput(size image.Point) error { + if g.output.image != nil { + g.output.image.Release() + g.output.image = nil + } + img, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, size.X, size.Y, + driver.FilterNearest, + driver.FilterNearest, + driver.BufferBindingShaderStorage|driver.BufferBindingTexture) + if err != nil { + return err + } + g.output.image = img + g.output.size = size + return nil +} + +func (g *compute) Release() { + if g.drawOps.pathCache != nil { + g.drawOps.pathCache.release() + } + if g.cache != nil { + g.cache.release() + } + progs := []driver.Program{ + g.programs.elements, + g.programs.tileAlloc, + g.programs.pathCoarse, + g.programs.backdrop, + g.programs.binning, + g.programs.coarse, + g.programs.kernel4, + } + if p := g.output.blitProg; p != nil { + p.Release() + } + for _, p := range progs { + if p != nil { + p.Release() + } + } + g.buffers.scene.release() + g.buffers.state.release() + g.buffers.memory.release() + if b := g.buffers.config; b != nil { + b.Release() + } + if g.output.image != nil { + g.output.image.Release() + } + if g.images.tex != nil { + g.images.tex.Release() + } + if g.materials.layout != nil { + g.materials.layout.Release() + } + if g.materials.prog != nil { + g.materials.prog.Release() + } + if g.materials.fbo != nil { + g.materials.fbo.Release() + } + if g.materials.tex != nil { + g.materials.tex.Release() + } + if g.materials.buffer != nil { + g.materials.buffer.Release() + } + if g.timers.t != nil { + g.timers.t.release() + } + + *g = compute{} +} + +func (g *compute) bindBuffers() { + bindStorageBuffers(g.programs.elements, g.buffers.memory.buffer, g.buffers.config, g.buffers.scene.buffer, g.buffers.state.buffer) + bindStorageBuffers(g.programs.tileAlloc, g.buffers.memory.buffer, g.buffers.config) + bindStorageBuffers(g.programs.pathCoarse, g.buffers.memory.buffer, g.buffers.config) + bindStorageBuffers(g.programs.backdrop, g.buffers.memory.buffer, g.buffers.config) + bindStorageBuffers(g.programs.binning, g.buffers.memory.buffer, g.buffers.config) + bindStorageBuffers(g.programs.coarse, g.buffers.memory.buffer, g.buffers.config) + bindStorageBuffers(g.programs.kernel4, g.buffers.memory.buffer, g.buffers.config) +} + +func (b *sizedBuffer) release() { + if b.buffer == nil { + return + } + b.buffer.Release() + *b = sizedBuffer{} +} + +func (b *sizedBuffer) ensureCapacity(ctx driver.Device, size int) error { + if b.size >= size { + return nil + } + if b.buffer != nil { + b.release() + } + buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, size) + if err != nil { + return err + } + b.buffer = buf + b.size = size + return nil +} + +func bindStorageBuffers(prog driver.Program, buffers ...driver.Buffer) { + for i, buf := range buffers { + prog.SetStorageBuffer(i, buf) + } +} + +var bo = binary.LittleEndian + +func (e *encoder) reset() { + e.scene = e.scene[:0] + e.npath = 0 + e.npathseg = 0 + e.ntrans = 0 +} + +func (e *encoder) numElements() int { + return len(e.scene) +} + +func (e *encoder) append(e2 encoder) { + e.scene = append(e.scene, e2.scene...) + e.npath += e2.npath + e.npathseg += e2.npathseg + e.ntrans += e2.ntrans +} + +func (e *encoder) transform(m f32.Affine2D) { + e.scene = append(e.scene, scene.Transform(m)) + e.ntrans++ +} + +func (e *encoder) lineWidth(width float32) { + e.scene = append(e.scene, scene.SetLineWidth(width)) +} + +func (e *encoder) fillMode(mode scene.FillMode) { + e.scene = append(e.scene, scene.SetFillMode(mode)) +} + +func (e *encoder) beginClip(bbox f32.Rectangle) { + e.scene = append(e.scene, scene.BeginClip(bbox)) + e.npath++ +} + +func (e *encoder) endClip(bbox f32.Rectangle) { + e.scene = append(e.scene, scene.EndClip(bbox)) + e.npath++ +} + +func (e *encoder) rect(r f32.Rectangle) { + // Rectangle corners, clock-wise. + c0, c1, c2, c3 := r.Min, f32.Pt(r.Min.X, r.Max.Y), r.Max, f32.Pt(r.Max.X, r.Min.Y) + e.line(c0, c1) + e.line(c1, c2) + e.line(c2, c3) + e.line(c3, c0) +} + +func (e *encoder) fillColor(col color.RGBA) { + e.scene = append(e.scene, scene.FillColor(col)) + e.npath++ +} + +func (e *encoder) setFillImageOffset(index int, offset image.Point) { + x := int16(offset.X) + y := int16(offset.Y) + e.scene[index][2] = uint32(uint16(x)) | uint32(uint16(y))<<16 +} + +func (e *encoder) fillImage(index int) { + e.scene = append(e.scene, scene.FillImage(index)) + e.npath++ +} + +func (e *encoder) line(start, end f32.Point) { + e.scene = append(e.scene, scene.Line(start, end)) + e.npathseg++ +} + +func (e *encoder) quad(start, ctrl, end f32.Point) { + e.scene = append(e.scene, scene.Quad(start, ctrl, end)) + e.npathseg++ +} diff --git a/pkg/gel/gio/gpu/gen.go b/pkg/gel/gio/gpu/gen.go new file mode 100644 index 0000000..238f002 --- /dev/null +++ b/pkg/gel/gio/gpu/gen.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +//go:generate go run ./internal/convertshaders -package gpu diff --git a/pkg/gel/gio/gpu/gpu.go b/pkg/gel/gio/gpu/gpu.go new file mode 100644 index 0000000..305822e --- /dev/null +++ b/pkg/gel/gio/gpu/gpu.go @@ -0,0 +1,1459 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package gpu implements the rendering of Gio drawing operations. It +is used by package app and package app/headless and is otherwise not +useful except for integrating with external window implementations. +*/ +package gpu + +import ( + "encoding/binary" + "errors" + "fmt" + "image" + "image/color" + "math" + "os" + "reflect" + "time" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/internal/scene" + "github.com/p9c/p9/pkg/gel/gio/internal/stroke" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + + // Register backends. + _ "github.com/p9c/p9/pkg/gel/gio/gpu/internal/d3d11" + _ "github.com/p9c/p9/pkg/gel/gio/gpu/internal/opengl" +) + +type GPU interface { + // Release non-Go resources. The GPU is no longer valid after Release. + Release() + // Clear sets the clear color for the next Frame. + Clear(color color.NRGBA) + // Collect the graphics operations from frame, given the viewport. + Collect(viewport image.Point, frame *op.Ops) + // Frame clears the color buffer and draws the collected operations. + Frame() error + // Profile returns the last available profiling information. Profiling + // information is requested when Collect sees a ProfileOp, and the result + // is available through Profile at some later time. + Profile() string +} + +type gpu struct { + cache *resourceCache + + profile string + timers *timers + frameStart time.Time + zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer + drawOps drawOps + ctx driver.Device + renderer *renderer +} + +type renderer struct { + ctx driver.Device + blitter *blitter + pather *pather + packer packer + intersections packer +} + +type drawOps struct { + profile bool + reader ops.Reader + states []drawState + cache *resourceCache + vertCache []byte + viewport image.Point + clear bool + clearColor f32color.RGBA + // allImageOps is the combined list of imageOps and + // zimageOps, in drawing order. + allImageOps []imageOp + imageOps []imageOp + // zimageOps are the rectangle clipped opaque images + // that can use fast front-to-back rendering with z-test + // and no blending. + zimageOps []imageOp + pathOps []*pathOp + pathOpCache []pathOp + qs quadSplitter + pathCache *opCache + // hack for the compute renderer to access + // converted path data. + compute bool +} + +type drawState struct { + clip f32.Rectangle + t f32.Affine2D + cpath *pathOp + rect bool + + matType materialType + // Current paint.ImageOp + image imageOpData + // Current paint.ColorOp, if any. + color color.NRGBA + + // Current paint.LinearGradientOp. + stop1 f32.Point + stop2 f32.Point + color1 color.NRGBA + color2 color.NRGBA +} + +type pathOp struct { + off f32.Point + // clip is the union of all + // later clip rectangles. + clip image.Rectangle + bounds f32.Rectangle + pathKey ops.Key + path bool + pathVerts []byte + parent *pathOp + place placement + + // For compute + trans f32.Affine2D + stroke clip.StrokeStyle +} + +type imageOp struct { + z float32 + path *pathOp + clip image.Rectangle + material material + clipType clipType + place placement +} + +func decodeStrokeOp(data []byte) clip.StrokeStyle { + _ = data[4] + if opconst.OpType(data[0]) != opconst.TypeStroke { + panic("invalid op") + } + bo := binary.LittleEndian + return clip.StrokeStyle{ + Width: math.Float32frombits(bo.Uint32(data[1:])), + } +} + +type quadsOp struct { + key ops.Key + aux []byte +} + +type material struct { + material materialType + opaque bool + // For materialTypeColor. + color f32color.RGBA + // For materialTypeLinearGradient. + color1 f32color.RGBA + color2 f32color.RGBA + // For materialTypeTexture. + data imageOpData + uvTrans f32.Affine2D + + // For the compute backend. + trans f32.Affine2D +} + +// clipOp is the shadow of clip.Op. +type clipOp struct { + // TODO: Use image.Rectangle? + bounds f32.Rectangle + outline bool +} + +// imageOpData is the shadow of paint.ImageOp. +type imageOpData struct { + src *image.RGBA + handle interface{} +} + +type linearGradientOpData struct { + stop1 f32.Point + color1 color.NRGBA + stop2 f32.Point + color2 color.NRGBA +} + +func (op *clipOp) decode(data []byte) { + if opconst.OpType(data[0]) != opconst.TypeClip { + panic("invalid op") + } + bo := binary.LittleEndian + r := image.Rectangle{ + Min: image.Point{ + X: int(int32(bo.Uint32(data[1:]))), + Y: int(int32(bo.Uint32(data[5:]))), + }, + Max: image.Point{ + X: int(int32(bo.Uint32(data[9:]))), + Y: int(int32(bo.Uint32(data[13:]))), + }, + } + *op = clipOp{ + bounds: layout.FRect(r), + outline: data[17] == 1, + } +} + +func decodeImageOp(data []byte, refs []interface{}) imageOpData { + if opconst.OpType(data[0]) != opconst.TypeImage { + panic("invalid op") + } + handle := refs[1] + if handle == nil { + return imageOpData{} + } + return imageOpData{ + src: refs[0].(*image.RGBA), + handle: handle, + } +} + +func decodeColorOp(data []byte) color.NRGBA { + if opconst.OpType(data[0]) != opconst.TypeColor { + panic("invalid op") + } + return color.NRGBA{ + R: data[1], + G: data[2], + B: data[3], + A: data[4], + } +} + +func decodeLinearGradientOp(data []byte) linearGradientOpData { + if opconst.OpType(data[0]) != opconst.TypeLinearGradient { + panic("invalid op") + } + bo := binary.LittleEndian + return linearGradientOpData{ + stop1: f32.Point{ + X: math.Float32frombits(bo.Uint32(data[1:])), + Y: math.Float32frombits(bo.Uint32(data[5:])), + }, + stop2: f32.Point{ + X: math.Float32frombits(bo.Uint32(data[9:])), + Y: math.Float32frombits(bo.Uint32(data[13:])), + }, + color1: color.NRGBA{ + R: data[17+0], + G: data[17+1], + B: data[17+2], + A: data[17+3], + }, + color2: color.NRGBA{ + R: data[21+0], + G: data[21+1], + B: data[21+2], + A: data[21+3], + }, + } +} + +type clipType uint8 + +type resource interface { + release() +} + +type texture struct { + src *image.RGBA + tex driver.Texture +} + +type blitter struct { + ctx driver.Device + viewport image.Point + prog [3]*program + layout driver.InputLayout + colUniforms *blitColUniforms + texUniforms *blitTexUniforms + linearGradientUniforms *blitLinearGradientUniforms + quadVerts driver.Buffer +} + +type blitColUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } + frag struct { + colorUniforms + } +} + +type blitTexUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } +} + +type blitLinearGradientUniforms struct { + vert struct { + blitUniforms + _ [12]byte // Padding to a multiple of 16. + } + frag struct { + gradientUniforms + } +} + +type uniformBuffer struct { + buf driver.Buffer + ptr []byte +} + +type program struct { + prog driver.Program + vertUniforms *uniformBuffer + fragUniforms *uniformBuffer +} + +type blitUniforms struct { + transform [4]float32 + uvTransformR1 [4]float32 + uvTransformR2 [4]float32 + z float32 +} + +type colorUniforms struct { + color f32color.RGBA +} + +type gradientUniforms struct { + color1 f32color.RGBA + color2 f32color.RGBA +} + +type materialType uint8 + +const ( + clipTypeNone clipType = iota + clipTypePath + clipTypeIntersection +) + +const ( + materialColor materialType = iota + materialLinearGradient + materialTexture +) + +func New(api API) (GPU, error) { + d, err := driver.NewDevice(api) + if err != nil { + return nil, err + } + forceCompute := os.Getenv("GIORENDERER") == "forcecompute" + feats := d.Caps().Features + switch { + case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets): + return newGPU(d) + case feats.Has(driver.FeatureCompute): + return newCompute(d) + default: + return nil, errors.New("gpu: no support for float render targets nor compute") + } +} + +func newGPU(ctx driver.Device) (*gpu, error) { + g := &gpu{ + cache: newResourceCache(), + } + g.drawOps.pathCache = newOpCache() + if err := g.init(ctx); err != nil { + return nil, err + } + return g, nil +} + +func (g *gpu) init(ctx driver.Device) error { + g.ctx = ctx + g.renderer = newRenderer(ctx) + return nil +} + +func (g *gpu) Clear(col color.NRGBA) { + g.drawOps.clear = true + g.drawOps.clearColor = f32color.LinearFromSRGB(col) +} + +func (g *gpu) Release() { + g.renderer.release() + g.drawOps.pathCache.release() + g.cache.release() + if g.timers != nil { + g.timers.release() + } + g.ctx.Release() +} + +func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) { + g.renderer.blitter.viewport = viewport + g.renderer.pather.viewport = viewport + g.drawOps.reset(g.cache, viewport) + g.drawOps.collect(g.ctx, g.cache, frameOps, viewport) + g.frameStart = time.Now() + if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { + g.timers = newTimers(g.ctx) + g.zopsTimer = g.timers.newTimer() + g.stencilTimer = g.timers.newTimer() + g.coverTimer = g.timers.newTimer() + g.cleanupTimer = g.timers.newTimer() + } +} + +func (g *gpu) Frame() error { + defFBO := g.ctx.BeginFrame() + defer g.ctx.EndFrame() + viewport := g.renderer.blitter.viewport + for _, img := range g.drawOps.imageOps { + expandPathOp(img.path, img.clip) + } + if g.drawOps.profile { + g.zopsTimer.begin() + } + g.ctx.BindFramebuffer(defFBO) + g.ctx.DepthFunc(driver.DepthFuncGreater) + // Note that Clear must be before ClearDepth if nothing else is rendered + // (len(zimageOps) == 0). If not, the Fairphone 2 will corrupt the depth buffer. + if g.drawOps.clear { + g.drawOps.clear = false + g.ctx.Clear(g.drawOps.clearColor.Float32()) + } + g.ctx.ClearDepth(0.0) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawZOps(g.cache, g.drawOps.zimageOps) + g.zopsTimer.end() + g.stencilTimer.begin() + g.ctx.SetBlend(true) + g.renderer.packStencils(&g.drawOps.pathOps) + g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps) + g.renderer.packIntersections(g.drawOps.imageOps) + g.renderer.intersect(g.drawOps.imageOps) + g.stencilTimer.end() + g.coverTimer.begin() + g.ctx.BindFramebuffer(defFBO) + g.ctx.Viewport(0, 0, viewport.X, viewport.Y) + g.renderer.drawOps(g.cache, g.drawOps.imageOps) + g.ctx.SetBlend(false) + g.renderer.pather.stenciler.invalidateFBO() + g.coverTimer.end() + g.ctx.BindFramebuffer(defFBO) + g.cleanupTimer.begin() + g.cache.frame() + g.drawOps.pathCache.frame() + g.cleanupTimer.end() + if g.drawOps.profile && g.timers.ready() { + zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed + ft := zt + st + covt + cleant + q := 100 * time.Microsecond + zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q) + frameDur := time.Since(g.frameStart).Round(q) + ft = ft.Round(q) + g.profile = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s", frameDur, ft, zt, st, covt) + } + return nil +} + +func (g *gpu) Profile() string { + return g.profile +} + +func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture { + var tex *texture + t, exists := cache.get(data.handle) + if !exists { + t = &texture{ + src: data.src, + } + cache.put(data.handle, t) + } + tex = t.(*texture) + if tex.tex != nil { + return tex.tex + } + handle, err := r.ctx.NewTexture(driver.TextureFormatSRGB, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear, driver.FilterLinear, driver.BufferBindingTexture) + if err != nil { + panic(err) + } + driver.UploadImage(handle, image.Pt(0, 0), data.src) + tex.tex = handle + return tex.tex +} + +func (t *texture) release() { + if t.tex != nil { + t.tex.Release() + } +} + +func newRenderer(ctx driver.Device) *renderer { + r := &renderer{ + ctx: ctx, + blitter: newBlitter(ctx), + pather: newPather(ctx), + } + + maxDim := ctx.Caps().MaxTextureSize + // Large atlas textures cause artifacts due to precision loss in + // shaders. + if cap := 8192; maxDim > cap { + maxDim = cap + } + + r.packer.maxDim = maxDim + r.intersections.maxDim = maxDim + return r +} + +func (r *renderer) release() { + r.pather.release() + r.blitter.release() +} + +func newBlitter(ctx driver.Device) *blitter { + quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, + byteslice.Slice([]float32{ + -1, +1, 0, 0, + +1, +1, 1, 0, + -1, -1, 0, 1, + +1, -1, 1, 1, + }), + ) + if err != nil { + panic(err) + } + b := &blitter{ + ctx: ctx, + quadVerts: quadVerts, + } + b.colUniforms = new(blitColUniforms) + b.texUniforms = new(blitTexUniforms) + b.linearGradientUniforms = new(blitLinearGradientUniforms) + prog, layout, err := createColorPrograms(ctx, shader_blit_vert, shader_blit_frag, + [3]interface{}{&b.colUniforms.vert, &b.linearGradientUniforms.vert, &b.texUniforms.vert}, + [3]interface{}{&b.colUniforms.frag, &b.linearGradientUniforms.frag, nil}, + ) + if err != nil { + panic(err) + } + b.prog = prog + b.layout = layout + return b +} + +func (b *blitter) release() { + b.quadVerts.Release() + for _, p := range b.prog { + p.Release() + } + b.layout.Release() +} + +func createColorPrograms(b driver.Device, vsSrc driver.ShaderSources, fsSrc [3]driver.ShaderSources, vertUniforms, fragUniforms [3]interface{}) ([3]*program, driver.InputLayout, error) { + var progs [3]*program + { + prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture]) + if err != nil { + return progs, nil, err + } + var vertBuffer, fragBuffer *uniformBuffer + if u := vertUniforms[materialTexture]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialTexture]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer) + } + { + var vertBuffer, fragBuffer *uniformBuffer + prog, err := b.NewProgram(vsSrc, fsSrc[materialColor]) + if err != nil { + progs[materialTexture].Release() + return progs, nil, err + } + if u := vertUniforms[materialColor]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialColor]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialColor] = newProgram(prog, vertBuffer, fragBuffer) + } + { + var vertBuffer, fragBuffer *uniformBuffer + prog, err := b.NewProgram(vsSrc, fsSrc[materialLinearGradient]) + if err != nil { + progs[materialTexture].Release() + progs[materialColor].Release() + return progs, nil, err + } + if u := vertUniforms[materialLinearGradient]; u != nil { + vertBuffer = newUniformBuffer(b, u) + prog.SetVertexUniforms(vertBuffer.buf) + } + if u := fragUniforms[materialLinearGradient]; u != nil { + fragBuffer = newUniformBuffer(b, u) + prog.SetFragmentUniforms(fragBuffer.buf) + } + progs[materialLinearGradient] = newProgram(prog, vertBuffer, fragBuffer) + } + layout, err := b.NewInputLayout(vsSrc, []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + progs[materialTexture].Release() + progs[materialColor].Release() + progs[materialLinearGradient].Release() + return progs, nil, err + } + return progs, layout, nil +} + +func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) { + if len(r.packer.sizes) == 0 { + return + } + fbo := -1 + r.pather.begin(r.packer.sizes) + for _, p := range ops { + if fbo != p.place.Idx { + fbo = p.place.Idx + f := r.pather.stenciler.cover(fbo) + r.ctx.BindFramebuffer(f.fbo) + r.ctx.Clear(0.0, 0.0, 0.0, 0.0) + } + v, _ := pathCache.get(p.pathKey) + r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data) + } +} + +func (r *renderer) intersect(ops []imageOp) { + if len(r.intersections.sizes) == 0 { + return + } + fbo := -1 + r.pather.stenciler.beginIntersect(r.intersections.sizes) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.pather.stenciler.iprog.layout) + for _, img := range ops { + if img.clipType != clipTypeIntersection { + continue + } + if fbo != img.place.Idx { + fbo = img.place.Idx + f := r.pather.stenciler.intersections.fbos[fbo] + r.ctx.BindFramebuffer(f.fbo) + r.ctx.Clear(1.0, 0.0, 0.0, 0.0) + } + r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy()) + r.intersectPath(img.path, img.clip) + } +} + +func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) { + if p.parent != nil { + r.intersectPath(p.parent, clip) + } + if !p.path { + return + } + uv := image.Rectangle{ + Min: p.place.Pos, + Max: p.place.Pos.Add(p.clip.Size()), + } + o := clip.Min.Sub(p.clip.Min) + sub := image.Rectangle{ + Min: o, + Max: o.Add(clip.Size()), + } + fbo := r.pather.stenciler.cover(p.place.Idx) + r.ctx.BindTexture(0, fbo.tex) + coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) + subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size()) + r.pather.stenciler.iprog.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} + r.pather.stenciler.iprog.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y} + r.pather.stenciler.iprog.prog.UploadUniforms() + r.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func (r *renderer) packIntersections(ops []imageOp) { + r.intersections.clear() + for i, img := range ops { + var npaths int + var onePath *pathOp + for p := img.path; p != nil; p = p.parent { + if p.path { + onePath = p + npaths++ + } + } + switch npaths { + case 0: + case 1: + place := onePath.place + place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min) + ops[i].place = place + ops[i].clipType = clipTypePath + default: + sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()} + place, ok := r.intersections.add(sz) + if !ok { + panic("internal error: if the intersection fit, the intersection should fit as well") + } + ops[i].clipType = clipTypeIntersection + ops[i].place = place + } + } +} + +func (r *renderer) packStencils(pops *[]*pathOp) { + r.packer.clear() + ops := *pops + // Allocate atlas space for cover textures. + var i int + for i < len(ops) { + p := ops[i] + if p.clip.Empty() { + ops[i] = ops[len(ops)-1] + ops = ops[:len(ops)-1] + continue + } + sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()} + place, ok := r.packer.add(sz) + if !ok { + // The clip area is at most the entire screen. Hopefully no + // screen is larger than GL_MAX_TEXTURE_SIZE. + panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d", p.clip, r.packer.maxDim, r.packer.maxDim)) + } + p.place = place + i++ + } + *pops = ops +} + +// intersects intersects clip and b where b is offset by off. +// ceilRect returns a bounding image.Rectangle for a f32.Rectangle. +func boundRectF(r f32.Rectangle) image.Rectangle { + return image.Rectangle{ + Min: image.Point{ + X: int(floor(r.Min.X)), + Y: int(floor(r.Min.Y)), + }, + Max: image.Point{ + X: int(ceil(r.Max.X)), + Y: int(ceil(r.Max.Y)), + }, + } +} + +func ceil(v float32) int { + return int(math.Ceil(float64(v))) +} + +func floor(v float32) int { + return int(math.Floor(float64(v))) +} + +func (d *drawOps) reset(cache *resourceCache, viewport image.Point) { + d.profile = false + d.cache = cache + d.viewport = viewport + d.imageOps = d.imageOps[:0] + d.allImageOps = d.allImageOps[:0] + d.zimageOps = d.zimageOps[:0] + d.pathOps = d.pathOps[:0] + d.pathOpCache = d.pathOpCache[:0] + d.vertCache = d.vertCache[:0] +} + +func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops, viewport image.Point) { + clip := f32.Rectangle{ + Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)}, + } + d.reader.Reset(root) + state := drawState{ + clip: clip, + rect: true, + color: color.NRGBA{A: 0xff}, + } + d.collectOps(&d.reader, state) + for _, p := range d.pathOps { + if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil { + data := buildPath(ctx, p.pathVerts) + var computePath encoder + if d.compute { + computePath = encodePath(p.pathVerts) + } + d.pathCache.put(p.pathKey, opCacheValue{ + data: data, + bounds: p.bounds, + computePath: computePath, + }) + } + p.pathVerts = nil + } +} + +func (d *drawOps) newPathOp() *pathOp { + d.pathOpCache = append(d.pathOpCache, pathOp{}) + return &d.pathOpCache[len(d.pathOpCache)-1] +} + +func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle) { + npath := d.newPathOp() + *npath = pathOp{ + parent: state.cpath, + bounds: bounds, + off: off, + trans: tr, + stroke: stroke, + } + state.cpath = npath + if len(aux) > 0 { + state.rect = false + state.cpath.pathKey = auxKey + state.cpath.path = true + state.cpath.pathVerts = aux + d.pathOps = append(d.pathOps, state.cpath) + } +} + +// split a transform into two parts, one which is pur offset and the +// other representing the scaling, shearing and rotation part +func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) { + sx, hx, ox, hy, sy, oy := t.Elems() + offset = f32.Point{X: ox, Y: oy} + srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0) + return +} + +func (d *drawOps) save(id int, state drawState) { + if extra := id - len(d.states) + 1; extra > 0 { + d.states = append(d.states, make([]drawState, extra)...) + } + d.states[id] = state +} + +func (d *drawOps) collectOps(r *ops.Reader, state drawState) { + var ( + quads quadsOp + str clip.StrokeStyle + z int + ) + d.save(opconst.InitialStateID, state) +loop: + for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeProfile: + d.profile = true + case opconst.TypeTransform: + dop := ops.DecodeTransform(encOp.Data) + state.t = state.t.Mul(dop) + + case opconst.TypeStroke: + str = decodeStrokeOp(encOp.Data) + + case opconst.TypePath: + encOp, ok = r.Decode() + if !ok { + break loop + } + quads.aux = encOp.Data[opconst.TypeAuxLen:] + quads.key = encOp.Key + + case opconst.TypeClip: + var op clipOp + op.decode(encOp.Data) + bounds := op.bounds + trans, off := splitTransform(state.t) + if len(quads.aux) > 0 { + // There is a clipping path, build the gpu data and update the + // cache key such that it will be equal only if the transform is the + // same also. Use cached data if we have it. + quads.key = quads.key.SetTransform(trans) + if v, ok := d.pathCache.get(quads.key); ok { + // Since the GPU data exists in the cache aux will not be used. + // Why is this not used for the offset shapes? + op.bounds = v.bounds + } else { + pathData, bounds := d.buildVerts( + quads.aux, trans, op.outline, str, + ) + op.bounds = bounds + if !d.compute { + quads.aux = pathData + } + // add it to the cache, without GPU data, so the transform can be + // reused. + d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds}) + } + } else { + quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds, trans) + quads.key = encOp.Key + quads.key.SetTransform(trans) + } + state.clip = state.clip.Intersect(op.bounds.Add(off)) + d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, str) + quads = quadsOp{} + str = clip.StrokeStyle{} + + case opconst.TypeColor: + state.matType = materialColor + state.color = decodeColorOp(encOp.Data) + case opconst.TypeLinearGradient: + state.matType = materialLinearGradient + op := decodeLinearGradientOp(encOp.Data) + state.stop1 = op.stop1 + state.stop2 = op.stop2 + state.color1 = op.color1 + state.color2 = op.color2 + case opconst.TypeImage: + state.matType = materialTexture + state.image = decodeImageOp(encOp.Data, encOp.Refs) + case opconst.TypePaint: + // Transform (if needed) the painting rectangle and if so generate a clip path, + // for those cases also compute a partialTrans that maps texture coordinates between + // the new bounding rectangle and the transformed original paint rectangle. + trans, off := splitTransform(state.t) + // Fill the clip area, unless the material is a (bounded) image. + // TODO: Find a tighter bound. + inf := float32(1e6) + dst := f32.Rect(-inf, -inf, inf, inf) + if state.matType == materialTexture { + dst = layout.FRect(state.image.src.Rect) + } + clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, trans) + cl := state.clip.Intersect(bnd.Add(off)) + if cl.Empty() { + continue + } + + wasrect := state.rect + if clipData != nil { + // The paint operation is sheared or rotated, add a clip path representing + // this transformed rectangle. + encOp.Key.SetTransform(trans) + d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}) + } + + bounds := boundRectF(cl) + mat := state.materialFor(bnd, off, partialTrans, bounds, state.t) + + if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && (mat.material == materialColor) { + // The image is a uniform opaque color and takes up the whole screen. + // Scrap images up to and including this image and set clear color. + d.allImageOps = d.allImageOps[:0] + d.zimageOps = d.zimageOps[:0] + d.imageOps = d.imageOps[:0] + z = 0 + d.clearColor = mat.color.Opaque() + d.clear = true + continue + } + z++ + if z != int(uint16(z)) { + // TODO(eliasnaur) github.com/p9c/p9/pkg/gel/gio/issue/127. + panic("more than 65k paint objects not supported") + } + // Assume 16-bit depth buffer. + const zdepth = 1 << 16 + // Convert z to window-space, assuming depth range [0;1]. + zf := float32(z)*2/zdepth - 1.0 + img := imageOp{ + z: zf, + path: state.cpath, + clip: bounds, + material: mat, + } + + d.allImageOps = append(d.allImageOps, img) + if state.rect && img.material.opaque { + d.zimageOps = append(d.zimageOps, img) + } else { + d.imageOps = append(d.imageOps, img) + } + if clipData != nil { + // we added a clip path that should not remain + state.cpath = state.cpath.parent + state.rect = wasrect + } + case opconst.TypeSave: + id := ops.DecodeSave(encOp.Data) + d.save(id, state) + case opconst.TypeLoad: + id, mask := ops.DecodeLoad(encOp.Data) + s := d.states[id] + if mask&opconst.TransformState != 0 { + state.t = s.t + } + if mask&^opconst.TransformState != 0 { + state = s + } + } + } +} + +func expandPathOp(p *pathOp, clip image.Rectangle) { + for p != nil { + pclip := p.clip + if !pclip.Empty() { + clip = clip.Union(pclip) + } + p.clip = clip + p = p.parent + } +} + +func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle, trans f32.Affine2D) material { + var m material + switch d.matType { + case materialColor: + m.material = materialColor + m.color = f32color.LinearFromSRGB(d.color) + m.opaque = m.color.A == 1.0 + case materialLinearGradient: + m.material = materialLinearGradient + + m.color1 = f32color.LinearFromSRGB(d.color1) + m.color2 = f32color.LinearFromSRGB(d.color2) + m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0 + + m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2)) + case materialTexture: + m.material = materialTexture + dr := boundRectF(rect.Add(off)) + sz := d.image.src.Bounds().Size() + sr := f32.Rectangle{ + Max: f32.Point{ + X: float32(sz.X), + Y: float32(sz.Y), + }, + } + dx := float32(dr.Dx()) + sdx := sr.Dx() + sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx + sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx + dy := float32(dr.Dy()) + sdy := sr.Dy() + sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy + sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy + uvScale, uvOffset := texSpaceTransform(sr, sz) + m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)) + m.trans = trans + m.data = d.image + } + return m +} + +func (r *renderer) drawZOps(cache *resourceCache, ops []imageOp) { + r.ctx.SetDepthTest(true) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.blitter.layout) + // Render front to back. + for i := len(ops) - 1; i >= 0; i-- { + img := ops[i] + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + } + drc := img.clip + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans) + } + r.ctx.SetDepthTest(false) +} + +func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) { + r.ctx.SetDepthTest(true) + r.ctx.DepthMask(false) + r.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha) + r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0) + r.ctx.BindInputLayout(r.pather.coverer.layout) + var coverTex driver.Texture + for _, img := range ops { + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(0, r.texHandle(cache, m.data)) + } + drc := img.clip + + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + var fbo stencilFBO + switch img.clipType { + case clipTypeNone: + r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans) + continue + case clipTypePath: + fbo = r.pather.stenciler.cover(img.place.Idx) + case clipTypeIntersection: + fbo = r.pather.stenciler.intersections.fbos[img.place.Idx] + } + if coverTex != fbo.tex { + coverTex = fbo.tex + r.ctx.BindTexture(1, coverTex) + } + uv := image.Rectangle{ + Min: img.place.Pos, + Max: img.place.Pos.Add(drc.Size()), + } + coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) + r.pather.cover(img.z, m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff) + } + r.ctx.DepthMask(true) + r.ctx.SetDepthTest(false) +} + +func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) { + p := b.prog[mat] + b.ctx.BindProgram(p.prog) + var uniforms *blitUniforms + switch mat { + case materialColor: + b.colUniforms.frag.color = col + uniforms = &b.colUniforms.vert.blitUniforms + case materialTexture: + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} + b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &b.texUniforms.vert.blitUniforms + case materialLinearGradient: + b.linearGradientUniforms.frag.color1 = col1 + b.linearGradientUniforms.frag.color2 = col2 + + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + b.linearGradientUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} + b.linearGradientUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &b.linearGradientUniforms.vert.blitUniforms + } + uniforms.z = z + uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} + p.UploadUniforms() + b.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +// newUniformBuffer creates a new GPU uniform buffer backed by the +// structure uniformBlock points to. +func newUniformBuffer(b driver.Device, uniformBlock interface{}) *uniformBuffer { + ref := reflect.ValueOf(uniformBlock) + // Determine the size of the uniforms structure, *uniforms. + size := ref.Elem().Type().Size() + // Map the uniforms structure as a byte slice. + ptr := (*[1 << 30]byte)(unsafe.Pointer(ref.Pointer()))[:size:size] + ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr)) + if err != nil { + panic(err) + } + return &uniformBuffer{buf: ubuf, ptr: ptr} +} + +func (u *uniformBuffer) Upload() { + u.buf.Upload(u.ptr) +} + +func (u *uniformBuffer) Release() { + u.buf.Release() + u.buf = nil +} + +func newProgram(prog driver.Program, vertUniforms, fragUniforms *uniformBuffer) *program { + if vertUniforms != nil { + prog.SetVertexUniforms(vertUniforms.buf) + } + if fragUniforms != nil { + prog.SetFragmentUniforms(fragUniforms.buf) + } + return &program{prog: prog, vertUniforms: vertUniforms, fragUniforms: fragUniforms} +} + +func (p *program) UploadUniforms() { + if p.vertUniforms != nil { + p.vertUniforms.Upload() + } + if p.fragUniforms != nil { + p.fragUniforms.Upload() + } +} + +func (p *program) Release() { + p.prog.Release() + p.prog = nil + if p.vertUniforms != nil { + p.vertUniforms.Release() + p.vertUniforms = nil + } + if p.fragUniforms != nil { + p.fragUniforms.Release() + p.fragUniforms = nil + } +} + +// texSpaceTransform return the scale and offset that transforms the given subimage +// into quad texture coordinates. +func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, f32.Point) { + size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)} + scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y} + offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y} + return scale, offset +} + +// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)]. +func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f32.Point) f32.Affine2D { + d := stop2.Sub(stop1) + l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y))) + a := float32(math.Atan2(float64(-d.Y), float64(d.X))) + + // TODO: optimize + zp := f32.Point{} + return f32.Affine2D{}. + Scale(zp, layout.FPt(clip.Size())). // scale to pixel space + Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space + Offset(zp.Sub(stop1)). // offset to first stop point + Rotate(zp, a). // rotate to align gradient + Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size +} + +// clipSpaceTransform returns the scale and offset that transforms the given +// rectangle from a viewport into OpenGL clip space. +func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32.Point) { + // First, transform UI coordinates to OpenGL coordinates: + // + // [(-1, +1) (+1, +1)] + // [(-1, -1) (+1, -1)] + // + x, y := float32(r.Min.X), float32(r.Min.Y) + w, h := float32(r.Dx()), float32(r.Dy()) + vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y) + x = x*vx - 1 + y = 1 - y*vy + w *= vx + h *= vy + + // Then, compute the transformation from the fullscreen quad to + // the rectangle at (x, y) and dimensions (w, h). + scale := f32.Point{X: w * .5, Y: h * .5} + offset := f32.Point{X: x + w*.5, Y: y - h*.5} + + return scale, offset +} + +// Fill in maximal Y coordinates of the NW and NE corners. +func fillMaxY(verts []byte) { + contour := 0 + bo := binary.LittleEndian + for len(verts) > 0 { + maxy := float32(math.Inf(-1)) + i := 0 + for ; i+vertStride*4 <= len(verts); i += vertStride * 4 { + vert := verts[i : i+vertStride] + // MaxY contains the integer contour index. + pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):])) + if contour != pathContour { + contour = pathContour + break + } + fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):])) + ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):])) + toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):])) + if fromy > maxy { + maxy = fromy + } + if ctrly > maxy { + maxy = ctrly + } + if toy > maxy { + maxy = toy + } + } + fillContourMaxY(maxy, verts[:i]) + verts = verts[i:] + } +} + +func fillContourMaxY(maxy float32, verts []byte) { + bo := binary.LittleEndian + for i := 0; i < len(verts); i += vertStride { + off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY)) + bo.PutUint32(verts[i+off:], math.Float32bits(maxy)) + } +} + +func (d *drawOps) writeVertCache(n int) []byte { + d.vertCache = append(d.vertCache, make([]byte, n)...) + return d.vertCache[len(d.vertCache)-n:] +} + +// transform, split paths as needed, calculate maxY, bounds and create GPU vertices. +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { + inf := float32(math.Inf(+1)) + d.qs.bounds = f32.Rectangle{ + Min: f32.Point{X: inf, Y: inf}, + Max: f32.Point{X: -inf, Y: -inf}, + } + d.qs.d = d + startLength := len(d.vertCache) + + switch { + case str.Width > 0: + // Stroke path. + ss := stroke.StrokeStyle{ + Width: str.Width, + Miter: str.Miter, + Cap: stroke.StrokeCap(str.Cap), + Join: stroke.StrokeJoin(str.Join), + } + quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) + for _, quad := range quads { + d.qs.contour = quad.Contour + quad.Quad = quad.Quad.Transform(tr) + + d.qs.splitAndEncode(quad.Quad) + } + + case outline: + decodeToOutlineQuads(&d.qs, tr, pathData) + } + + fillMaxY(d.vertCache[startLength:]) + return d.vertCache[startLength:], d.qs.bounds +} + +// decodeOutlineQuads decodes scene commands, splits them into quadratic béziers +// as needed and feeds them to the supplied splitter. +func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { + for len(pathData) >= scene.CommandSize+4 { + qs.contour = bo.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q stroke.QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + q = q.Transform(tr) + qs.splitAndEncode(q) + case scene.OpQuad: + var q stroke.QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + q = q.Transform(tr) + qs.splitAndEncode(q) + case scene.OpCubic: + for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) { + q = q.Transform(tr) + qs.splitAndEncode(q) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } +} + +// create GPU vertices for transformed r, find the bounds and establish texture transform. +func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) { + if isPureOffset(tr) { + // fast-path to allow blitting of pure rectangles + _, _, ox, _, _, oy := tr.Elems() + off := f32.Pt(ox, oy) + bnd.Min = r.Min.Add(off) + bnd.Max = r.Max.Add(off) + return + } + + // transform all corners, find new bounds + corners := [4]f32.Point{ + tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)), + tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)), + } + bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32) + bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32) + for _, c := range corners { + if c.X < bnd.Min.X { + bnd.Min.X = c.X + } + if c.Y < bnd.Min.Y { + bnd.Min.Y = c.Y + } + if c.X > bnd.Max.X { + bnd.Max.X = c.X + } + if c.Y > bnd.Max.Y { + bnd.Max.Y = c.Y + } + } + + // build the GPU vertices + l := len(d.vertCache) + if !d.compute { + d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...) + aux = d.vertCache[l:] + encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), corners[1]) + encodeQuadTo(aux[vertStride*4:], 0, corners[1], corners[1].Add(corners[2]).Mul(0.5), corners[2]) + encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], corners[2].Add(corners[3]).Mul(0.5), corners[3]) + encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], corners[3].Add(corners[0]).Mul(0.5), corners[0]) + fillMaxY(aux) + } else { + d.vertCache = append(d.vertCache, make([]byte, (scene.CommandSize+4)*4)...) + aux = d.vertCache[l:] + buf := aux + bo := binary.LittleEndian + bo.PutUint32(buf, 0) // Contour + ops.EncodeCommand(buf[4:], scene.Line(r.Min, f32.Pt(r.Max.X, r.Min.Y))) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Max.X, r.Min.Y), r.Max)) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(r.Max, f32.Pt(r.Min.X, r.Max.Y))) + buf = buf[4+scene.CommandSize:] + bo.PutUint32(buf, 0) + ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Min.X, r.Max.Y), r.Min)) + } + + // establish the transform mapping from bounds rectangle to transformed corners + var P1, P2, P3 f32.Point + P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X) + P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y) + sx, sy := P2.X-P3.X, P2.Y-P3.Y + ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert() + + return +} + +func isPureOffset(t f32.Affine2D) bool { + a, b, _, d, e, _ := t.Elems() + return a == 1 && b == 0 && d == 0 && e == 1 +} diff --git a/pkg/gel/gio/gpu/headless/driver_test.go b/pkg/gel/gio/gpu/headless/driver_test.go new file mode 100644 index 0000000..b6cac4a --- /dev/null +++ b/pkg/gel/gio/gpu/headless/driver_test.go @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "bytes" + "flag" + "image" + "image/color" + "image/png" + "io/ioutil" + "runtime" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" +) + +var dumpImages = flag.Bool("saveimages", false, "save test images") + +var clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe} +var clearColExpect = f32color.NRGBAToRGBA(clearCol) + +func TestFramebufferClear(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } +} + +func TestSimpleShader(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + p, err := b.NewProgram(shader_simple_vert, shader_simple_frag) + if err != nil { + t.Fatal(err) + } + defer p.Release() + b.BindProgram(p) + b.DrawArrays(driver.DrawModeTriangles, 0, 3) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } + // Just off the center to catch inverted triangles. + cx, cy := 300, 400 + shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} + if got, exp := img.RGBAAt(cx, cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) + } +} + +func TestInputShader(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo := setupFBO(t, b, sz) + p, err := b.NewProgram(shader_input_vert, shader_simple_frag) + if err != nil { + t.Fatal(err) + } + defer p.Release() + b.BindProgram(p) + buf, err := b.NewImmutableBuffer(driver.BufferBindingVertices, + byteslice.Slice([]float32{ + 0, .5, .5, 1, + -.5, -.5, .5, 1, + .5, -.5, .5, 1, + }), + ) + if err != nil { + t.Fatal(err) + } + defer buf.Release() + b.BindVertexBuffer(buf, 4*4, 0) + layout, err := b.NewInputLayout(shader_input_vert, []driver.InputDesc{ + { + Type: driver.DataTypeFloat, + Size: 4, + Offset: 0, + }, + }) + if err != nil { + t.Fatal(err) + } + defer layout.Release() + b.BindInputLayout(layout) + b.DrawArrays(driver.DrawModeTriangles, 0, 3) + img := screenshot(t, b, fbo, sz) + if got := img.RGBAAt(0, 0); got != clearColExpect { + t.Errorf("got color %v, expected %v", got, clearColExpect) + } + cx, cy := 300, 400 + shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} + if got, exp := img.RGBAAt(cx, cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) + } +} + +func TestFramebuffers(t *testing.T) { + b := newDriver(t) + sz := image.Point{X: 800, Y: 600} + fbo1 := newFBO(t, b, sz) + fbo2 := newFBO(t, b, sz) + var ( + col1 = color.NRGBA{R: 0xac, G: 0xbd, B: 0xef, A: 0xde} + col2 = color.NRGBA{R: 0xfe, G: 0xba, B: 0xbe, A: 0xca} + ) + fcol1, fcol2 := f32color.LinearFromSRGB(col1), f32color.LinearFromSRGB(col2) + b.BindFramebuffer(fbo1) + b.Clear(fcol1.Float32()) + b.BindFramebuffer(fbo2) + b.Clear(fcol2.Float32()) + img := screenshot(t, b, fbo1, sz) + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1)) + } + img = screenshot(t, b, fbo2, sz) + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2)) + } +} + +func setupFBO(t *testing.T, b driver.Device, size image.Point) driver.Framebuffer { + fbo := newFBO(t, b, size) + b.BindFramebuffer(fbo) + // ClearColor accepts linear RGBA colors, while 8-bit colors + // are in the sRGB color space. + col := f32color.LinearFromSRGB(clearCol) + b.Clear(col.Float32()) + b.ClearDepth(0.0) + b.Viewport(0, 0, size.X, size.Y) + return fbo +} + +func newFBO(t *testing.T, b driver.Device, size image.Point) driver.Framebuffer { + fboTex, err := b.NewTexture( + driver.TextureFormatSRGB, + size.X, size.Y, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingFramebuffer, + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fboTex.Release() + }) + const depthBits = 16 + fbo, err := b.NewFramebuffer(fboTex, depthBits) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + fbo.Release() + }) + return fbo +} + +func newDriver(t *testing.T) driver.Device { + ctx, err := newContext() + if err != nil { + t.Skipf("no context available: %v", err) + } + runtime.LockOSThread() + if err := ctx.MakeCurrent(); err != nil { + t.Fatal(err) + } + b, err := driver.NewDevice(ctx.API()) + if err != nil { + t.Fatal(err) + } + b.BeginFrame() + t.Cleanup(func() { + b.EndFrame() + ctx.ReleaseCurrent() + runtime.UnlockOSThread() + ctx.Release() + }) + return b +} + +func screenshot(t *testing.T, d driver.Device, fbo driver.Framebuffer, size image.Point) *image.RGBA { + img, err := driver.DownloadImage(d, fbo, image.Rectangle{Max: size}) + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } + return img +} + +func saveImage(file string, img image.Image) error { + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +} diff --git a/pkg/gel/gio/gpu/headless/gen.go b/pkg/gel/gio/gpu/headless/gen.go new file mode 100644 index 0000000..b9e1fed --- /dev/null +++ b/pkg/gel/gio/gpu/headless/gen.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +//go:generate go run ../internal/convertshaders -package headless diff --git a/pkg/gel/gio/gpu/headless/headless.go b/pkg/gel/gio/gpu/headless/headless.go new file mode 100644 index 0000000..511159d --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package headless implements headless windows for rendering +// an operation list to an image. +package headless + +import ( + "image" + "image/color" + "runtime" + + "github.com/p9c/p9/pkg/gel/gio/gpu" + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Window is a headless window. +type Window struct { + size image.Point + ctx context + dev driver.Device + gpu gpu.GPU + fboTex driver.Texture + fbo driver.Framebuffer +} + +type context interface { + API() gpu.API + MakeCurrent() error + ReleaseCurrent() + Release() +} + +// NewWindow creates a new headless window. +func NewWindow(width, height int) (*Window, error) { + ctx, err := newContext() + if err != nil { + return nil, err + } + w := &Window{ + size: image.Point{X: width, Y: height}, + ctx: ctx, + } + err = contextDo(ctx, func() error { + api := ctx.API() + dev, err := driver.NewDevice(api) + if err != nil { + return err + } + dev.Viewport(0, 0, width, height) + fboTex, err := dev.NewTexture( + driver.TextureFormatSRGB, + width, height, + driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingFramebuffer, + ) + if err != nil { + return nil + } + const depthBits = 16 + fbo, err := dev.NewFramebuffer(fboTex, depthBits) + if err != nil { + fboTex.Release() + return err + } + gp, err := gpu.New(api) + if err != nil { + fbo.Release() + fboTex.Release() + return err + } + w.fboTex = fboTex + w.fbo = fbo + w.gpu = gp + w.dev = dev + return err + }) + if err != nil { + ctx.Release() + return nil, err + } + return w, nil +} + +// Release resources associated with the window. +func (w *Window) Release() { + contextDo(w.ctx, func() error { + if w.fbo != nil { + w.fbo.Release() + w.fbo = nil + } + if w.fboTex != nil { + w.fboTex.Release() + w.fboTex = nil + } + if w.gpu != nil { + w.gpu.Release() + w.gpu = nil + } + return nil + }) + if w.ctx != nil { + w.ctx.Release() + w.ctx = nil + } +} + +// Frame replace the window content and state with the +// operation list. +func (w *Window) Frame(frame *op.Ops) error { + return contextDo(w.ctx, func() error { + w.dev.BindFramebuffer(w.fbo) + w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) + w.gpu.Collect(w.size, frame) + return w.gpu.Frame() + }) +} + +// Screenshot returns an image with the content of the window. +func (w *Window) Screenshot() (*image.RGBA, error) { + var img *image.RGBA + err := contextDo(w.ctx, func() error { + var err error + img, err = driver.DownloadImage(w.dev, w.fbo, image.Rectangle{Max: w.size}) + return err + }) + if err != nil { + return nil, err + } + return img, nil +} + +func contextDo(ctx context, f func() error) error { + errCh := make(chan error) + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if err := ctx.MakeCurrent(); err != nil { + errCh <- err + return + } + err := f() + ctx.ReleaseCurrent() + errCh <- err + }() + return <-errCh +} diff --git a/pkg/gel/gio/gpu/headless/headless_darwin.go b/pkg/gel/gio/gpu/headless/headless_darwin.go new file mode 100644 index 0000000..569d5dd --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_darwin.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "github.com/p9c/p9/pkg/gel/gio/gpu" + _ "github.com/p9c/p9/pkg/gel/gio/internal/cocoainit" +) + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c + +#include + +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void); +__attribute__ ((visibility ("hidden"))) void gio_headless_releaseContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef); +__attribute__ ((visibility ("hidden"))) void gio_headless_prepareContext(CFTypeRef ctxRef); +*/ +import "C" + +type nsContext struct { + ctx C.CFTypeRef + prepared bool +} + +func newGLContext() (context, error) { + ctx := C.gio_headless_newContext() + return &nsContext{ctx: ctx}, nil +} + +func (c *nsContext) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *nsContext) MakeCurrent() error { + C.gio_headless_makeCurrentContext(c.ctx) + if !c.prepared { + C.gio_headless_prepareContext(c.ctx) + c.prepared = true + } + return nil +} + +func (c *nsContext) ReleaseCurrent() { + C.gio_headless_clearCurrentContext(c.ctx) +} + +func (d *nsContext) Release() { + if d.ctx != 0 { + C.gio_headless_releaseContext(d.ctx) + d.ctx = 0 + } +} diff --git a/pkg/gel/gio/gpu/headless/headless_egl.go b/pkg/gel/gio/gpu/headless/headless_egl.go new file mode 100644 index 0000000..1c09bc8 --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_egl.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux freebsd windows openbsd + +package headless + +import ( + "github.com/p9c/p9/pkg/gel/gio/internal/egl" +) + +func newGLContext() (context, error) { + return egl.NewContext(egl.EGL_DEFAULT_DISPLAY) +} diff --git a/pkg/gel/gio/gpu/headless/headless_gl.go b/pkg/gel/gio/gpu/headless/headless_gl.go new file mode 100644 index 0000000..c00083e --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_gl.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !windows + +package headless + +func newContext() (context, error) { + return newGLContext() +} diff --git a/pkg/gel/gio/gpu/headless/headless_ios.m b/pkg/gel/gio/gpu/headless/headless_ios.m new file mode 100644 index 0000000..fd72d25 --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_ios.m @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import OpenGLES; + +#include +#include "_cgo_export.h" + +void gio_headless_releaseContext(CFTypeRef ctxRef) { + CFBridgingRelease(ctxRef); +} + +CFTypeRef gio_headless_newContext(void) { + EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (ctx == nil) { + return nil; + } + return CFBridgingRetain(ctx); +} + +void gio_headless_clearCurrentContext(CFTypeRef ctxRef) { + [EAGLContext setCurrentContext:nil]; +} + +void gio_headless_makeCurrentContext(CFTypeRef ctxRef) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + [EAGLContext setCurrentContext:ctx]; +} + +void gio_headless_prepareContext(CFTypeRef ctxRef) { +} diff --git a/pkg/gel/gio/gpu/headless/headless_js.go b/pkg/gel/gio/gpu/headless/headless_js.go new file mode 100644 index 0000000..12a550f --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_js.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "errors" + "syscall/js" + + "github.com/p9c/p9/pkg/gel/gio/gpu" + "github.com/p9c/p9/pkg/gel/gio/internal/gl" +) + +type jsContext struct { + ctx js.Value +} + +func newGLContext() (context, error) { + doc := js.Global().Get("document") + cnv := doc.Call("createElement", "canvas") + ctx := cnv.Call("getContext", "webgl2") + if ctx.IsNull() { + ctx = cnv.Call("getContext", "webgl") + } + if ctx.IsNull() { + return nil, errors.New("headless: webgl is not supported") + } + c := &jsContext{ + ctx: ctx, + } + return c, nil +} + +func (c *jsContext) API() gpu.API { + return gpu.OpenGL{Context: gl.Context(c.ctx)} +} + +func (c *jsContext) Release() { +} + +func (c *jsContext) ReleaseCurrent() { +} + +func (c *jsContext) MakeCurrent() error { + return nil +} diff --git a/pkg/gel/gio/gpu/headless/headless_macos.m b/pkg/gel/gio/gpu/headless/headless_macos.m new file mode 100644 index 0000000..46deb37 --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_macos.m @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; +@import OpenGL; +@import OpenGL.GL; +@import OpenGL.GL3; + +#include +#include "_cgo_export.h" + +void gio_headless_releaseContext(CFTypeRef ctxRef) { + CFBridgingRelease(ctxRef); +} + +CFTypeRef gio_headless_newContext(void) { + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFAAccelerated, + // Opt-in to automatic GPU switching. CGL-only property. + kCGLPFASupportsAutomaticGraphicsSwitching, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + if (pixFormat == nil) { + return NULL; + } + NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil]; + return CFBridgingRetain(ctx); +} + +void gio_headless_clearCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + CGLUnlockContext([ctx CGLContextObj]); + [NSOpenGLContext clearCurrentContext]; +} + +void gio_headless_makeCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + [ctx makeCurrentContext]; + CGLLockContext([ctx CGLContextObj]); +} + +void gio_headless_prepareContext(CFTypeRef ctxRef) { + // Bind a default VBA to emulate OpenGL ES 2. + GLuint defVBA; + glGenVertexArrays(1, &defVBA); + glBindVertexArray(defVBA); + glEnable(GL_FRAMEBUFFER_SRGB); +} diff --git a/pkg/gel/gio/gpu/headless/headless_test.go b/pkg/gel/gio/gpu/headless/headless_test.go new file mode 100644 index 0000000..628624b --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_test.go @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "image" + "image/color" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +func TestHeadless(t *testing.T) { + w, release := newTestWindow(t) + defer release() + + sz := w.size + col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} + var ops op.Ops + paint.ColorOp{Color: col}.Add(&ops) + // Paint only part of the screen to avoid the glClear optimization. + paint.FillShape(&ops, col, clip.Rect(image.Rect(0, 0, sz.X-100, sz.Y-100)).Op()) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if isz := img.Bounds().Size(); isz != sz { + t.Errorf("got %v screenshot, expected %v", isz, sz) + } + if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col) { + t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col)) + } +} + +func TestClipping(t *testing.T) { + w, release := newTestWindow(t) + defer release() + + col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} + col2 := color.NRGBA{A: 0xff, R: 0x00, G: 0xfe} + var ops op.Ops + paint.ColorOp{Color: col}.Add(&ops) + clip.RRect{ + Rect: f32.Rectangle{ + Min: f32.Point{X: 50, Y: 50}, + Max: f32.Point{X: 250, Y: 250}, + }, + SE: 75, + }.Add(&ops) + paint.PaintOp{}.Add(&ops) + paint.ColorOp{Color: col2}.Add(&ops) + clip.RRect{ + Rect: f32.Rectangle{ + Min: f32.Point{X: 100, Y: 100}, + Max: f32.Point{X: 350, Y: 350}, + }, + NW: 75, + }.Add(&ops) + paint.PaintOp{}.Add(&ops) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage("clip.png", img); err != nil { + t.Fatal(err) + } + } + bg := color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff} + tests := []struct { + x, y int + color color.NRGBA + }{ + {120, 120, col}, + {130, 130, col2}, + {210, 210, col2}, + {230, 230, bg}, + } + for _, test := range tests { + if got := img.RGBAAt(test.x, test.y); got != f32color.NRGBAToRGBA(test.color) { + t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, f32color.NRGBAToRGBA(test.color)) + } + } +} + +func TestDepth(t *testing.T) { + w, release := newTestWindow(t) + defer release() + var ops op.Ops + + blue := color.NRGBA{B: 0xFF, A: 0xFF} + paint.FillShape(&ops, blue, clip.Rect(image.Rect(0, 0, 50, 100)).Op()) + red := color.NRGBA{R: 0xFF, A: 0xFF} + paint.FillShape(&ops, red, clip.Rect(image.Rect(0, 0, 100, 50)).Op()) + if err := w.Frame(&ops); err != nil { + t.Fatal(err) + } + + img, err := w.Screenshot() + if err != nil { + t.Fatal(err) + } + if *dumpImages { + if err := saveImage("depth.png", img); err != nil { + t.Fatal(err) + } + } + tests := []struct { + x, y int + color color.NRGBA + }{ + {25, 25, red}, + {75, 25, red}, + {25, 75, blue}, + } + for _, test := range tests { + if got := img.RGBAAt(test.x, test.y); got != f32color.NRGBAToRGBA(test.color) { + t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, f32color.NRGBAToRGBA(test.color)) + } + } +} + +func newTestWindow(t *testing.T) (*Window, func()) { + t.Helper() + sz := image.Point{X: 800, Y: 600} + w, err := NewWindow(sz.X, sz.Y) + if err != nil { + t.Skipf("headless windows not supported: %v", err) + } + return w, func() { + w.Release() + } +} diff --git a/pkg/gel/gio/gpu/headless/headless_windows.go b/pkg/gel/gio/gpu/headless/headless_windows.go new file mode 100644 index 0000000..b656f77 --- /dev/null +++ b/pkg/gel/gio/gpu/headless/headless_windows.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package headless + +import ( + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/gpu" + "github.com/p9c/p9/pkg/gel/gio/internal/d3d11" +) + +type d3d11Context struct { + dev *d3d11.Device +} + +func newContext() (context, error) { + dev, ctx, _, err := d3d11.CreateDevice( + d3d11.DRIVER_TYPE_HARDWARE, + 0, + ) + if err != nil { + return nil, err + } + // Don't need it. + d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) + return &d3d11Context{dev: dev}, nil +} + +func (c *d3d11Context) API() gpu.API { + return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} +} + +func (c *d3d11Context) MakeCurrent() error { + return nil +} + +func (c *d3d11Context) ReleaseCurrent() { +} + +func (c *d3d11Context) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) + c.dev = nil +} diff --git a/pkg/gel/gio/gpu/headless/shaders.go b/pkg/gel/gio/gpu/headless/shaders.go new file mode 100644 index 0000000..896d41f --- /dev/null +++ b/pkg/gel/gio/gpu/headless/shaders.go @@ -0,0 +1,233 @@ +// Code generated by build.go. DO NOT EDIT. + +package headless + +import "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + +var ( + shader_input_vert = driver.ShaderSources{ + Name: "input.vert", + Inputs: []driver.InputLocation{{Name: "position", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 4}}, + GLSL100ES: `#version 100 + +attribute vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL300ES: `#version 300 es + +layout(location = 0) in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec4 position; + +void main() +{ + gl_Position = position; +} + +`, + HLSL: "DXBC\x1e»\x11\xd3iX7\xd4F\xb9\xa4\xf4R\xf9J\x01\x00\x00\x00\x10\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x00\x00\x00\xe0\x00\x00\x00\\\x01\x00\x00\xa8\x01\x00\x00\xdc\x01\x00\x00Aon9\\\x00\x00\x00\\\x00\x00\x00\x00\x02\xfe\xff4\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xff\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\xc0\x00\x00\xff\x90\x00\x00\xe4\xa0\x00\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x00\x00\xe4\x90\xff\xff\x00\x00SHDR<\x00\x00\x00@\x00\x01\x00\x0f\x00\x00\x00_\x00\x00\x03\xf2\x10\x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05\xf2 \x10\x00\x00\x00\x00\x00F\x1e\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x0f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00", + } + shader_simple_frag = driver.ShaderSources{ + Name: "simple.frag", + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +void main() +{ + gl_FragData[0] = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(location = 0) out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec4 fragColor; + +void main() +{ + fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0); +} + +`, + HLSL: "DXBC\xf5F\xdef$)\xa8\xbbV\xeas\xb5ks\x12r\x01\x00\x00\x00\xdc\x01\x00\x00\x06\x00\x00\x008\x00\x00\x00\x90\x00\x00\x00\xd0\x00\x00\x00L\x01\x00\x00\x98\x01\x00\x00\xa8\x01\x00\x00Aon9P\x00\x00\x00P\x00\x00\x00\x00\x02\xff\xff,\x00\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR8\x00\x00\x00@\x00\x00\x00\x0e\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\b\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_simple_vert = driver.ShaderSources{ + Name: "simple.vert", + GLSL100ES: `#version 100 + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + float x; + float y; + if (gl_VertexID == 0) + { + x = 0.0; + y = 0.5; + } + else + { + if (gl_VertexID == 1) + { + x = 0.5; + y = -0.5; + } + else + { + x = -0.5; + y = -0.5; + } + } + gl_Position = vec4(x, y, 0.5, 1.0); +} + +`, + HLSL: "DXBC\xc8 \\\"\xec\xe9\xb2)@\xdf|Z(\xea\f\xb8\x01\x00\x00\x00H\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00\xcc\x01\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDR\xdc\x00\x00\x00@\x00\x01\x007\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00 \x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x01\x00\x00\x007\x00\x00\x0f2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\f2 \x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } +) diff --git a/pkg/gel/gio/gpu/headless/shaders/input.vert b/pkg/gel/gio/gpu/headless/shaders/input.vert new file mode 100644 index 0000000..ed9a4bd --- /dev/null +++ b/pkg/gel/gio/gpu/headless/shaders/input.vert @@ -0,0 +1,11 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision highp float; + +layout(location=0) in vec4 position; + +void main() { + gl_Position = position; +} diff --git a/pkg/gel/gio/gpu/headless/shaders/simple.frag b/pkg/gel/gio/gpu/headless/shaders/simple.frag new file mode 100644 index 0000000..4614f33 --- /dev/null +++ b/pkg/gel/gio/gpu/headless/shaders/simple.frag @@ -0,0 +1,11 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision mediump float; + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = vec4(.25, .55, .75, 1.0); +} diff --git a/pkg/gel/gio/gpu/headless/shaders/simple.vert b/pkg/gel/gio/gpu/headless/shaders/simple.vert new file mode 100644 index 0000000..a226816 --- /dev/null +++ b/pkg/gel/gio/gpu/headless/shaders/simple.vert @@ -0,0 +1,20 @@ +#version 310 es + +// SPDX-License-Identifier: Unlicense OR MIT + +precision highp float; + +void main() { + float x, y; + if (gl_VertexIndex == 0) { + x = 0.0; + y = .5; + } else if (gl_VertexIndex == 1) { + x = .5; + y = -.5; + } else { + x = -.5; + y = -.5; + } + gl_Position = vec4(x, y, 0.5, 1.0); +} diff --git a/pkg/gel/gio/gpu/internal/convertshaders/glslvalidate.go b/pkg/gel/gio/gpu/internal/convertshaders/glslvalidate.go new file mode 100644 index 0000000..0d02a29 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/convertshaders/glslvalidate.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" +) + +// GLSLValidator is OpenGL reference compiler. +type GLSLValidator struct { + Bin string + WorkDir WorkDir +} + +func NewGLSLValidator() *GLSLValidator { return &GLSLValidator{Bin: "glslangValidator"} } + +// Convert converts a glsl shader to spirv. +func (glsl *GLSLValidator) Convert(path, variant string, hlsl bool, input []byte) ([]byte, error) { + base := glsl.WorkDir.Path(filepath.Base(path), variant) + pathout := base + ".out" + + cmd := exec.Command(glsl.Bin, + "--stdin", + "-I"+filepath.Dir(path), + "-V", // OpenGL ES 3.1. + "-w", // Suppress warnings. + "-S", filepath.Ext(path)[1:], + "-o", pathout, + ) + if hlsl { + cmd.Args = append(cmd.Args, "-DHLSL") + } + cmd.Stdin = bytes.NewBuffer(input) + + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(pathout) + if err != nil { + return nil, fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return compiled, nil +} diff --git a/pkg/gel/gio/gpu/internal/convertshaders/hlsl.go b/pkg/gel/gio/gpu/internal/convertshaders/hlsl.go new file mode 100644 index 0000000..a007925 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/convertshaders/hlsl.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +// FXC is hlsl compiler that targets ShaderModel 5.x and lower. +type FXC struct { + Bin string + WorkDir WorkDir +} + +func NewFXC() *FXC { return &FXC{Bin: "fxc.exe"} } + +// Compile compiles the input shader. +func (fxc *FXC) Compile(path, variant string, input []byte, entryPoint string, profileVersion string) (string, error) { + base := fxc.WorkDir.Path(filepath.Base(path), variant, profileVersion) + pathin := base + ".in" + pathout := base + ".out" + result := pathout + + if err := fxc.WorkDir.WriteFile(pathin, input); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(fxc.Bin) + if runtime.GOOS != "windows" { + cmd = exec.Command("wine", fxc.Bin) + if err := winepath(&pathin, &pathout); err != nil { + return "", err + } + } + + var profile string + switch filepath.Ext(path) { + case ".frag": + profile = "ps_" + profileVersion + case ".vert": + profile = "vs_" + profileVersion + case ".comp": + profile = "cs_" + profileVersion + default: + return "", fmt.Errorf("unrecognized shader type %s", path) + } + + cmd.Args = append(cmd.Args, + "/Fo", pathout, + "/T", profile, + "/E", entryPoint, + pathin, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + info := "" + if runtime.GOOS != "windows" { + info = "If the fxc tool cannot be found, set WINEPATH to the Windows path for the Windows SDK.\n" + } + return "", fmt.Errorf("%s\n%sfailed to run %v: %w", output, info, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(result) + if err != nil { + return "", fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return string(compiled), nil +} + +// DXC is hlsl compiler that targets ShaderModel 6.0 and newer. +type DXC struct { + Bin string + WorkDir WorkDir +} + +func NewDXC() *DXC { return &DXC{Bin: "dxc"} } + +// Compile compiles the input shader. +func (dxc *DXC) Compile(path, variant string, input []byte, entryPoint string, profile string) (string, error) { + base := dxc.WorkDir.Path(filepath.Base(path), variant, profile) + pathin := base + ".in" + pathout := base + ".out" + result := pathout + + if err := dxc.WorkDir.WriteFile(pathin, input); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(dxc.Bin) + + cmd.Args = append(cmd.Args, + "-Fo", pathout, + "-T", profile, + "-E", entryPoint, + "-Qstrip_reflect", + pathin, + ) + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\nfailed to run %v: %w", output, cmd.Args, err) + } + + compiled, err := ioutil.ReadFile(result) + if err != nil { + return "", fmt.Errorf("unable to read output %q: %w", pathout, err) + } + + return string(compiled), nil +} + +// winepath uses the winepath tool to convert a paths to Windows format. +// The returned path can be used as arguments for Windows command line tools. +func winepath(paths ...*string) error { + winepath := exec.Command("winepath", "--windows") + for _, path := range paths { + winepath.Args = append(winepath.Args, *path) + } + // Use a pipe instead of Output, because winepath may have left wineserver + // running for several seconds as a grandchild. + out, err := winepath.StdoutPipe() + if err != nil { + return fmt.Errorf("unable to start winepath: %w", err) + } + if err := winepath.Start(); err != nil { + return fmt.Errorf("unable to start winepath: %w", err) + } + var buf bytes.Buffer + if _, err := io.Copy(&buf, out); err != nil { + return fmt.Errorf("unable to run winepath: %w", err) + } + winPaths := strings.Split(strings.TrimSpace(buf.String()), "\n") + for i, path := range paths { + *path = winPaths[i] + } + return nil +} diff --git a/pkg/gel/gio/gpu/internal/convertshaders/main.go b/pkg/gel/gio/gpu/internal/convertshaders/main.go new file mode 100644 index 0000000..0c07581 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/convertshaders/main.go @@ -0,0 +1,410 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + "text/template" + + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" +) + +func main() { + packageName := flag.String("package", "", "specify Go package name") + workdir := flag.String("work", "", "temporary working directory (default TEMP)") + shadersDir := flag.String("dir", "shaders", "shaders directory") + directCompute := flag.Bool("directcompute", false, "enable compiling DirectCompute shaders") + + flag.Parse() + + var work WorkDir + cleanup := func() {} + if *workdir == "" { + tempdir, err := ioutil.TempDir("", "shader-convert") + if err != nil { + fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err) + os.Exit(1) + } + cleanup = func() { os.RemoveAll(tempdir) } + defer cleanup() + + work = WorkDir(tempdir) + } else { + if abs, err := filepath.Abs(*workdir); err == nil { + *workdir = abs + } + work = WorkDir(*workdir) + } + + var out bytes.Buffer + conv := NewConverter(work, *packageName, *shadersDir, *directCompute) + if err := conv.Run(&out); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + cleanup() + os.Exit(1) + } + + if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil { + fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err) + cleanup() + os.Exit(1) + } + + cmd := exec.Command("gofmt", "-s", "-w", "shaders.go") + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err) + cleanup() + os.Exit(1) + } +} + +type Converter struct { + workDir WorkDir + shadersDir string + directCompute bool + + packageName string + + glslvalidator *GLSLValidator + spirv *SPIRVCross + fxc *FXC +} + +func NewConverter(workDir WorkDir, packageName, shadersDir string, directCompute bool) *Converter { + if abs, err := filepath.Abs(shadersDir); err == nil { + shadersDir = abs + } + + conv := &Converter{} + conv.workDir = workDir + conv.shadersDir = shadersDir + conv.directCompute = directCompute + + conv.packageName = packageName + + conv.glslvalidator = NewGLSLValidator() + conv.spirv = NewSPIRVCross() + conv.fxc = NewFXC() + + verifyBinaryPath(&conv.glslvalidator.Bin) + verifyBinaryPath(&conv.spirv.Bin) + // We cannot check fxc since it may depend on wine. + + conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator") + conv.fxc.WorkDir = workDir.Dir("fxc") + conv.spirv.WorkDir = workDir.Dir("spirv") + + return conv +} + +func verifyBinaryPath(bin *string) { + new, err := exec.LookPath(*bin) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err) + } else { + *bin = new + } +} + +func (conv *Converter) Run(out io.Writer) error { + shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*")) + if len(shaders) == 0 || err != nil { + return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir, err) + } + + sort.Strings(shaders) + + var workers Workers + + type ShaderResult struct { + Path string + Shaders []driver.ShaderSources + Error error + } + shaderResults := make([]ShaderResult, len(shaders)) + + for i, shaderPath := range shaders { + i, shaderPath := i, shaderPath + + switch filepath.Ext(shaderPath) { + case ".vert", ".frag": + workers.Go(func() { + shaders, err := conv.Shader(shaderPath) + shaderResults[i] = ShaderResult{ + Path: shaderPath, + Shaders: shaders, + Error: err, + } + }) + case ".comp": + workers.Go(func() { + shaders, err := conv.ComputeShader(shaderPath) + shaderResults[i] = ShaderResult{ + Path: shaderPath, + Shaders: shaders, + Error: err, + } + }) + default: + continue + } + } + + workers.Wait() + + var allErrors string + for _, r := range shaderResults { + if r.Error != nil { + if len(allErrors) > 0 { + allErrors += "\n\n" + } + allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n" + } + } + if len(allErrors) > 0 { + return errors.New(allErrors) + } + + fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n") + fmt.Fprintf(out, "package %s\n\n", conv.packageName) + fmt.Fprintf(out, "import %q\n\n", "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver") + + fmt.Fprintf(out, "var (\n") + + for _, r := range shaderResults { + if len(r.Shaders) == 0 { + continue + } + + name := filepath.Base(r.Path) + name = strings.ReplaceAll(name, ".", "_") + fmt.Fprintf(out, "\tshader_%s = ", name) + + multiVariant := len(r.Shaders) > 1 + if multiVariant { + fmt.Fprintf(out, "[...]driver.ShaderSources{\n") + } + + for _, src := range r.Shaders { + fmt.Fprintf(out, "driver.ShaderSources{\n") + fmt.Fprintf(out, "Name: %#v,\n", src.Name) + if len(src.Inputs) > 0 { + fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs) + } + if u := src.Uniforms; len(u.Blocks) > 0 { + fmt.Fprintf(out, "Uniforms: driver.UniformsReflection{\n") + fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks) + fmt.Fprintf(out, "Locations: %#v,\n", u.Locations) + fmt.Fprintf(out, "Size: %d,\n", u.Size) + fmt.Fprintf(out, "},\n") + } + if len(src.Textures) > 0 { + fmt.Fprintf(out, "Textures: %#v,\n", src.Textures) + } + if len(src.GLSL100ES) > 0 { + fmt.Fprintf(out, "GLSL100ES: `%s`,\n", src.GLSL100ES) + } + if len(src.GLSL300ES) > 0 { + fmt.Fprintf(out, "GLSL300ES: `%s`,\n", src.GLSL300ES) + } + if len(src.GLSL310ES) > 0 { + fmt.Fprintf(out, "GLSL310ES: `%s`,\n", src.GLSL310ES) + } + if len(src.GLSL130) > 0 { + fmt.Fprintf(out, "GLSL130: `%s`,\n", src.GLSL130) + } + if len(src.GLSL150) > 0 { + fmt.Fprintf(out, "GLSL150: `%s`,\n", src.GLSL150) + } + if len(src.HLSL) > 0 { + fmt.Fprintf(out, "HLSL: %q,\n", src.HLSL) + } + fmt.Fprintf(out, "}") + if multiVariant { + fmt.Fprintf(out, ",") + } + fmt.Fprintf(out, "\n") + } + if multiVariant { + fmt.Fprintf(out, "}\n") + } + } + fmt.Fprintf(out, ")\n") + + return nil +} + +func (conv *Converter) Shader(shaderPath string) ([]driver.ShaderSources, error) { + type Variant struct { + FetchColorExpr string + Header string + } + variantArgs := [...]Variant{ + { + FetchColorExpr: `_color.color`, + Header: `layout(binding=0) uniform Color { vec4 color; } _color;`, + }, + { + FetchColorExpr: `mix(_gradient.color1, _gradient.color2, clamp(vUV.x, 0.0, 1.0))`, + Header: `layout(binding=0) uniform Gradient { vec4 color1; vec4 color2; } _gradient;`, + }, + { + FetchColorExpr: `texture(tex, vUV)`, + Header: `layout(binding=0) uniform sampler2D tex;`, + }, + } + + shaderTemplate, err := template.ParseFiles(shaderPath) + if err != nil { + return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath, err) + } + + var variants []driver.ShaderSources + for i, variantArg := range variantArgs { + variantName := strconv.Itoa(i) + var buf bytes.Buffer + err := shaderTemplate.Execute(&buf, variantArg) + if err != nil { + return nil, fmt.Errorf("failed to execute template %q with %#v: %w", shaderPath, variantArg, err) + } + + var sources driver.ShaderSources + sources.Name = filepath.Base(shaderPath) + + // Ignore error; some shaders are not meant to run in GLSL 1.00. + sources.GLSL100ES, _, _ = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "es", "100") + + var metadata Metadata + sources.GLSL300ES, metadata, err = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "es", "300") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err) + } + + sources.GLSL130, _, err = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "glsl", "130") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err) + } + + hlsl, _, err := conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "hlsl", "40") + if err != nil { + return nil, fmt.Errorf("failed to convert HLSL:\n%w", err) + } + sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, []byte(hlsl), "main", "4_0_level_9_1") + if err != nil { + // Attempt shader model 4.0. Only the gpu/headless + // test shaders use features not supported by level + // 9.1. + sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName, []byte(hlsl), "main", "4_0") + if err != nil { + return nil, fmt.Errorf("failed to compile HLSL: %w", err) + } + } + + sources.GLSL150, _, err = conv.ShaderVariant(shaderPath, variantName, buf.Bytes(), "glsl", "150") + if err != nil { + return nil, fmt.Errorf("failed to convert GLSL150:\n%w", err) + } + + sources.Uniforms = metadata.Uniforms + sources.Inputs = metadata.Inputs + sources.Textures = metadata.Textures + + variants = append(variants, sources) + } + + // If the shader don't use the variant arguments, output only a single version. + if variants[0].GLSL100ES == variants[1].GLSL100ES { + variants = variants[:1] + } + + return variants, nil +} + +func (conv *Converter) ShaderVariant(shaderPath, variant string, src []byte, lang, profile string) (string, Metadata, error) { + spirv, err := conv.glslvalidator.Convert(shaderPath, variant, lang == "hlsl", src) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to generate SPIR-V for %q: %w", shaderPath, err) + } + + dst, err := conv.spirv.Convert(shaderPath, variant, spirv, lang, profile) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to convert shader %q: %w", shaderPath, err) + } + + meta, err := conv.spirv.Metadata(shaderPath, variant, spirv) + if err != nil { + return "", Metadata{}, fmt.Errorf("failed to extract metadata for shader %q: %w", shaderPath, err) + } + + return dst, meta, nil +} + +func (conv *Converter) ComputeShader(shaderPath string) ([]driver.ShaderSources, error) { + shader, err := ioutil.ReadFile(shaderPath) + if err != nil { + return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err) + } + + spirv, err := conv.glslvalidator.Convert(shaderPath, "", false, shader) + if err != nil { + return nil, fmt.Errorf("failed to convert compute shader %q: %w", shaderPath, err) + } + + var sources driver.ShaderSources + sources.Name = filepath.Base(shaderPath) + + sources.GLSL310ES, err = conv.spirv.Convert(shaderPath, "", spirv, "es", "310") + if err != nil { + return nil, fmt.Errorf("failed to convert es compute shader %q: %w", shaderPath, err) + } + sources.GLSL310ES = unixLineEnding(sources.GLSL310ES) + + hlslSource, err := conv.spirv.Convert(shaderPath, "", spirv, "hlsl", "50") + if err != nil { + return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w", shaderPath, err) + } + + dxil, err := conv.fxc.Compile(shaderPath, "0", []byte(hlslSource), "main", "5_0") + if err != nil { + return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w", shaderPath, err) + } + if conv.directCompute { + sources.HLSL = dxil + } + + return []driver.ShaderSources{sources}, nil +} + +// Workers implements wait group with synchronous logging. +type Workers struct { + running sync.WaitGroup +} + +func (lg *Workers) Go(fn func()) { + lg.running.Add(1) + go func() { + defer lg.running.Done() + fn() + }() +} + +func (lg *Workers) Wait() { + lg.running.Wait() +} + +func unixLineEnding(s string) string { + return strings.ReplaceAll(s, "\r\n", "\n") +} diff --git a/pkg/gel/gio/gpu/internal/convertshaders/spirvcross.go b/pkg/gel/gio/gpu/internal/convertshaders/spirvcross.go new file mode 100644 index 0000000..1a9682d --- /dev/null +++ b/pkg/gel/gio/gpu/internal/convertshaders/spirvcross.go @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + "sort" + "strings" + + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" +) + +// Metadata contains reflection data about a shader. +type Metadata struct { + Uniforms driver.UniformsReflection + Inputs []driver.InputLocation + Textures []driver.TextureBinding +} + +// SPIRVCross cross-compiles spirv shaders to es, hlsl and others. +type SPIRVCross struct { + Bin string + WorkDir WorkDir +} + +func NewSPIRVCross() *SPIRVCross { return &SPIRVCross{Bin: "spirv-cross"} } + +// Convert converts compute shader from spirv format to a target format. +func (spirv *SPIRVCross) Convert(path, variant string, shader []byte, target, version string) (string, error) { + base := spirv.WorkDir.Path(filepath.Base(path), variant) + + if err := spirv.WorkDir.WriteFile(base, shader); err != nil { + return "", fmt.Errorf("unable to write shader to disk: %w", err) + } + + var cmd *exec.Cmd + switch target { + case "glsl": + cmd = exec.Command(spirv.Bin, + "--no-es", + "--version", version, + ) + case "es": + cmd = exec.Command(spirv.Bin, + "--es", + "--version", version, + ) + case "hlsl": + cmd = exec.Command(spirv.Bin, + "--hlsl", + "--shader-model", version, + ) + default: + return "", fmt.Errorf("unknown target %q", target) + } + cmd.Args = append(cmd.Args, "--no-420pack-extension", base) + + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err) + } + s := string(out) + if target != "hlsl" { + // Strip Windows \r in line endings. + s = unixLineEnding(s) + } + + return s, nil +} + +// Metadata extracts metadata for a SPIR-V shader. +func (spirv *SPIRVCross) Metadata(path, variant string, shader []byte) (Metadata, error) { + base := spirv.WorkDir.Path(filepath.Base(path), variant) + + if err := spirv.WorkDir.WriteFile(base, shader); err != nil { + return Metadata{}, fmt.Errorf("unable to write shader to disk: %w", err) + } + + cmd := exec.Command(spirv.Bin, + base, + "--reflect", + ) + + out, err := cmd.Output() + if err != nil { + return Metadata{}, fmt.Errorf("failed to run %v: %w", cmd.Args, err) + } + + meta, err := parseMetadata(out) + if err != nil { + return Metadata{}, fmt.Errorf("%s\nfailed to parse metadata: %w", out, err) + } + + return meta, nil +} + +func parseMetadata(data []byte) (Metadata, error) { + var reflect struct { + Types map[string]struct { + Name string `json:"name"` + Members []struct { + Name string `json:"name"` + Type string `json:"type"` + Offset int `json:"offset"` + } `json:"members"` + } `json:"types"` + Inputs []struct { + Name string `json:"name"` + Type string `json:"type"` + Location int `json:"location"` + } `json:"inputs"` + Textures []struct { + Name string `json:"name"` + Type string `json:"type"` + Set int `json:"set"` + Binding int `json:"binding"` + } `json:"textures"` + UBOs []struct { + Name string `json:"name"` + Type string `json:"type"` + BlockSize int `json:"block_size"` + Set int `json:"set"` + Binding int `json:"binding"` + } `json:"ubos"` + } + if err := json.Unmarshal(data, &reflect); err != nil { + return Metadata{}, fmt.Errorf("failed to parse reflection data: %w", err) + } + + var m Metadata + + for _, input := range reflect.Inputs { + dataType, dataSize, err := parseDataType(input.Type) + if err != nil { + return Metadata{}, fmt.Errorf("parseReflection: %v", err) + } + m.Inputs = append(m.Inputs, driver.InputLocation{ + Name: input.Name, + Location: input.Location, + Semantic: "TEXCOORD", + SemanticIndex: input.Location, + Type: dataType, + Size: dataSize, + }) + } + + sort.Slice(m.Inputs, func(i, j int) bool { + return m.Inputs[i].Location < m.Inputs[j].Location + }) + + blockOffset := 0 + for _, block := range reflect.UBOs { + m.Uniforms.Blocks = append(m.Uniforms.Blocks, driver.UniformBlock{ + Name: block.Name, + Binding: block.Binding, + }) + t := reflect.Types[block.Type] + // By convention uniform block variables are named by prepending an underscore + // and converting to lowercase. + blockVar := "_" + strings.ToLower(block.Name) + for _, member := range t.Members { + dataType, size, err := parseDataType(member.Type) + if err != nil { + return Metadata{}, fmt.Errorf("failed to parse reflection data: %v", err) + } + m.Uniforms.Locations = append(m.Uniforms.Locations, driver.UniformLocation{ + Name: fmt.Sprintf("%s.%s", blockVar, member.Name), + Type: dataType, + Size: size, + Offset: blockOffset + member.Offset, + }) + } + blockOffset += block.BlockSize + } + m.Uniforms.Size = blockOffset + + for _, texture := range reflect.Textures { + m.Textures = append(m.Textures, driver.TextureBinding{ + Name: texture.Name, + Binding: texture.Binding, + }) + } + + //return m, fmt.Errorf("not yet!: %+v", reflect) + return m, nil +} + +func parseDataType(t string) (driver.DataType, int, error) { + switch t { + case "float": + return driver.DataTypeFloat, 1, nil + case "vec2": + return driver.DataTypeFloat, 2, nil + case "vec3": + return driver.DataTypeFloat, 3, nil + case "vec4": + return driver.DataTypeFloat, 4, nil + case "int": + return driver.DataTypeInt, 1, nil + case "int2": + return driver.DataTypeInt, 2, nil + case "int3": + return driver.DataTypeInt, 3, nil + case "int4": + return driver.DataTypeInt, 4, nil + default: + return 0, 0, fmt.Errorf("unsupported input data type: %s", t) + } +} diff --git a/pkg/gel/gio/gpu/internal/convertshaders/workdir.go b/pkg/gel/gio/gpu/internal/convertshaders/workdir.go new file mode 100644 index 0000000..4c1c092 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/convertshaders/workdir.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +type WorkDir string + +func (wd WorkDir) Dir(path string) WorkDir { + dirname := filepath.Join(string(wd), path) + if err := os.Mkdir(dirname, 0755); err != nil { + if !os.IsExist(err) { + fmt.Fprintf(os.Stderr, "failed to create %q: %v\n", dirname, err) + } + } + return WorkDir(dirname) +} + +func (wd WorkDir) Path(path ...string) (fullpath string) { + return filepath.Join(string(wd), strings.Join(path, ".")) +} + +func (wd WorkDir) WriteFile(path string, data []byte) error { + err := ioutil.WriteFile(path, data, 0644) + if err != nil { + return fmt.Errorf("unable to create %v: %w", path, err) + } + return nil +} diff --git a/pkg/gel/gio/gpu/internal/d3d11/d3d11.go b/pkg/gel/gio/gpu/internal/d3d11/d3d11.go new file mode 100644 index 0000000..3ddf7c3 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/d3d11/d3d11.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// This file exists so this package builds on non-Windows platforms. + +package d3d11 diff --git a/pkg/gel/gio/gpu/internal/d3d11/d3d11_windows.go b/pkg/gel/gio/gpu/internal/d3d11/d3d11_windows.go new file mode 100644 index 0000000..4762cf4 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/d3d11/d3d11_windows.go @@ -0,0 +1,761 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "errors" + "fmt" + "image" + "math" + "reflect" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/internal/d3d11" +) + +type Backend struct { + dev *d3d11.Device + ctx *d3d11.DeviceContext + + // Temporary storage to avoid garbage. + clearColor [4]float32 + viewport d3d11.VIEWPORT + depthState depthState + blendState blendState + + // Current program. + prog *Program + + caps driver.Caps + + // fbo is the currently bound fbo. + fbo *Framebuffer + + floatFormat uint32 + + // cached state objects. + depthStates map[depthState]*d3d11.DepthStencilState + blendStates map[blendState]*d3d11.BlendState +} + +type blendState struct { + enable bool + sfactor driver.BlendFactor + dfactor driver.BlendFactor +} + +type depthState struct { + enable bool + mask bool + fn driver.DepthFunc +} + +type Texture struct { + backend *Backend + format uint32 + bindings driver.BufferBinding + tex *d3d11.Texture2D + sampler *d3d11.SamplerState + resView *d3d11.ShaderResourceView + width int + height int +} + +type Program struct { + backend *Backend + + vert struct { + shader *d3d11.VertexShader + uniforms *Buffer + } + frag struct { + shader *d3d11.PixelShader + uniforms *Buffer + } +} + +type Framebuffer struct { + dev *d3d11.Device + ctx *d3d11.DeviceContext + format uint32 + resource *d3d11.Resource + renderTarget *d3d11.RenderTargetView + depthView *d3d11.DepthStencilView + foreign bool +} + +type Buffer struct { + backend *Backend + bind uint32 + buf *d3d11.Buffer + immutable bool +} + +type InputLayout struct { + layout *d3d11.InputLayout +} + +func init() { + driver.NewDirect3D11Device = newDirect3D11Device +} + +func detectFloatFormat(dev *d3d11.Device) (uint32, bool) { + formats := []uint32{ + d3d11.DXGI_FORMAT_R16_FLOAT, + d3d11.DXGI_FORMAT_R32_FLOAT, + d3d11.DXGI_FORMAT_R16G16_FLOAT, + d3d11.DXGI_FORMAT_R32G32_FLOAT, + // These last two are really wasteful, but c'est la vie. + d3d11.DXGI_FORMAT_R16G16B16A16_FLOAT, + d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT, + } + for _, format := range formats { + need := uint32(d3d11.FORMAT_SUPPORT_TEXTURE2D | d3d11.FORMAT_SUPPORT_RENDER_TARGET) + if support, _ := dev.CheckFormatSupport(format); support&need == need { + return format, true + } + } + return 0, false +} + +func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) { + dev := (*d3d11.Device)(api.Device) + b := &Backend{ + dev: dev, + ctx: dev.GetImmediateContext(), + caps: driver.Caps{ + MaxTextureSize: 2048, // 9.1 maximum + }, + depthStates: make(map[depthState]*d3d11.DepthStencilState), + blendStates: make(map[blendState]*d3d11.BlendState), + } + featLvl := dev.GetFeatureLevel() + if featLvl < d3d11.FEATURE_LEVEL_9_1 { + d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) + return nil, fmt.Errorf("d3d11: feature level too low: %d", featLvl) + } + switch { + case featLvl >= d3d11.FEATURE_LEVEL_11_0: + b.caps.MaxTextureSize = 16384 + case featLvl >= d3d11.FEATURE_LEVEL_9_3: + b.caps.MaxTextureSize = 4096 + } + if fmt, ok := detectFloatFormat(dev); ok { + b.floatFormat = fmt + b.caps.Features |= driver.FeatureFloatRenderTargets + } + // Enable depth mask to match OpenGL. + b.depthState.mask = true + // Disable backface culling to match OpenGL. + state, err := dev.CreateRasterizerState(&d3d11.RASTERIZER_DESC{ + CullMode: d3d11.CULL_NONE, + FillMode: d3d11.FILL_SOLID, + DepthClipEnable: 1, + }) + if err != nil { + return nil, err + } + defer d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + b.ctx.RSSetState(state) + return b, nil +} + +func (b *Backend) BeginFrame() driver.Framebuffer { + renderTarget, depthView := b.ctx.OMGetRenderTargets() + // Assume someone else is holding on to the render targets. + if renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), renderTarget.Vtbl.Release) + } + if depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(depthView), depthView.Vtbl.Release) + } + return &Framebuffer{ctx: b.ctx, dev: b.dev, renderTarget: renderTarget, depthView: depthView, foreign: true} +} + +func (b *Backend) EndFrame() { +} + +func (b *Backend) Caps() driver.Caps { + return b.caps +} + +func (b *Backend) NewTimer() driver.Timer { + panic("timers not supported") +} + +func (b *Backend) IsTimeContinuous() bool { + panic("timers not supported") +} + +func (b *Backend) Release() { + for _, state := range b.depthStates { + d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + } + for _, state := range b.blendStates { + d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) + } + d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) + *b = Backend{} +} + +func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, bindings driver.BufferBinding) (driver.Texture, error) { + var d3dfmt uint32 + switch format { + case driver.TextureFormatFloat: + d3dfmt = b.floatFormat + case driver.TextureFormatSRGB: + d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + default: + return nil, fmt.Errorf("unsupported texture format %d", format) + } + tex, err := b.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ + Width: uint32(width), + Height: uint32(height), + MipLevels: 1, + ArraySize: 1, + Format: d3dfmt, + SampleDesc: d3d11.DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + BindFlags: convBufferBinding(bindings), + }) + if err != nil { + return nil, err + } + var ( + sampler *d3d11.SamplerState + resView *d3d11.ShaderResourceView + ) + if bindings&driver.BufferBindingTexture != 0 { + var filter uint32 + switch { + case minFilter == driver.FilterNearest && magFilter == driver.FilterNearest: + filter = d3d11.FILTER_MIN_MAG_MIP_POINT + case minFilter == driver.FilterLinear && magFilter == driver.FilterLinear: + filter = d3d11.FILTER_MIN_MAG_LINEAR_MIP_POINT + default: + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + return nil, fmt.Errorf("unsupported texture filter combination %d, %d", minFilter, magFilter) + } + var err error + sampler, err = b.dev.CreateSamplerState(&d3d11.SAMPLER_DESC{ + Filter: filter, + AddressU: d3d11.TEXTURE_ADDRESS_CLAMP, + AddressV: d3d11.TEXTURE_ADDRESS_CLAMP, + AddressW: d3d11.TEXTURE_ADDRESS_CLAMP, + MaxAnisotropy: 1, + MinLOD: -math.MaxFloat32, + MaxLOD: math.MaxFloat32, + }) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + return nil, err + } + resView, err = b.dev.CreateShaderResourceViewTEX2D( + (*d3d11.Resource)(unsafe.Pointer(tex)), + &d3d11.SHADER_RESOURCE_VIEW_DESC_TEX2D{ + SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{ + Format: d3dfmt, + ViewDimension: d3d11.SRV_DIMENSION_TEXTURE2D, + }, + Texture2D: d3d11.TEX2D_SRV{ + MostDetailedMip: 0, + MipLevels: ^uint32(0), + }, + }, + ) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release) + return nil, err + } + } + return &Texture{backend: b, format: d3dfmt, tex: tex, sampler: sampler, resView: resView, bindings: bindings, width: width, height: height}, nil +} + +func (b *Backend) NewFramebuffer(tex driver.Texture, depthBits int) (driver.Framebuffer, error) { + d3dtex := tex.(*Texture) + if d3dtex.bindings&driver.BufferBindingFramebuffer == 0 { + return nil, errors.New("the texture was created without BufferBindingFramebuffer binding") + } + resource := (*d3d11.Resource)(unsafe.Pointer(d3dtex.tex)) + renderTarget, err := b.dev.CreateRenderTargetView(resource) + if err != nil { + return nil, err + } + fbo := &Framebuffer{ctx: b.ctx, dev: b.dev, format: d3dtex.format, resource: resource, renderTarget: renderTarget} + if depthBits > 0 { + depthView, err := d3d11.CreateDepthView(b.dev, d3dtex.width, d3dtex.height, depthBits) + if err != nil { + d3d11.IUnknownRelease(unsafe.Pointer(renderTarget), renderTarget.Vtbl.Release) + return nil, err + } + fbo.depthView = depthView + } + return fbo, nil +} + +func (b *Backend) NewInputLayout(vertexShader driver.ShaderSources, layout []driver.InputDesc) (driver.InputLayout, error) { + if len(vertexShader.Inputs) != len(layout) { + return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", len(layout), len(vertexShader.Inputs)) + } + descs := make([]d3d11.INPUT_ELEMENT_DESC, len(layout)) + for i, l := range layout { + inp := vertexShader.Inputs[i] + cname, err := windows.BytePtrFromString(inp.Semantic) + if err != nil { + return nil, err + } + var format uint32 + switch l.Type { + case driver.DataTypeFloat: + switch l.Size { + case 1: + format = d3d11.DXGI_FORMAT_R32_FLOAT + case 2: + format = d3d11.DXGI_FORMAT_R32G32_FLOAT + case 3: + format = d3d11.DXGI_FORMAT_R32G32B32_FLOAT + case 4: + format = d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT + default: + panic("unsupported data size") + } + case driver.DataTypeShort: + switch l.Size { + case 1: + format = d3d11.DXGI_FORMAT_R16_SINT + case 2: + format = d3d11.DXGI_FORMAT_R16G16_SINT + default: + panic("unsupported data size") + } + default: + panic("unsupported data type") + } + descs[i] = d3d11.INPUT_ELEMENT_DESC{ + SemanticName: cname, + SemanticIndex: uint32(inp.SemanticIndex), + Format: format, + AlignedByteOffset: uint32(l.Offset), + } + } + l, err := b.dev.CreateInputLayout(descs, []byte(vertexShader.HLSL)) + if err != nil { + return nil, err + } + return &InputLayout{layout: l}, nil +} + +func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) { + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniform buffers cannot have other bindings") + } + if size%16 != 0 { + return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", size) + } + } + bind := convBufferBinding(typ) + buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ + ByteWidth: uint32(size), + BindFlags: bind, + }, nil) + if err != nil { + return nil, err + } + return &Buffer{backend: b, buf: buf, bind: bind}, nil +} + +func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) { + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniform buffers cannot have other bindings") + } + if len(data)%16 != 0 { + return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", len(data)) + } + } + bind := convBufferBinding(typ) + buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ + ByteWidth: uint32(len(data)), + Usage: d3d11.USAGE_IMMUTABLE, + BindFlags: bind, + }, data) + if err != nil { + return nil, err + } + return &Buffer{backend: b, buf: buf, bind: bind, immutable: true}, nil +} + +func (b *Backend) NewComputeProgram(shader driver.ShaderSources) (driver.Program, error) { + panic("not implemented") +} + +func (b *Backend) NewProgram(vertexShader, fragmentShader driver.ShaderSources) (driver.Program, error) { + vs, err := b.dev.CreateVertexShader([]byte(vertexShader.HLSL)) + if err != nil { + return nil, err + } + ps, err := b.dev.CreatePixelShader([]byte(fragmentShader.HLSL)) + if err != nil { + return nil, err + } + p := &Program{backend: b} + p.vert.shader = vs + p.frag.shader = ps + return p, nil +} + +func (b *Backend) Clear(colr, colg, colb, cola float32) { + b.clearColor = [4]float32{colr, colg, colb, cola} + b.ctx.ClearRenderTargetView(b.fbo.renderTarget, &b.clearColor) +} + +func (b *Backend) ClearDepth(depth float32) { + if b.fbo.depthView != nil { + b.ctx.ClearDepthStencilView(b.fbo.depthView, d3d11.CLEAR_DEPTH|d3d11.CLEAR_STENCIL, depth, 0) + } +} + +func (b *Backend) Viewport(x, y, width, height int) { + b.viewport = d3d11.VIEWPORT{ + TopLeftX: float32(x), + TopLeftY: float32(y), + Width: float32(width), + Height: float32(height), + MinDepth: 0.0, + MaxDepth: 1.0, + } + b.ctx.RSSetViewports(&b.viewport) +} + +func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) { + b.prepareDraw(mode) + b.ctx.Draw(uint32(count), uint32(off)) +} + +func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) { + b.prepareDraw(mode) + b.ctx.DrawIndexed(uint32(count), uint32(off), 0) +} + +func (b *Backend) prepareDraw(mode driver.DrawMode) { + if p := b.prog; p != nil { + b.ctx.VSSetShader(p.vert.shader) + b.ctx.PSSetShader(p.frag.shader) + if buf := p.vert.uniforms; buf != nil { + b.ctx.VSSetConstantBuffers(buf.buf) + } + if buf := p.frag.uniforms; buf != nil { + b.ctx.PSSetConstantBuffers(buf.buf) + } + } + var topology uint32 + switch mode { + case driver.DrawModeTriangles: + topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLELIST + case driver.DrawModeTriangleStrip: + topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLESTRIP + default: + panic("unsupported draw mode") + } + b.ctx.IASetPrimitiveTopology(topology) + + depthState, ok := b.depthStates[b.depthState] + if !ok { + var desc d3d11.DEPTH_STENCIL_DESC + if b.depthState.enable { + desc.DepthEnable = 1 + } + if b.depthState.mask { + desc.DepthWriteMask = d3d11.DEPTH_WRITE_MASK_ALL + } + switch b.depthState.fn { + case driver.DepthFuncGreater: + desc.DepthFunc = d3d11.COMPARISON_GREATER + case driver.DepthFuncGreaterEqual: + desc.DepthFunc = d3d11.COMPARISON_GREATER_EQUAL + default: + panic("unsupported depth func") + } + var err error + depthState, err = b.dev.CreateDepthStencilState(&desc) + if err != nil { + panic(err) + } + b.depthStates[b.depthState] = depthState + } + b.ctx.OMSetDepthStencilState(depthState, 0) + + blendState, ok := b.blendStates[b.blendState] + if !ok { + var desc d3d11.BLEND_DESC + t0 := &desc.RenderTarget[0] + t0.RenderTargetWriteMask = d3d11.COLOR_WRITE_ENABLE_ALL + t0.BlendOp = d3d11.BLEND_OP_ADD + t0.BlendOpAlpha = d3d11.BLEND_OP_ADD + if b.blendState.enable { + t0.BlendEnable = 1 + } + scol, salpha := toBlendFactor(b.blendState.sfactor) + dcol, dalpha := toBlendFactor(b.blendState.dfactor) + t0.SrcBlend = scol + t0.SrcBlendAlpha = salpha + t0.DestBlend = dcol + t0.DestBlendAlpha = dalpha + var err error + blendState, err = b.dev.CreateBlendState(&desc) + if err != nil { + panic(err) + } + b.blendStates[b.blendState] = blendState + } + b.ctx.OMSetBlendState(blendState, nil, 0xffffffff) +} + +func (b *Backend) DepthFunc(f driver.DepthFunc) { + b.depthState.fn = f +} + +func (b *Backend) SetBlend(enable bool) { + b.blendState.enable = enable +} + +func (b *Backend) SetDepthTest(enable bool) { + b.depthState.enable = enable +} + +func (b *Backend) DepthMask(mask bool) { + b.depthState.mask = mask +} + +func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { + b.blendState.sfactor = sfactor + b.blendState.dfactor = dfactor +} + +func (b *Backend) BindImageTexture(unit int, tex driver.Texture, access driver.AccessBits, f driver.TextureFormat) { + panic("not implemented") +} + +func (b *Backend) MemoryBarrier() { + panic("not implemented") +} + +func (b *Backend) DispatchCompute(x, y, z int) { + panic("not implemented") +} + +func (t *Texture) Upload(offset, size image.Point, pixels []byte) { + stride := size.X * 4 + dst := &d3d11.BOX{ + Left: uint32(offset.X), + Top: uint32(offset.Y), + Right: uint32(offset.X + size.X), + Bottom: uint32(offset.Y + size.Y), + Front: 0, + Back: 1, + } + res := (*d3d11.Resource)(unsafe.Pointer(t.tex)) + t.backend.ctx.UpdateSubresource(res, dst, uint32(stride), uint32(len(pixels)), pixels) +} + +func (t *Texture) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(t.tex), t.tex.Vtbl.Release) + t.tex = nil + if t.sampler != nil { + d3d11.IUnknownRelease(unsafe.Pointer(t.sampler), t.sampler.Vtbl.Release) + t.sampler = nil + } + if t.resView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(t.resView), t.resView.Vtbl.Release) + t.resView = nil + } +} + +func (b *Backend) BindTexture(unit int, tex driver.Texture) { + t := tex.(*Texture) + b.ctx.PSSetSamplers(uint32(unit), t.sampler) + b.ctx.PSSetShaderResources(uint32(unit), t.resView) +} + +func (b *Backend) BindProgram(prog driver.Program) { + b.prog = prog.(*Program) +} + +func (p *Program) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(p.vert.shader), p.vert.shader.Vtbl.Release) + d3d11.IUnknownRelease(unsafe.Pointer(p.frag.shader), p.frag.shader.Vtbl.Release) + p.vert.shader = nil + p.frag.shader = nil +} + +func (p *Program) SetStorageBuffer(binding int, buffer driver.Buffer) { + panic("not implemented") +} + +func (p *Program) SetVertexUniforms(buf driver.Buffer) { + p.vert.uniforms = buf.(*Buffer) +} + +func (p *Program) SetFragmentUniforms(buf driver.Buffer) { + p.frag.uniforms = buf.(*Buffer) +} + +func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) { + b.ctx.IASetVertexBuffers(buf.(*Buffer).buf, uint32(stride), uint32(offset)) +} + +func (b *Backend) BindIndexBuffer(buf driver.Buffer) { + b.ctx.IASetIndexBuffer(buf.(*Buffer).buf, d3d11.DXGI_FORMAT_R16_UINT, 0) +} + +func (b *Buffer) Download(data []byte) error { + panic("not implemented") +} + +func (b *Buffer) Upload(data []byte) { + b.backend.ctx.UpdateSubresource((*d3d11.Resource)(unsafe.Pointer(b.buf)), nil, 0, 0, data) +} + +func (b *Buffer) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(b.buf), b.buf.Vtbl.Release) + b.buf = nil +} + +func (f *Framebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { + if f.resource == nil { + return errors.New("framebuffer does not support ReadPixels") + } + w, h := src.Dx(), src.Dy() + tex, err := f.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ + Width: uint32(w), + Height: uint32(h), + MipLevels: 1, + ArraySize: 1, + Format: f.format, + SampleDesc: d3d11.DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + Usage: d3d11.USAGE_STAGING, + CPUAccessFlags: d3d11.CPU_ACCESS_READ, + }) + if err != nil { + return fmt.Errorf("ReadPixels: %v", err) + } + defer d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) + res := (*d3d11.Resource)(unsafe.Pointer(tex)) + f.ctx.CopySubresourceRegion( + res, + 0, // Destination subresource. + 0, 0, 0, // Destination coordinates (x, y, z). + f.resource, + 0, // Source subresource. + &d3d11.BOX{ + Left: uint32(src.Min.X), + Top: uint32(src.Min.Y), + Right: uint32(src.Max.X), + Bottom: uint32(src.Max.Y), + Front: 0, + Back: 1, + }, + ) + resMap, err := f.ctx.Map(res, 0, d3d11.MAP_READ, 0) + if err != nil { + return fmt.Errorf("ReadPixels: %v", err) + } + defer f.ctx.Unmap(res, 0) + srcPitch := w * 4 + dstPitch := int(resMap.RowPitch) + mapSize := dstPitch * h + data := sliceOf(resMap.PData, mapSize) + width := w * 4 + for r := 0; r < h; r++ { + pixels := pixels[r*srcPitch:] + copy(pixels[:width], data[r*dstPitch:]) + } + return nil +} + +func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { + b.fbo = fbo.(*Framebuffer) + b.ctx.OMSetRenderTargets(b.fbo.renderTarget, b.fbo.depthView) +} + +func (f *Framebuffer) Invalidate() { +} + +func (f *Framebuffer) Release() { + if f.foreign { + panic("framebuffer not created by NewFramebuffer") + } + if f.renderTarget != nil { + d3d11.IUnknownRelease(unsafe.Pointer(f.renderTarget), f.renderTarget.Vtbl.Release) + f.renderTarget = nil + } + if f.depthView != nil { + d3d11.IUnknownRelease(unsafe.Pointer(f.depthView), f.depthView.Vtbl.Release) + f.depthView = nil + } +} + +func (b *Backend) BindInputLayout(layout driver.InputLayout) { + b.ctx.IASetInputLayout(layout.(*InputLayout).layout) +} + +func (l *InputLayout) Release() { + d3d11.IUnknownRelease(unsafe.Pointer(l.layout), l.layout.Vtbl.Release) + l.layout = nil +} + +func convBufferBinding(typ driver.BufferBinding) uint32 { + var bindings uint32 + if typ&driver.BufferBindingVertices != 0 { + bindings |= d3d11.BIND_VERTEX_BUFFER + } + if typ&driver.BufferBindingIndices != 0 { + bindings |= d3d11.BIND_INDEX_BUFFER + } + if typ&driver.BufferBindingUniforms != 0 { + bindings |= d3d11.BIND_CONSTANT_BUFFER + } + if typ&driver.BufferBindingTexture != 0 { + bindings |= d3d11.BIND_SHADER_RESOURCE + } + if typ&driver.BufferBindingFramebuffer != 0 { + bindings |= d3d11.BIND_RENDER_TARGET + } + return bindings +} + +func toBlendFactor(f driver.BlendFactor) (uint32, uint32) { + switch f { + case driver.BlendFactorOne: + return d3d11.BLEND_ONE, d3d11.BLEND_ONE + case driver.BlendFactorOneMinusSrcAlpha: + return d3d11.BLEND_INV_SRC_ALPHA, d3d11.BLEND_INV_SRC_ALPHA + case driver.BlendFactorZero: + return d3d11.BLEND_ZERO, d3d11.BLEND_ZERO + case driver.BlendFactorDstColor: + return d3d11.BLEND_DEST_COLOR, d3d11.BLEND_DEST_ALPHA + default: + panic("unsupported blend source factor") + } +} + +// sliceOf returns a slice from a (native) pointer. +func sliceOf(ptr uintptr, cap int) []byte { + var data []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&data)) + h.Data = ptr + h.Cap = cap + h.Len = cap + return data +} diff --git a/pkg/gel/gio/gpu/internal/driver/api.go b/pkg/gel/gio/gpu/internal/driver/api.go new file mode 100644 index 0000000..3e37aa4 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/driver/api.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package driver + +import ( + "fmt" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/internal/gl" +) + +// See gpu/api.go for documentation for the API types + +type API interface { + implementsAPI() +} + +type OpenGL struct { + // Context contains the WebGL context for WebAssembly platforms. It is + // empty for all other platforms; an OpenGL context is assumed current when + // calling NewDevice. + Context gl.Context +} + +type Direct3D11 struct { + // Device contains a *ID3D11Device. + Device unsafe.Pointer +} + +// API specific device constructors. +var ( + NewOpenGLDevice func(api OpenGL) (Device, error) + NewDirect3D11Device func(api Direct3D11) (Device, error) +) + +// NewDevice creates a new Device given the api. +// +// Note that the device does not assume ownership of the resources contained in +// api; the caller must ensure the resources are valid until the device is +// released. +func NewDevice(api API) (Device, error) { + switch api := api.(type) { + case OpenGL: + if NewOpenGLDevice != nil { + return NewOpenGLDevice(api) + } + case Direct3D11: + if NewDirect3D11Device != nil { + return NewDirect3D11Device(api) + } + } + return nil, fmt.Errorf("driver: no driver available for the API %T", api) +} + +func (OpenGL) implementsAPI() {} +func (Direct3D11) implementsAPI() {} diff --git a/pkg/gel/gio/gpu/internal/driver/driver.go b/pkg/gel/gio/gpu/internal/driver/driver.go new file mode 100644 index 0000000..14d3d85 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/driver/driver.go @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package driver + +import ( + "errors" + "image" + "time" +) + +// Device represents the abstraction of underlying GPU +// APIs such as OpenGL, Direct3D useful for rendering Gio +// operations. +type Device interface { + BeginFrame() Framebuffer + EndFrame() + Caps() Caps + NewTimer() Timer + // IsContinuousTime reports whether all timer measurements + // are valid at the point of call. + IsTimeContinuous() bool + NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, bindings BufferBinding) (Texture, error) + NewFramebuffer(tex Texture, depthBits int) (Framebuffer, error) + NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error) + NewBuffer(typ BufferBinding, size int) (Buffer, error) + NewComputeProgram(shader ShaderSources) (Program, error) + NewProgram(vertexShader, fragmentShader ShaderSources) (Program, error) + NewInputLayout(vertexShader ShaderSources, layout []InputDesc) (InputLayout, error) + + DepthFunc(f DepthFunc) + ClearDepth(d float32) + Clear(r, g, b, a float32) + Viewport(x, y, width, height int) + DrawArrays(mode DrawMode, off, count int) + DrawElements(mode DrawMode, off, count int) + SetBlend(enable bool) + SetDepthTest(enable bool) + DepthMask(mask bool) + BlendFunc(sfactor, dfactor BlendFactor) + + BindInputLayout(i InputLayout) + BindProgram(p Program) + BindFramebuffer(f Framebuffer) + BindTexture(unit int, t Texture) + BindVertexBuffer(b Buffer, stride, offset int) + BindIndexBuffer(b Buffer) + BindImageTexture(unit int, texture Texture, access AccessBits, format TextureFormat) + + MemoryBarrier() + DispatchCompute(x, y, z int) + + Release() +} + +type ShaderSources struct { + Name string + GLSL100ES string + GLSL300ES string + GLSL310ES string + GLSL130 string + GLSL150 string + HLSL string + Uniforms UniformsReflection + Inputs []InputLocation + Textures []TextureBinding +} + +type UniformsReflection struct { + Blocks []UniformBlock + Locations []UniformLocation + Size int +} + +type TextureBinding struct { + Name string + Binding int +} + +type UniformBlock struct { + Name string + Binding int +} + +type UniformLocation struct { + Name string + Type DataType + Size int + Offset int +} + +type InputLocation struct { + // For GLSL. + Name string + Location int + // For HLSL. + Semantic string + SemanticIndex int + + Type DataType + Size int +} + +// InputDesc describes a vertex attribute as laid out in a Buffer. +type InputDesc struct { + Type DataType + Size int + + Offset int +} + +// InputLayout is the driver specific representation of the mapping +// between Buffers and shader attributes. +type InputLayout interface { + Release() +} + +type AccessBits uint8 + +type BlendFactor uint8 + +type DrawMode uint8 + +type TextureFilter uint8 +type TextureFormat uint8 + +type BufferBinding uint8 + +type DataType uint8 + +type DepthFunc uint8 + +type Features uint + +type Caps struct { + // BottomLeftOrigin is true if the driver has the origin in the lower left + // corner. The OpenGL driver returns true. + BottomLeftOrigin bool + Features Features + MaxTextureSize int +} + +type Program interface { + Release() + SetStorageBuffer(binding int, buf Buffer) + SetVertexUniforms(buf Buffer) + SetFragmentUniforms(buf Buffer) +} + +type Buffer interface { + Release() + Upload(data []byte) + Download(data []byte) error +} + +type Framebuffer interface { + Invalidate() + Release() + ReadPixels(src image.Rectangle, pixels []byte) error +} + +type Timer interface { + Begin() + End() + Duration() (time.Duration, bool) + Release() +} + +type Texture interface { + Upload(offset, size image.Point, pixels []byte) + Release() +} + +const ( + DepthFuncGreater DepthFunc = iota + DepthFuncGreaterEqual +) + +const ( + DataTypeFloat DataType = iota + DataTypeInt + DataTypeShort +) + +const ( + BufferBindingIndices BufferBinding = 1 << iota + BufferBindingVertices + BufferBindingUniforms + BufferBindingTexture + BufferBindingFramebuffer + BufferBindingShaderStorage +) + +const ( + TextureFormatSRGB TextureFormat = iota + TextureFormatFloat + TextureFormatRGBA8 +) + +const ( + AccessRead AccessBits = 1 + iota + AccessWrite +) + +const ( + FilterNearest TextureFilter = iota + FilterLinear +) + +const ( + FeatureTimers Features = 1 << iota + FeatureFloatRenderTargets + FeatureCompute +) + +const ( + DrawModeTriangleStrip DrawMode = iota + DrawModeTriangles +) + +const ( + BlendFactorOne BlendFactor = iota + BlendFactorOneMinusSrcAlpha + BlendFactorZero + BlendFactorDstColor +) + +var ErrContentLost = errors.New("buffer content lost") + +func (f Features) Has(feats Features) bool { + return f&feats == feats +} + +func DownloadImage(d Device, f Framebuffer, r image.Rectangle) (*image.RGBA, error) { + img := image.NewRGBA(r) + if err := f.ReadPixels(r, img.Pix); err != nil { + return nil, err + } + if d.Caps().BottomLeftOrigin { + // OpenGL origin is in the lower-left corner. Flip the image to + // match. + flipImageY(r.Dx()*4, r.Dy(), img.Pix) + } + return img, nil +} + +func flipImageY(stride, height int, pixels []byte) { + // Flip image in y-direction. OpenGL's origin is in the lower + // left corner. + row := make([]uint8, stride) + for y := 0; y < height/2; y++ { + y1 := height - y - 1 + dest := y1 * stride + src := y * stride + copy(row, pixels[dest:]) + copy(pixels[dest:], pixels[src:src+len(row)]) + copy(pixels[src:], row) + } +} + +func UploadImage(t Texture, offset image.Point, img *image.RGBA) { + var pixels []byte + size := img.Bounds().Size() + if img.Stride != size.X*4 { + panic("unsupported stride") + } + start := img.PixOffset(0, 0) + end := img.PixOffset(size.X, size.Y-1) + pixels = img.Pix[start:end] + t.Upload(offset, size, pixels) +} diff --git a/pkg/gel/gio/gpu/internal/opengl/opengl.go b/pkg/gel/gio/gpu/internal/opengl/opengl.go new file mode 100644 index 0000000..cbeb5af --- /dev/null +++ b/pkg/gel/gio/gpu/internal/opengl/opengl.go @@ -0,0 +1,945 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package opengl + +import ( + "errors" + "fmt" + "image" + "strings" + "time" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/internal/gl" +) + +// Backend implements driver.Device. +type Backend struct { + funcs *gl.Functions + + state glstate + + glver [2]int + gles bool + ubo bool + feats driver.Caps + // floatTriple holds the settings for floating point + // textures. + floatTriple textureTriple + // Single channel alpha textures. + alphaTriple textureTriple + srgbaTriple textureTriple +} + +// State tracking. +type glstate struct { + // nattr is the current number of enabled vertex arrays. + nattr int + prog *gpuProgram + texUnits [4]*gpuTexture + layout *gpuInputLayout + buffer bufferBinding +} + +type bufferBinding struct { + buf *gpuBuffer + offset int + stride int +} + +type gpuTimer struct { + funcs *gl.Functions + obj gl.Query +} + +type gpuTexture struct { + backend *Backend + obj gl.Texture + triple textureTriple + width int + height int +} + +type gpuFramebuffer struct { + backend *Backend + obj gl.Framebuffer + hasDepth bool + depthBuf gl.Renderbuffer + foreign bool +} + +type gpuBuffer struct { + backend *Backend + hasBuffer bool + obj gl.Buffer + typ driver.BufferBinding + size int + immutable bool + version int + // For emulation of uniform buffers. + data []byte +} + +type gpuProgram struct { + backend *Backend + obj gl.Program + nattr int + vertUniforms uniformsTracker + fragUniforms uniformsTracker + storage [storageBindings]*gpuBuffer +} + +type uniformsTracker struct { + locs []uniformLocation + size int + buf *gpuBuffer + version int +} + +type uniformLocation struct { + uniform gl.Uniform + offset int + typ driver.DataType + size int +} + +type gpuInputLayout struct { + inputs []driver.InputLocation + layout []driver.InputDesc +} + +// textureTriple holds the type settings for +// a TexImage2D call. +type textureTriple struct { + internalFormat gl.Enum + format gl.Enum + typ gl.Enum +} + +type Context = gl.Context + +const ( + storageBindings = 32 +) + +func init() { + driver.NewOpenGLDevice = newOpenGLDevice +} + +func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) { + f, err := gl.NewFunctions(api.Context) + if err != nil { + return nil, err + } + exts := strings.Split(f.GetString(gl.EXTENSIONS), " ") + glVer := f.GetString(gl.VERSION) + ver, gles, err := gl.ParseGLVersion(glVer) + if err != nil { + return nil, err + } + floatTriple, ffboErr := floatTripleFor(f, ver, exts) + srgbaTriple, err := srgbaTripleFor(ver, exts) + if err != nil { + return nil, err + } + gles30 := gles && ver[0] >= 3 + gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1)) + gl40 := !gles && ver[0] >= 4 + b := &Backend{ + glver: ver, + gles: gles, + ubo: gles30 || gl40, + funcs: f, + floatTriple: floatTriple, + alphaTriple: alphaTripleFor(ver), + srgbaTriple: srgbaTriple, + } + b.feats.BottomLeftOrigin = true + if ffboErr == nil { + b.feats.Features |= driver.FeatureFloatRenderTargets + } + if gles31 { + b.feats.Features |= driver.FeatureCompute + } + if hasExtension(exts, "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts, "GL_EXT_disjoint_timer_query") { + b.feats.Features |= driver.FeatureTimers + } + b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE) + return b, nil +} + +func (b *Backend) BeginFrame() driver.Framebuffer { + // Assume GL state is reset between frames. + b.state = glstate{} + fboID := gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING)) + return &gpuFramebuffer{backend: b, obj: fboID, foreign: true} +} + +func (b *Backend) EndFrame() { + b.funcs.ActiveTexture(gl.TEXTURE0) +} + +func (b *Backend) Caps() driver.Caps { + return b.feats +} + +func (b *Backend) NewTimer() driver.Timer { + return &gpuTimer{ + funcs: b.funcs, + obj: b.funcs.CreateQuery(), + } +} + +func (b *Backend) IsTimeContinuous() bool { + return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE +} + +func (b *Backend) NewFramebuffer(tex driver.Texture, depthBits int) (driver.Framebuffer, error) { + glErr(b.funcs) + gltex := tex.(*gpuTexture) + fb := b.funcs.CreateFramebuffer() + fbo := &gpuFramebuffer{backend: b, obj: fb} + b.BindFramebuffer(fbo) + if err := glErr(b.funcs); err != nil { + fbo.Release() + return nil, err + } + b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, gltex.obj, 0) + if depthBits > 0 { + size := gl.Enum(gl.DEPTH_COMPONENT16) + switch { + case depthBits > 24: + size = gl.DEPTH_COMPONENT32F + case depthBits > 16: + size = gl.DEPTH_COMPONENT24 + } + depthBuf := b.funcs.CreateRenderbuffer() + b.funcs.BindRenderbuffer(gl.RENDERBUFFER, depthBuf) + b.funcs.RenderbufferStorage(gl.RENDERBUFFER, size, gltex.width, gltex.height) + b.funcs.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuf) + fbo.depthBuf = depthBuf + fbo.hasDepth = true + if err := glErr(b.funcs); err != nil { + fbo.Release() + return nil, err + } + } + if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + fbo.Release() + return nil, fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d", st, b.funcs.GetError()) + } + return fbo, nil +} + +func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, binding driver.BufferBinding) (driver.Texture, error) { + glErr(b.funcs) + tex := &gpuTexture{backend: b, obj: b.funcs.CreateTexture(), width: width, height: height} + switch format { + case driver.TextureFormatFloat: + tex.triple = b.floatTriple + case driver.TextureFormatSRGB: + tex.triple = b.srgbaTriple + case driver.TextureFormatRGBA8: + tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE} + default: + return nil, errors.New("unsupported texture format") + } + b.BindTexture(0, tex) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, toTexFilter(magFilter)) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, toTexFilter(minFilter)) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + if b.gles && b.glver[0] >= 3 { + // Immutable textures are required for BindImageTexture, and can't hurt otherwise. + b.funcs.TexStorage2D(gl.TEXTURE_2D, 1, tex.triple.internalFormat, width, height) + } else { + b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width, height, tex.triple.format, tex.triple.typ) + } + if err := glErr(b.funcs); err != nil { + tex.Release() + return nil, err + } + return tex, nil +} + +func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) { + glErr(b.funcs) + buf := &gpuBuffer{backend: b, typ: typ, size: size} + if typ&driver.BufferBindingUniforms != 0 { + if typ != driver.BufferBindingUniforms { + return nil, errors.New("uniforms buffers cannot be bound as anything else") + } + if !b.ubo { + // GLES 2 doesn't support uniform buffers. + buf.data = make([]byte, size) + } + } + if typ&^driver.BufferBindingUniforms != 0 || b.ubo { + buf.hasBuffer = true + buf.obj = b.funcs.CreateBuffer() + if err := glErr(b.funcs); err != nil { + buf.Release() + return nil, err + } + firstBinding := firstBufferType(typ) + b.funcs.BindBuffer(firstBinding, buf.obj) + b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW) + } + return buf, nil +} + +func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) { + glErr(b.funcs) + obj := b.funcs.CreateBuffer() + buf := &gpuBuffer{backend: b, obj: obj, typ: typ, size: len(data), hasBuffer: true} + firstBinding := firstBufferType(typ) + b.funcs.BindBuffer(firstBinding, buf.obj) + b.funcs.BufferData(firstBinding, len(data), gl.STATIC_DRAW) + buf.Upload(data) + buf.immutable = true + if err := glErr(b.funcs); err != nil { + buf.Release() + return nil, err + } + return buf, nil +} + +func glErr(f *gl.Functions) error { + if st := f.GetError(); st != gl.NO_ERROR { + return fmt.Errorf("glGetError: %#x", st) + } + return nil +} + +func (b *Backend) Release() { +} + +func (b *Backend) MemoryBarrier() { + b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS) +} + +func (b *Backend) DispatchCompute(x, y, z int) { + if p := b.state.prog; p != nil { + for binding, buf := range p.storage { + if buf != nil { + b.funcs.BindBufferBase(gl.SHADER_STORAGE_BUFFER, binding, buf.obj) + } + } + } + b.funcs.DispatchCompute(x, y, z) +} + +func (b *Backend) BindImageTexture(unit int, tex driver.Texture, access driver.AccessBits, f driver.TextureFormat) { + t := tex.(*gpuTexture) + var acc gl.Enum + switch access { + case driver.AccessWrite: + acc = gl.WRITE_ONLY + case driver.AccessRead: + acc = gl.READ_ONLY + default: + panic("unsupported access bits") + } + var format gl.Enum + switch f { + case driver.TextureFormatRGBA8: + format = gl.RGBA8 + default: + panic("unsupported format") + } + b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, format) +} + +func (b *Backend) bindTexture(unit int, t *gpuTexture) { + if b.state.texUnits[unit] != t { + b.funcs.ActiveTexture(gl.TEXTURE0 + gl.Enum(unit)) + b.funcs.BindTexture(gl.TEXTURE_2D, t.obj) + b.state.texUnits[unit] = t + } +} + +func (b *Backend) useProgram(p *gpuProgram) { + if b.state.prog != p { + p.backend.funcs.UseProgram(p.obj) + b.state.prog = p + } +} + +func (b *Backend) enableVertexArrays(n int) { + // Enable needed arrays. + for i := b.state.nattr; i < n; i++ { + b.funcs.EnableVertexAttribArray(gl.Attrib(i)) + } + // Disable extra arrays. + for i := n; i < b.state.nattr; i++ { + b.funcs.DisableVertexAttribArray(gl.Attrib(i)) + } + b.state.nattr = n +} + +func (b *Backend) SetDepthTest(enable bool) { + if enable { + b.funcs.Enable(gl.DEPTH_TEST) + } else { + b.funcs.Disable(gl.DEPTH_TEST) + } +} + +func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { + b.funcs.BlendFunc(toGLBlendFactor(sfactor), toGLBlendFactor(dfactor)) +} + +func toGLBlendFactor(f driver.BlendFactor) gl.Enum { + switch f { + case driver.BlendFactorOne: + return gl.ONE + case driver.BlendFactorOneMinusSrcAlpha: + return gl.ONE_MINUS_SRC_ALPHA + case driver.BlendFactorZero: + return gl.ZERO + case driver.BlendFactorDstColor: + return gl.DST_COLOR + default: + panic("unsupported blend factor") + } +} + +func (b *Backend) DepthMask(mask bool) { + b.funcs.DepthMask(mask) +} + +func (b *Backend) SetBlend(enable bool) { + if enable { + b.funcs.Enable(gl.BLEND) + } else { + b.funcs.Disable(gl.BLEND) + } +} + +func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) { + b.prepareDraw() + // off is in 16-bit indices, but DrawElements take a byte offset. + byteOff := off * 2 + b.funcs.DrawElements(toGLDrawMode(mode), count, gl.UNSIGNED_SHORT, byteOff) +} + +func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) { + b.prepareDraw() + b.funcs.DrawArrays(toGLDrawMode(mode), off, count) +} + +func (b *Backend) prepareDraw() { + nattr := b.state.prog.nattr + b.enableVertexArrays(nattr) + if nattr > 0 { + b.setupVertexArrays() + } + if p := b.state.prog; p != nil { + p.updateUniforms() + } +} + +func toGLDrawMode(mode driver.DrawMode) gl.Enum { + switch mode { + case driver.DrawModeTriangleStrip: + return gl.TRIANGLE_STRIP + case driver.DrawModeTriangles: + return gl.TRIANGLES + default: + panic("unsupported draw mode") + } +} + +func (b *Backend) Viewport(x, y, width, height int) { + b.funcs.Viewport(x, y, width, height) +} + +func (b *Backend) Clear(colR, colG, colB, colA float32) { + b.funcs.ClearColor(colR, colG, colB, colA) + b.funcs.Clear(gl.COLOR_BUFFER_BIT) +} + +func (b *Backend) ClearDepth(d float32) { + b.funcs.ClearDepthf(d) + b.funcs.Clear(gl.DEPTH_BUFFER_BIT) +} + +func (b *Backend) DepthFunc(f driver.DepthFunc) { + var glfunc gl.Enum + switch f { + case driver.DepthFuncGreater: + glfunc = gl.GREATER + case driver.DepthFuncGreaterEqual: + glfunc = gl.GEQUAL + default: + panic("unsupported depth func") + } + b.funcs.DepthFunc(glfunc) +} + +func (b *Backend) NewInputLayout(vs driver.ShaderSources, layout []driver.InputDesc) (driver.InputLayout, error) { + if len(vs.Inputs) != len(layout) { + return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", len(layout), len(vs.Inputs)) + } + for i, inp := range vs.Inputs { + if exp, got := inp.Size, layout[i].Size; exp != got { + return nil, fmt.Errorf("NewInputLayout: data size mismatch for %q: got %d expected %d", inp.Name, got, exp) + } + } + return &gpuInputLayout{ + inputs: vs.Inputs, + layout: layout, + }, nil +} + +func (b *Backend) NewComputeProgram(src driver.ShaderSources) (driver.Program, error) { + p, err := gl.CreateComputeProgram(b.funcs, src.GLSL310ES) + if err != nil { + return nil, fmt.Errorf("%s: %v", src.Name, err) + } + gpuProg := &gpuProgram{ + backend: b, + obj: p, + } + return gpuProg, nil +} + +func (b *Backend) NewProgram(vertShader, fragShader driver.ShaderSources) (driver.Program, error) { + attr := make([]string, len(vertShader.Inputs)) + for _, inp := range vertShader.Inputs { + attr[inp.Location] = inp.Name + } + vsrc, fsrc := vertShader.GLSL100ES, fragShader.GLSL100ES + if b.glver[0] >= 3 { + // OpenGL (ES) 3.0. + switch { + case b.gles: + vsrc, fsrc = vertShader.GLSL300ES, fragShader.GLSL300ES + case b.glver[0] >= 4 || b.glver[1] >= 2: + // OpenGL 3.2 Core only accepts glsl 1.50 or newer. + vsrc, fsrc = vertShader.GLSL150, fragShader.GLSL150 + default: + vsrc, fsrc = vertShader.GLSL130, fragShader.GLSL130 + } + } + p, err := gl.CreateProgram(b.funcs, vsrc, fsrc, attr) + if err != nil { + return nil, err + } + gpuProg := &gpuProgram{ + backend: b, + obj: p, + nattr: len(attr), + } + b.BindProgram(gpuProg) + // Bind texture uniforms. + for _, tex := range vertShader.Textures { + u := b.funcs.GetUniformLocation(p, tex.Name) + if u.Valid() { + b.funcs.Uniform1i(u, tex.Binding) + } + } + for _, tex := range fragShader.Textures { + u := b.funcs.GetUniformLocation(p, tex.Name) + if u.Valid() { + b.funcs.Uniform1i(u, tex.Binding) + } + } + if b.ubo { + for _, block := range vertShader.Uniforms.Blocks { + blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name) + if blockIdx != gl.INVALID_INDEX { + b.funcs.UniformBlockBinding(p, blockIdx, uint(block.Binding)) + } + } + // To match Direct3D 11 with separate vertex and fragment + // shader uniform buffers, offset all fragment blocks to be + // located after the vertex blocks. + off := len(vertShader.Uniforms.Blocks) + for _, block := range fragShader.Uniforms.Blocks { + blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name) + if blockIdx != gl.INVALID_INDEX { + b.funcs.UniformBlockBinding(p, blockIdx, uint(block.Binding+off)) + } + } + } else { + gpuProg.vertUniforms.setup(b.funcs, p, vertShader.Uniforms.Size, vertShader.Uniforms.Locations) + gpuProg.fragUniforms.setup(b.funcs, p, fragShader.Uniforms.Size, fragShader.Uniforms.Locations) + } + return gpuProg, nil +} + +func lookupUniform(funcs *gl.Functions, p gl.Program, loc driver.UniformLocation) uniformLocation { + u := funcs.GetUniformLocation(p, loc.Name) + if !u.Valid() { + panic(fmt.Errorf("uniform %q not found", loc.Name)) + } + return uniformLocation{uniform: u, offset: loc.Offset, typ: loc.Type, size: loc.Size} +} + +func (p *gpuProgram) SetStorageBuffer(binding int, buffer driver.Buffer) { + buf := buffer.(*gpuBuffer) + if buf.typ&driver.BufferBindingShaderStorage == 0 { + panic("not a shader storage buffer") + } + p.storage[binding] = buf +} + +func (p *gpuProgram) SetVertexUniforms(buffer driver.Buffer) { + p.vertUniforms.setBuffer(buffer) +} + +func (p *gpuProgram) SetFragmentUniforms(buffer driver.Buffer) { + p.fragUniforms.setBuffer(buffer) +} + +func (p *gpuProgram) updateUniforms() { + f := p.backend.funcs + if p.backend.ubo { + if b := p.vertUniforms.buf; b != nil { + f.BindBufferBase(gl.UNIFORM_BUFFER, 0, b.obj) + } + if b := p.fragUniforms.buf; b != nil { + f.BindBufferBase(gl.UNIFORM_BUFFER, 1, b.obj) + } + } else { + p.vertUniforms.update(f) + p.fragUniforms.update(f) + } +} + +func (b *Backend) BindProgram(prog driver.Program) { + p := prog.(*gpuProgram) + b.useProgram(p) +} + +func (p *gpuProgram) Release() { + p.backend.funcs.DeleteProgram(p.obj) +} + +func (u *uniformsTracker) setup(funcs *gl.Functions, p gl.Program, uniformSize int, uniforms []driver.UniformLocation) { + u.locs = make([]uniformLocation, len(uniforms)) + for i, uniform := range uniforms { + u.locs[i] = lookupUniform(funcs, p, uniform) + } + u.size = uniformSize +} + +func (u *uniformsTracker) setBuffer(buffer driver.Buffer) { + buf := buffer.(*gpuBuffer) + if buf.typ&driver.BufferBindingUniforms == 0 { + panic("not a uniform buffer") + } + if buf.size < u.size { + panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size, u.size)) + } + u.buf = buf + // Force update. + u.version = buf.version - 1 +} + +func (p *uniformsTracker) update(funcs *gl.Functions) { + b := p.buf + if b == nil || b.version == p.version { + return + } + p.version = b.version + data := b.data + for _, u := range p.locs { + data := data[u.offset:] + switch { + case u.typ == driver.DataTypeFloat && u.size == 1: + data := data[:4] + v := *(*[1]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform1f(u.uniform, v[0]) + case u.typ == driver.DataTypeFloat && u.size == 2: + data := data[:8] + v := *(*[2]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform2f(u.uniform, v[0], v[1]) + case u.typ == driver.DataTypeFloat && u.size == 3: + data := data[:12] + v := *(*[3]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform3f(u.uniform, v[0], v[1], v[2]) + case u.typ == driver.DataTypeFloat && u.size == 4: + data := data[:16] + v := *(*[4]float32)(unsafe.Pointer(&data[0])) + funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3]) + default: + panic("unsupported uniform data type or size") + } + } +} + +func (b *gpuBuffer) Upload(data []byte) { + if b.immutable { + panic("immutable buffer") + } + if len(data) > b.size { + panic("buffer size overflow") + } + b.version++ + copy(b.data, data) + if b.hasBuffer { + firstBinding := firstBufferType(b.typ) + b.backend.funcs.BindBuffer(firstBinding, b.obj) + if len(data) == b.size { + // the iOS GL implementation doesn't recognize when BufferSubData + // clears the entire buffer. Tell it and avoid GPU stalls. + // See also https://github.com/godotengine/godot/issues/23956. + b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW) + } + b.backend.funcs.BufferSubData(firstBinding, 0, data) + } +} + +func (b *gpuBuffer) Download(data []byte) error { + if len(data) > b.size { + panic("buffer size overflow") + } + if !b.hasBuffer { + copy(data, b.data) + return nil + } + firstBinding := firstBufferType(b.typ) + b.backend.funcs.BindBuffer(firstBinding, b.obj) + bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data), gl.MAP_READ_BIT) + if bufferMap == nil { + return fmt.Errorf("MapBufferRange: error %#x", b.backend.funcs.GetError()) + } + copy(data, bufferMap) + if !b.backend.funcs.UnmapBuffer(firstBinding) { + return driver.ErrContentLost + } + return nil +} + +func (b *gpuBuffer) Release() { + if b.hasBuffer { + b.backend.funcs.DeleteBuffer(b.obj) + b.hasBuffer = false + } +} + +func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) { + gbuf := buf.(*gpuBuffer) + if gbuf.typ&driver.BufferBindingVertices == 0 { + panic("not a vertex buffer") + } + b.state.buffer = bufferBinding{buf: gbuf, stride: stride, offset: offset} +} + +func (b *Backend) setupVertexArrays() { + layout := b.state.layout + if layout == nil { + return + } + buf := b.state.buffer + b.funcs.BindBuffer(gl.ARRAY_BUFFER, buf.buf.obj) + for i, inp := range layout.inputs { + l := layout.layout[i] + var gltyp gl.Enum + switch l.Type { + case driver.DataTypeFloat: + gltyp = gl.FLOAT + case driver.DataTypeShort: + gltyp = gl.SHORT + default: + panic("unsupported data type") + } + b.funcs.VertexAttribPointer(gl.Attrib(inp.Location), l.Size, gltyp, false, buf.stride, buf.offset+l.Offset) + } +} + +func (b *Backend) BindIndexBuffer(buf driver.Buffer) { + gbuf := buf.(*gpuBuffer) + if gbuf.typ&driver.BufferBindingIndices == 0 { + panic("not an index buffer") + } + b.funcs.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, gbuf.obj) +} + +func (b *Backend) BlitFramebuffer(dst, src driver.Framebuffer, srect, drect image.Rectangle) { + b.funcs.BindFramebuffer(gl.DRAW_FRAMEBUFFER, dst.(*gpuFramebuffer).obj) + b.funcs.BindFramebuffer(gl.READ_FRAMEBUFFER, src.(*gpuFramebuffer).obj) + b.funcs.BlitFramebuffer( + srect.Min.X, srect.Min.Y, srect.Max.X, srect.Max.Y, + drect.Min.X, drect.Min.Y, drect.Max.X, drect.Max.Y, + gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT|gl.STENCIL_BUFFER_BIT, + gl.NEAREST) +} + +func (f *gpuFramebuffer) ReadPixels(src image.Rectangle, pixels []byte) error { + glErr(f.backend.funcs) + f.backend.BindFramebuffer(f) + if len(pixels) < src.Dx()*src.Dy()*4 { + return errors.New("unexpected RGBA size") + } + f.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, src.Dx(), src.Dy(), gl.RGBA, gl.UNSIGNED_BYTE, pixels) + return glErr(f.backend.funcs) +} + +func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) { + b.funcs.BindFramebuffer(gl.FRAMEBUFFER, fbo.(*gpuFramebuffer).obj) +} + +func (f *gpuFramebuffer) Invalidate() { + f.backend.BindFramebuffer(f) + f.backend.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) +} + +func (f *gpuFramebuffer) Release() { + if f.foreign { + panic("framebuffer not created by NewFramebuffer") + } + f.backend.funcs.DeleteFramebuffer(f.obj) + if f.hasDepth { + f.backend.funcs.DeleteRenderbuffer(f.depthBuf) + } +} + +func toTexFilter(f driver.TextureFilter) int { + switch f { + case driver.FilterNearest: + return gl.NEAREST + case driver.FilterLinear: + return gl.LINEAR + default: + panic("unsupported texture filter") + } +} + +func (b *Backend) BindTexture(unit int, t driver.Texture) { + b.bindTexture(unit, t.(*gpuTexture)) +} + +func (t *gpuTexture) Release() { + t.backend.funcs.DeleteTexture(t.obj) +} + +func (t *gpuTexture) Upload(offset, size image.Point, pixels []byte) { + if min := size.X * size.Y * 4; min > len(pixels) { + panic(fmt.Errorf("size %d larger than data %d", min, len(pixels))) + } + t.backend.BindTexture(0, t) + t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X, size.Y, t.triple.format, t.triple.typ, pixels) +} + +func (t *gpuTimer) Begin() { + t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj) +} + +func (t *gpuTimer) End() { + t.funcs.EndQuery(gl.TIME_ELAPSED_EXT) +} + +func (t *gpuTimer) ready() bool { + return t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT_AVAILABLE) == gl.TRUE +} + +func (t *gpuTimer) Release() { + t.funcs.DeleteQuery(t.obj) +} + +func (t *gpuTimer) Duration() (time.Duration, bool) { + if !t.ready() { + return 0, false + } + nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT) + return time.Duration(nanos), true +} + +func (b *Backend) BindInputLayout(l driver.InputLayout) { + b.state.layout = l.(*gpuInputLayout) +} + +func (l *gpuInputLayout) Release() {} + +// floatTripleFor determines the best texture triple for floating point FBOs. +func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple, error) { + var triples []textureTriple + if ver[0] >= 3 { + triples = append(triples, textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)}) + } + // According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to + // render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not + // report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're + // wrong. + if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts, "GL_EXT_color_buffer_half_float") { + // Try single channel. + triples = append(triples, textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE), gl.Enum(gl.HALF_FLOAT_OES)}) + // Fallback to 4 channels. + triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.HALF_FLOAT_OES)}) + } + if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts, "GL_EXT_color_buffer_float") { + triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)}) + } + tex := f.CreateTexture() + defer f.DeleteTexture(tex) + f.BindTexture(gl.TEXTURE_2D, tex) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + fbo := f.CreateFramebuffer() + defer f.DeleteFramebuffer(fbo) + defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING)) + f.BindFramebuffer(gl.FRAMEBUFFER, fbo) + defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO) + var attempts []string + for _, tt := range triples { + const size = 256 + f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format, tt.typ) + f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0) + st := f.CheckFramebufferStatus(gl.FRAMEBUFFER) + if st == gl.FRAMEBUFFER_COMPLETE { + return tt, nil + } + attempts = append(attempts, fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat, tt.format, tt.typ, st)) + } + return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)", attempts) +} + +func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) { + switch { + case ver[0] >= 3: + return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA), gl.Enum(gl.UNSIGNED_BYTE)}, nil + case hasExtension(exts, "GL_EXT_sRGB"): + return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT), gl.Enum(gl.UNSIGNED_BYTE)}, nil + default: + return textureTriple{}, errors.New("no sRGB texture formats found") + } +} + +func alphaTripleFor(ver [2]int) textureTriple { + intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED) + if ver[0] < 3 { + // R8, RED not supported on OpenGL ES 2.0. + intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE) + } + return textureTriple{intf, f, gl.UNSIGNED_BYTE} +} + +func hasExtension(exts []string, ext string) bool { + for _, e := range exts { + if ext == e { + return true + } + } + return false +} + +func firstBufferType(typ driver.BufferBinding) gl.Enum { + switch { + case typ&driver.BufferBindingIndices != 0: + return gl.ELEMENT_ARRAY_BUFFER + case typ&driver.BufferBindingVertices != 0: + return gl.ARRAY_BUFFER + case typ&driver.BufferBindingUniforms != 0: + return gl.UNIFORM_BUFFER + case typ&driver.BufferBindingShaderStorage != 0: + return gl.SHADER_STORAGE_BUFFER + default: + panic("unsupported buffer type") + } +} diff --git a/pkg/gel/gio/gpu/internal/rendertest/bench_test.go b/pkg/gel/gio/gpu/internal/rendertest/bench_test.go new file mode 100644 index 0000000..add60ca --- /dev/null +++ b/pkg/gel/gio/gpu/internal/rendertest/bench_test.go @@ -0,0 +1,311 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/font/gofont" + "github.com/p9c/p9/pkg/gel/gio/gpu/headless" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/widget/material" +) + +// use some global variables for benchmarking so as to not pollute +// the reported allocs with allocations that we do not want to count. +var ( + c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp) + op1, op2, op3 op.Ops +) + +func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.Theme) { + sz := image.Point{X: 1024, Y: 1200} + w := newWindow(b, sz.X, sz.Y) + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(sz), + } + th := material.NewTheme(gofont.Collection()) + return gtx, w, th +} + +func resetOps(gtx layout.Context) { + gtx.Ops.Reset() + op1.Reset() + op2.Reset() + op3.Reset() +} + +func finishBenchmark(b *testing.B, w *headless.Window) { + b.StopTimer() + if *dumpImages { + img, err := w.Screenshot() + w.Release() + if err != nil { + b.Error(err) + } + if err := saveImage(b.Name()+".png", img); err != nil { + b.Error(err) + } + } +} + +func BenchmarkDrawUICached(b *testing.B) { + // As BenchmarkDraw but the same op.Ops every time that is not reset - this + // should thus allow for maximal cache usage. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ResetTimer() + for i := 0; i < b.N; i++ { + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUI(b *testing.B) { + // BenchmarkDraw is intended as a reasonable overall benchmark for + // the drawing performance of the full drawing pipeline, in each iteration + // resetting the ops and drawing, similar to how a typical UI would function. + // This will allow font caching across frames. + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Save(gtx.Ops) + off := float32(math.Mod(float64(i)/10, 10)) + op.Offset(f32.Pt(off, off)).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Load() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func BenchmarkDrawUITransformed(b *testing.B) { + // Like BenchmarkDraw UI but transformed at every frame + gtx, w, th := setupBenchmark(b) + drawCore(gtx, th) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + + p := op.Save(gtx.Ops) + angle := float32(math.Mod(float64(i)/1000, 0.05)) + a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, angle) + op.Affine(a).Add(gtx.Ops) + + drawCore(gtx, th) + + p.Load() + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000Circles(b *testing.B) { + // Benchmark1000Shapes draws 1000 individual shapes such that no caching between + // shapes will be possible and resets buffers on each operation to prevent caching + // between frames. + gtx, w, _ := setupBenchmark(b) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000Circles(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func Benchmark1000CirclesInstanced(b *testing.B) { + // Like Benchmark1000Circles but will record them and thus allow for caching between + // them. + gtx, w, _ := setupBenchmark(b) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + resetOps(gtx) + draw1000CirclesInstanced(gtx) + w.Frame(gtx.Ops) + } + finishBenchmark(b, w) +} + +func draw1000Circles(gtx layout.Context) { + ops := gtx.Ops + for x := 0; x < 100; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + paint.FillShape(ops, + color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}, + clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Op(ops), + ) + op.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Load() + } +} + +func draw1000CirclesInstanced(gtx layout.Context) { + ops := gtx.Ops + + r := op.Record(ops) + clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Add(ops) + paint.PaintOp{}.Add(ops) + c := r.Stop() + + for x := 0; x < 100; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*10), 0)).Add(ops) + for y := 0; y < 10; y++ { + pi := op.Save(ops) + paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) + c.Add(ops) + pi.Load() + op.Offset(f32.Pt(0, float32(100))).Add(ops) + } + p.Load() + } +} + +func drawCore(gtx layout.Context, th *material.Theme) { + c1 := drawIndividualShapes(gtx, th) + c2 := drawShapeInstances(gtx, th) + c3 := drawText(gtx, th) + + (<-c1).Add(gtx.Ops) + (<-c2).Add(gtx.Ops) + (<-c3).Add(gtx.Ops) +} + +func drawIndividualShapes(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 81 rounded rectangles of different solid colors - each one individually + go func() { + ops := &op1 + c := op.Record(ops) + for x := 0; x < 9; x++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*50), 0)).Add(ops) + for y := 0; y < 9; y++ { + paint.FillShape(ops, + color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}, + clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Op(ops), + ) + op.Offset(f32.Pt(0, float32(50))).Add(ops) + } + p.Load() + } + c1 <- c.Stop() + }() + return c1 +} + +func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 400 textured circle instances, each with individual transform + go func() { + ops := &op2 + co := op.Record(ops) + + r := op.Record(ops) + clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Add(ops) + paint.PaintOp{}.Add(ops) + c := r.Stop() + + squares.Add(ops) + rad := float32(0) + for x := 0; x < 20; x++ { + for y := 0; y < 20; y++ { + p := op.Save(ops) + op.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops) + c.Add(ops) + p.Load() + rad += math.Pi * 2 / 400 + } + } + c2 <- co.Stop() + }() + return c2 +} + +func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp { + // draw 40 lines of text with different transforms. + go func() { + ops := &op3 + c := op.Record(ops) + + txt := material.H6(th, "") + for x := 0; x < 40; x++ { + txt.Text = textRows[x] + p := op.Save(ops) + op.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops) + gtx.Ops = ops + txt.Layout(gtx) + p.Load() + } + c3 <- c.Stop() + }() + return c3 +} + +var textRows = []string{ + "1. I learned from my grandfather, Verus, to use good manners, and to", + "put restraint on anger. 2. In the famous memory of my father I had a", + "pattern of modesty and manliness. 3. Of my mother I learned to be", + "pious and generous; to keep myself not only from evil deeds, but even", + "from evil thoughts; and to live with a simplicity which is far from", + "customary among the rich. 4. I owe it to my great-grandfather that I", + "did not attend public lectures and discussions, but had good and able", + "teachers at home; and I owe him also the knowledge that for things of", + "this nature a man should count no expense too great.", + "5. My tutor taught me not to favour either green or blue at the", + "chariot races, nor, in the contests of gladiators, to be a supporter", + "either of light or heavy armed. He taught me also to endure labour;", + "not to need many things; to serve myself without troubling others; not", + "to intermeddle in the affairs of others, and not easily to listen to", + "slanders against them.", + "6. Of Diognetus I had the lesson not to busy myself about vain things;", + "not to credit the great professions of such as pretend to work", + "wonders, or of sorcerers about their charms, and their expelling of", + "Demons and the like; not to keep quails (for fighting or divination),", + "nor to run after such things; to suffer freedom of speech in others,", + "and to apply myself heartily to philosophy. Him also I must thank for", + "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote", + "dialogues in my youth, and took a liking to the philosopher's pallet", + "and skins, and to the other things which, by the Grecian discipline,", + "belong to that profession.", + "7. To Rusticus I owe my first apprehensions that my nature needed", + "reform and cure; and that I did not fall into the ambition of the", + "common Sophists, either by composing speculative writings or by", + "declaiming harangues of exhortation in public; further, that I never", + "strove to be admired by ostentation of great patience in an ascetic", + "life, or by display of activity and application; that I gave over the", + "study of rhetoric, poetry, and the graces of language; and that I did", + "not pace my house in my senatorial robes, or practise any similar", + "affectation. I observed also the simplicity of style in his letters,", + "particularly in that which he wrote to my mother from Sinuessa. I", + "learned from him to be easily appeased, and to be readily reconciled", + "with those who had displeased me or given cause of offence, so soon as", + "they inclined to make their peace; to read with care; not to rest", + "satisfied with a slight and superficial knowledge; nor quickly to", + "assent to great talkers. I have him to thank that I met with the", +} diff --git a/pkg/gel/gio/gpu/internal/rendertest/clip_test.go b/pkg/gel/gio/gpu/internal/rendertest/clip_test.go new file mode 100644 index 0000000..8d0b34d --- /dev/null +++ b/pkg/gel/gio/gpu/internal/rendertest/clip_test.go @@ -0,0 +1,579 @@ +package rendertest + +import ( + "image" + "math" + "testing" + + "golang.org/x/image/colornames" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +func TestPaintRect(t *testing.T) { + run(t, func(o *op.Ops) { + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, colornames.Red) + r.expect(49, 0, colornames.Red) + r.expect(50, 0, transparent) + r.expect(10, 50, transparent) + }) +} + +func TestPaintClippedRect(t *testing.T) { + run(t, func(o *op.Ops) { + clip.RRect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(24, 35, transparent) + r.expect(25, 35, colornames.Red) + r.expect(50, 0, transparent) + r.expect(10, 50, transparent) + }) +} + +func TestPaintClippedCircle(t *testing.T) { + run(t, func(o *op.Ops) { + r := float32(10) + clip.RRect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r, NE: r}.Add(o) + clip.Rect(image.Rect(0, 0, 30, 50)).Add(o) + paint.Fill(o, red) + }, func(r result) { + r.expect(21, 21, transparent) + r.expect(25, 30, colornames.Red) + r.expect(31, 30, transparent) + }) +} + +func TestPaintArc(t *testing.T) { + run(t, func(o *op.Ops) { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 20)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi) + p.Line(f32.Pt(30, 0)) + p.Line(f32.Pt(0, 25)) + p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi) + p.Line(f32.Pt(0, 25)) + p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi) + p.Line(f32.Pt(-10, 0)) + p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi) + p.Line(f32.Pt(-10, 0)) + p.Line(f32.Pt(0, -10)) + p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi) + p.Line(f32.Pt(0, -10)) + p.Line(f32.Pt(-50, 0)) + p.Close() + clip.Outline{ + Path: p.End(), + }.Op().Add(o) + + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(0, 25, colornames.Red) + r.expect(0, 15, transparent) + }) +} + +func TestPaintAbsolute(t *testing.T) { + run(t, func(o *op.Ops) { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(100, 100)) // offset the initial pen position to test "MoveTo" + + p.MoveTo(f32.Pt(20, 20)) + p.LineTo(f32.Pt(80, 20)) + p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80)) + p.Close() + clip.Outline{ + Path: p.End(), + }.Op().Add(o) + + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(30, 30, colornames.Red) + r.expect(79, 79, transparent) + r.expect(90, 90, transparent) + }) +} + +func TestPaintTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + scale(80.0/512, 80.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(0, 0, colornames.Blue) + r.expect(79, 10, colornames.Green) + r.expect(80, 0, transparent) + r.expect(10, 80, transparent) + }) +} + +func TestTexturedStrokeClipped(t *testing.T) { + run(t, func(o *op.Ops) { + smallSquares.Add(o) + op.Offset(f32.Pt(50, 50)).Add(o) + clip.Stroke{ + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Style: clip.StrokeStyle{ + Width: 10, + }, + }.Op().Add(o) + clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Add(o) + op.Offset(f32.Pt(-10, -10)).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func TestTexturedStroke(t *testing.T) { + run(t, func(o *op.Ops) { + smallSquares.Add(o) + op.Offset(f32.Pt(50, 50)).Add(o) + clip.Stroke{ + Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o), + Style: clip.StrokeStyle{ + Width: 10, + }, + }.Op().Add(o) + op.Offset(f32.Pt(-10, -10)).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func TestPaintClippedTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + clip.RRect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o) + scale(80.0/512, 80.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(40, 40, transparent) + r.expect(25, 35, colornames.Blue) + }) +} + +func TestStrokedPathBevelFlat(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathBevelRound(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathBevelSquare(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.SquareCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathRoundRound(t *testing.T) { + run(t, func(o *op.Ops) { + p := newStrokedPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2.5, + Cap: clip.RoundCap, + Join: clip.RoundJoin, + }, + }.Op().Add(o) + + paint.Fill(o, red) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Red) + }) +} + +func TestStrokedPathFlatMiter(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: 5, + }, + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathFlatMiterInf(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + }) +} + +func TestStrokedPathZeroWidth(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(50, 0)) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + + paint.Fill(o, black) + stk.Load() + } + + { + stk := op.Save(o) + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(30, 0)) + clip.Stroke{ + Path: p.End(), + }.Op().Add(o) // width=0, disable stroke + + paint.Fill(o, red) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(10, 50, colornames.Black) + r.expect(30, 50, colornames.Black) + r.expect(65, 50, transparent) + }) +} + +func TestDashedPathFlatCapEllipse(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newEllipsePath(o) + + var dash clip.Dash + dash.Begin(o) + dash.Dash(5) + dash.Dash(3) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + + paint.Fill( + o, + red, + ) + stk.Load() + } + { + stk := op.Save(o) + p := newEllipsePath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + }, + }.Op().Add(o) + + paint.Fill( + o, + black, + ) + stk.Load() + } + + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(0, 62, colornames.Red) + r.expect(0, 65, colornames.Black) + }) +} + +func TestDashedPathFlatCapZ(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + var dash clip.Dash + dash.Begin(o) + dash.Dash(5) + dash.Dash(3) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + r.expect(46, 12, transparent) + }) +} + +func TestDashedPathFlatCapZNoDash(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + p := newZigZagPath(o) + var dash clip.Dash + dash.Begin(o) + dash.Phase(1) + + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + clip.Stroke{ + Path: newZigZagPath(o), + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, colornames.Red) + r.expect(46, 12, colornames.Red) + }) +} + +func TestDashedPathFlatCapZNoPath(t *testing.T) { + run(t, func(o *op.Ops) { + { + stk := op.Save(o) + var dash clip.Dash + dash.Begin(o) + dash.Dash(0) + clip.Stroke{ + Path: newZigZagPath(o), + Style: clip.StrokeStyle{ + Width: 10, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + Miter: float32(math.Inf(+1)), + }, + Dashes: dash.End(), + }.Op().Add(o) + paint.Fill(o, red) + stk.Load() + } + { + stk := op.Save(o) + p := newZigZagPath(o) + clip.Stroke{ + Path: p, + Style: clip.StrokeStyle{ + Width: 2, + Cap: clip.FlatCap, + Join: clip.BevelJoin, + }, + }.Op().Add(o) + paint.Fill(o, black) + stk.Load() + } + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(40, 10, colornames.Black) + r.expect(40, 12, transparent) + r.expect(46, 12, transparent) + }) +} + +func newStrokedPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(10, 50)) + p.Line(f32.Pt(10, 0)) + p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi) + p.Line(f32.Pt(10, 0)) + p.Line(f32.Pt(10, 10)) + p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi) + p.Line(f32.Pt(-20, 0)) + p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30)) + return p.End() +} + +func newZigZagPath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(40, 10)) + p.Line(f32.Pt(50, 0)) + p.Line(f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50)) + p.Line(f32.Pt(50, 0)) + return p.End() +} + +func newEllipsePath(o *op.Ops) clip.PathSpec { + p := new(clip.Path) + p.Begin(o) + p.Move(f32.Pt(0, 65)) + p.Line(f32.Pt(20, 0)) + p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi) + return p.End() +} diff --git a/pkg/gel/gio/gpu/internal/rendertest/doc.go b/pkg/gel/gio/gpu/internal/rendertest/doc.go new file mode 100644 index 0000000..9f6948e --- /dev/null +++ b/pkg/gel/gio/gpu/internal/rendertest/doc.go @@ -0,0 +1,2 @@ +// Package rendertest is intended for testing of drawing ops only. +package rendertest diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png new file mode 100644 index 0000000..fb50427 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png new file mode 100644 index 0000000..8ff717b Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipOffset.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipOffset.png new file mode 100644 index 0000000..6396fb4 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipOffset.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png new file mode 100644 index 0000000..0fe37e6 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipRotate.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipRotate.png new file mode 100644 index 0000000..e6c15e3 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipRotate.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipScale.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipScale.png new file mode 100644 index 0000000..6396fb4 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestClipScale.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png new file mode 100644 index 0000000..4a92e3c Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png new file mode 100644 index 0000000..79bae38 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png new file mode 100644 index 0000000..12212e9 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png new file mode 100644 index 0000000..d315f0f Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png new file mode 100644 index 0000000..94c160e Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png new file mode 100644 index 0000000..b562f12 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png new file mode 100644 index 0000000..9d416b9 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestLinearGradient.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestLinearGradient.png new file mode 100644 index 0000000..c3c007c Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestLinearGradient.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png new file mode 100644 index 0000000..3ba0734 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png new file mode 100644 index 0000000..fb50427 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png new file mode 100644 index 0000000..e774064 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png new file mode 100644 index 0000000..515a4d2 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png new file mode 100644 index 0000000..87386e8 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png new file mode 100644 index 0000000..dd09760 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintArc.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintArc.png new file mode 100644 index 0000000..f432914 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintArc.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png new file mode 100644 index 0000000..f8fcfbb Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png new file mode 100644 index 0000000..bdf1fce Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png new file mode 100644 index 0000000..c8cf2f6 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png new file mode 100644 index 0000000..c1dd7a0 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png new file mode 100644 index 0000000..ae0e066 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintOffset.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintOffset.png new file mode 100644 index 0000000..82394d5 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintOffset.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintRect.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintRect.png new file mode 100644 index 0000000..f942601 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintRect.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintRotate.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintRotate.png new file mode 100644 index 0000000..fe15d7d Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintRotate.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintShear.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintShear.png new file mode 100644 index 0000000..6d1a4c9 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintShear.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintTexture.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintTexture.png new file mode 100644 index 0000000..9120231 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestPaintTexture.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png new file mode 100644 index 0000000..da201dc Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestReuseStencil.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestReuseStencil.png new file mode 100644 index 0000000..349db1f Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestReuseStencil.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png new file mode 100644 index 0000000..56c3182 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestRotateTexture.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestRotateTexture.png new file mode 100644 index 0000000..e56c972 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestRotateTexture.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png new file mode 100644 index 0000000..9d442f5 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png new file mode 100644 index 0000000..a37235c Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png new file mode 100644 index 0000000..8d2919d Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png new file mode 100644 index 0000000..ae6472a Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png new file mode 100644 index 0000000..d315f0f Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png new file mode 100644 index 0000000..8ef5a94 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png new file mode 100644 index 0000000..0fc6fe8 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png new file mode 100644 index 0000000..637c932 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png new file mode 100644 index 0000000..637c932 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestTransformMacro.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTransformMacro.png new file mode 100644 index 0000000..a9cce29 Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTransformMacro.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/refs/TestTransformOrder.png b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTransformOrder.png new file mode 100644 index 0000000..720ca3c Binary files /dev/null and b/pkg/gel/gio/gpu/internal/rendertest/refs/TestTransformOrder.png differ diff --git a/pkg/gel/gio/gpu/internal/rendertest/render_test.go b/pkg/gel/gio/gpu/internal/rendertest/render_test.go new file mode 100644 index 0000000..83e6404 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/rendertest/render_test.go @@ -0,0 +1,351 @@ +package rendertest + +import ( + "image" + "image/color" + "math" + "testing" + + "golang.org/x/image/colornames" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +func TestTransformMacro(t *testing.T) { + // testcase resulting from original bug when rendering layout.Stacked + + // Build clip-path. + c := constSqPath() + + run(t, func(o *op.Ops) { + + // render the first Stacked item + m1 := op.Record(o) + dr := image.Rect(0, 0, 128, 50) + paint.FillShape(o, black, clip.Rect(dr).Op()) + c1 := m1.Stop() + + // Render the second stacked item + m2 := op.Record(o) + paint.ColorOp{Color: red}.Add(o) + // Simulate a draw text call + stack := op.Save(o) + op.Offset(f32.Pt(0, 10)).Add(o) + + // Apply the clip-path. + c.Add(o) + + paint.PaintOp{}.Add(o) + stack.Load() + + c2 := m2.Stop() + + // Call each of them in a transform + s1 := op.Save(o) + op.Offset(f32.Pt(0, 0)).Add(o) + c1.Add(o) + s1.Load() + s2 := op.Save(o) + op.Offset(f32.Pt(0, 0)).Add(o) + c2.Add(o) + s2.Load() + }, func(r result) { + r.expect(5, 15, colornames.Red) + r.expect(15, 15, colornames.Black) + r.expect(11, 51, transparent) + }) +} + +func TestRepeatedPaintsZ(t *testing.T) { + run(t, func(o *op.Ops) { + // Draw a rectangle + paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op()) + + builder := clip.Path{} + builder.Begin(o) + builder.Move(f32.Pt(0, 0)) + builder.Line(f32.Pt(10, 0)) + builder.Line(f32.Pt(0, 10)) + builder.Line(f32.Pt(-10, 0)) + builder.Line(f32.Pt(0, -10)) + p := builder.End() + clip.Outline{ + Path: p, + }.Op().Add(o) + paint.Fill(o, red) + }, func(r result) { + r.expect(5, 5, colornames.Red) + r.expect(11, 15, colornames.Black) + r.expect(11, 51, transparent) + }) +} + +func TestNoClipFromPaint(t *testing.T) { + // ensure that a paint operation does not pollute the state + // by leaving any clip paths in place. + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op()) + a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4) + op.Affine(a).Add(o) + + paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(1, 1, colornames.Black) + r.expect(20, 20, colornames.Black) + r.expect(49, 49, colornames.Black) + r.expect(51, 51, transparent) + }) +} + +func TestDeferredPaint(t *testing.T) { + run(t, func(o *op.Ops) { + state := op.Save(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + + op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o) + m := op.Record(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + paintMacro := m.Stop() + op.Defer(o, paintMacro) + + state.Load() + op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) + clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) + paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + }) +} + +func constSqPath() op.CallOp { + innerOps := new(op.Ops) + m := op.Record(innerOps) + builder := clip.Path{} + builder.Begin(innerOps) + builder.Move(f32.Pt(0, 0)) + builder.Line(f32.Pt(10, 0)) + builder.Line(f32.Pt(0, 10)) + builder.Line(f32.Pt(-10, 0)) + builder.Line(f32.Pt(0, -10)) + p := builder.End() + clip.Outline{Path: p}.Op().Add(innerOps) + return m.Stop() +} + +func constSqCirc() op.CallOp { + innerOps := new(op.Ops) + m := op.Record(innerOps) + clip.RRect{Rect: f32.Rect(0, 0, 40, 40), + NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps) + return m.Stop() +} + +func drawChild(ops *op.Ops, text op.CallOp) op.CallOp { + r1 := op.Record(ops) + text.Add(ops) + paint.PaintOp{}.Add(ops) + return r1.Stop() +} + +func TestReuseStencil(t *testing.T) { + txt := constSqPath() + run(t, func(ops *op.Ops) { + c1 := drawChild(ops, txt) + c2 := drawChild(ops, txt) + + // lay out the children + stack1 := op.Save(ops) + c1.Add(ops) + stack1.Load() + + stack2 := op.Save(ops) + op.Offset(f32.Pt(0, 50)).Add(ops) + c2.Add(ops) + stack2.Load() + }, func(r result) { + r.expect(5, 5, colornames.Black) + r.expect(5, 55, colornames.Black) + }) +} + +func TestBuildOffscreen(t *testing.T) { + // Check that something we in one frame build outside the screen + // still is rendered correctly if moved into the screen in a later + // frame. + + txt := constSqCirc() + draw := func(off float32, o *op.Ops) { + s := op.Save(o) + op.Offset(f32.Pt(0, off)).Add(o) + txt.Add(o) + paint.PaintOp{}.Add(o) + s.Load() + } + + multiRun(t, + frame( + func(ops *op.Ops) { + draw(-100, ops) + }, func(r result) { + r.expect(5, 5, transparent) + r.expect(20, 20, transparent) + }), + frame( + func(ops *op.Ops) { + draw(0, ops) + }, func(r result) { + r.expect(2, 2, transparent) + r.expect(20, 20, colornames.Black) + r.expect(38, 38, transparent) + })) +} + +func TestNegativeOverlaps(t *testing.T) { + run(t, func(ops *op.Ops) { + clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops) + clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops) + paint.PaintOp{}.Add(ops) + }, func(r result) { + r.expect(60, 60, transparent) + r.expect(60, 110, transparent) + r.expect(60, 120, transparent) + r.expect(60, 122, transparent) + }) +} + +func TestDepthOverlap(t *testing.T) { + run(t, func(ops *op.Ops) { + stack := op.Save(ops) + paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op()) + stack.Load() + + stack = op.Save(ops) + paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op()) + stack.Load() + }, func(r result) { + r.expect(96, 32, colornames.Red) + r.expect(32, 96, colornames.Green) + r.expect(32, 32, colornames.Green) + }) +} + +type Gradient struct { + From, To color.NRGBA +} + +var gradients = []Gradient{ + {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, + {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, +} + +func TestLinearGradient(t *testing.T) { + t.Skip("linear gradients don't support transformations") + + const gradienth = 8 + // 0.5 offset from ends to ensure that the center of the pixel + // aligns with gradient from and to colors. + pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth) + samples := []int{0, 12, 32, 64, 96, 115, 127} + + run(t, func(ops *op.Ops) { + gr := f32.Rect(0, 0, 128, gradienth) + for _, g := range gradients { + paint.LinearGradientOp{ + Stop1: f32.Pt(gr.Min.X, gr.Min.Y), + Color1: g.From, + Stop2: f32.Pt(gr.Max.X, gr.Min.Y), + Color2: g.To, + }.Add(ops) + st := op.Save(ops) + clip.RRect{Rect: gr}.Add(ops) + op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops) + scale(pixelAligned.Dx()/128, 1).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + gr = gr.Add(f32.Pt(0, gradienth)) + } + }, func(r result) { + gr := pixelAligned + for _, g := range gradients { + from := f32color.LinearFromSRGB(g.From) + to := f32color.LinearFromSRGB(g.To) + for _, p := range samples { + exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1)) + r.expect(p, int(gr.Min.Y+gradienth/2), f32color.NRGBAToRGBA(exp.SRGB())) + } + gr = gr.Add(f32.Pt(0, gradienth)) + } + }) +} + +func TestLinearGradientAngled(t *testing.T) { + run(t, func(ops *op.Ops) { + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: black, + Stop2: f32.Pt(0, 0), + Color2: red, + }.Add(ops) + st := op.Save(ops) + clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: white, + Stop2: f32.Pt(128, 0), + Color2: green, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: black, + Stop2: f32.Pt(128, 128), + Color2: blue, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + + paint.LinearGradientOp{ + Stop1: f32.Pt(64, 64), + Color1: white, + Stop2: f32.Pt(0, 128), + Color2: magenta, + }.Add(ops) + st = op.Save(ops) + clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops) + paint.PaintOp{}.Add(ops) + st.Load() + }, func(r result) {}) +} + +// lerp calculates linear interpolation with color b and p. +func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { + return f32color.RGBA{ + R: a.R*(1-p) + b.R*p, + G: a.G*(1-p) + b.G*p, + B: a.B*(1-p) + b.B*p, + A: a.A*(1-p) + b.A*p, + } +} diff --git a/pkg/gel/gio/gpu/internal/rendertest/transform_test.go b/pkg/gel/gio/gpu/internal/rendertest/transform_test.go new file mode 100644 index 0000000..cb40339 --- /dev/null +++ b/pkg/gel/gio/gpu/internal/rendertest/transform_test.go @@ -0,0 +1,200 @@ +package rendertest + +import ( + "image" + "math" + "testing" + + "golang.org/x/image/colornames" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +func TestPaintOffset(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(10, 20)).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(59, 30, colornames.Red) + r.expect(60, 30, transparent) + r.expect(10, 70, transparent) + }) +} + +func TestPaintRotate(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op()) + }, func(r result) { + r.expect(40, 40, colornames.Red) + r.expect(50, 19, colornames.Red) + r.expect(59, 19, transparent) + r.expect(21, 21, transparent) + }) +} + +func TestPaintShear(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0) + op.Affine(a).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op()) + }, func(r result) { + r.expect(10, 30, transparent) + }) +} + +func TestClipPaintOffset(t *testing.T) { + run(t, func(o *op.Ops) { + clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o) + op.Offset(f32.Pt(20, 20)).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(19, 19, transparent) + r.expect(20, 20, colornames.Red) + r.expect(30, 30, transparent) + }) +} + +func TestClipOffset(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(20, 20)).Add(o) + clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(29, 29, transparent) + r.expect(30, 30, colornames.Red) + r.expect(49, 49, colornames.Red) + r.expect(50, 50, transparent) + }) +} + +func TestClipScale(t *testing.T) { + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, 10)) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op()) + }, func(r result) { + r.expect(19+10, 19+10, transparent) + r.expect(20+10, 20+10, colornames.Red) + r.expect(39+10, 39+10, colornames.Red) + r.expect(40+10, 40+10, transparent) + }) +} + +func TestClipRotate(t *testing.T) { + run(t, func(o *op.Ops) { + op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o) + clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op()) + }, func(r result) { + r.expect(39, 39, transparent) + r.expect(41, 41, colornames.Red) + r.expect(50, 50, transparent) + }) +} + +func TestOffsetTexture(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(15, 15)).Add(o) + squares.Add(o) + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(14, 20, transparent) + r.expect(66, 20, transparent) + r.expect(16, 64, colornames.Green) + r.expect(64, 16, colornames.Green) + }) +} + +func TestOffsetScaleTexture(t *testing.T) { + run(t, func(o *op.Ops) { + op.Offset(f32.Pt(15, 15)).Add(o) + squares.Add(o) + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o) + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(114, 64, colornames.Blue) + r.expect(116, 64, transparent) + }) +} + +func TestRotateTexture(t *testing.T) { + run(t, func(o *op.Ops) { + defer op.Save(o).Load() + squares.Add(o) + a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40), math.Pi/4) + op.Affine(a).Add(o) + scale(20.0/512, 20.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(40, 40-12, colornames.Blue) + r.expect(40+12, 40, colornames.Green) + }) +} + +func TestRotateClipTexture(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o) + op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) + scale(60.0/512, 60.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(0, 0, transparent) + r.expect(37, 39, colornames.Green) + r.expect(36, 39, colornames.Green) + r.expect(35, 39, colornames.Green) + r.expect(34, 39, colornames.Green) + r.expect(33, 39, colornames.Green) + }) +} + +func TestComplicatedTransform(t *testing.T) { + run(t, func(o *op.Ops) { + squares.Add(o) + + clip.RRect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, NE: 50}.Add(o) + + a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0) + op.Affine(a).Add(o) + clip.RRect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o) + + scale(50.0/512, 50.0/512).Add(o) + paint.PaintOp{}.Add(o) + }, func(r result) { + r.expect(20, 5, transparent) + }) +} + +func TestTransformOrder(t *testing.T) { + // check the ordering of operations bot in affine and in gpu stack. + run(t, func(o *op.Ops) { + a := f32.Affine2D{}.Offset(f32.Pt(64, 64)) + op.Affine(a).Add(o) + + b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8)) + op.Affine(b).Add(o) + + c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, f32.Pt(0.5, 0.5)) + op.Affine(c).Add(o) + paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op()) + }, func(r result) { + // centered and with radius 40 + r.expect(64-41, 64, transparent) + r.expect(64-39, 64, colornames.Red) + r.expect(64+39, 64, colornames.Red) + r.expect(64+41, 64, transparent) + }) +} diff --git a/pkg/gel/gio/gpu/internal/rendertest/util_test.go b/pkg/gel/gio/gpu/internal/rendertest/util_test.go new file mode 100644 index 0000000..5df027c --- /dev/null +++ b/pkg/gel/gio/gpu/internal/rendertest/util_test.go @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package rendertest + +import ( + "bytes" + "flag" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "path/filepath" + "strconv" + "testing" + + "golang.org/x/image/colornames" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/gpu/headless" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +var ( + dumpImages = flag.Bool("saveimages", false, "save test images") + squares paint.ImageOp + smallSquares paint.ImageOp +) + +var ( + red = f32color.RGBAToNRGBA(colornames.Red) + green = f32color.RGBAToNRGBA(colornames.Green) + blue = f32color.RGBAToNRGBA(colornames.Blue) + magenta = f32color.RGBAToNRGBA(colornames.Magenta) + black = f32color.RGBAToNRGBA(colornames.Black) + white = f32color.RGBAToNRGBA(colornames.White) + transparent = color.RGBA{} +) + +func init() { + squares = buildSquares(512) + smallSquares = buildSquares(50) +} + +func buildSquares(size int) paint.ImageOp { + sub := size / 4 + im := image.NewNRGBA(image.Rect(0, 0, size, size)) + c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue) + for r := 0; r < 4; r++ { + for c := 0; c < 4; c++ { + c1, c2 = c2, c1 + draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, image.Point{}, draw.Over) + } + c1, c2 = c2, c1 + } + return paint.NewImageOp(im) +} + +func drawImage(t *testing.T, size int, ops *op.Ops, draw func(o *op.Ops)) (im *image.RGBA, err error) { + sz := image.Point{X: size, Y: size} + w := newWindow(t, sz.X, sz.Y) + draw(ops) + if err := w.Frame(ops); err != nil { + return nil, err + } + return w.Screenshot() +} + +func run(t *testing.T, f func(o *op.Ops), c func(r result)) { + // draw a few times and check that it is correct each time, to + // ensure any caching effects still generate the correct images. + var img *image.RGBA + var err error + ops := new(op.Ops) + for i := 0; i < 3; i++ { + ops.Reset() + img, err = drawImage(t, 128, ops, f) + if err != nil { + t.Error("error rendering:", err) + return + } + // check for a reference image and make sure we are identical. + if !verifyRef(t, img, 0) { + name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i) + if err := saveImage(name, img); err != nil { + t.Error(err) + } + } + c(result{t: t, img: img}) + } + + if *dumpImages { + if err := saveImage(t.Name()+".png", img); err != nil { + t.Error(err) + } + } +} + +func frame(f func(o *op.Ops), c func(r result)) frameT { + return frameT{f: f, c: c} +} + +type frameT struct { + f func(o *op.Ops) + c func(r result) +} + +// multiRun is used to run test cases over multiple frames, typically +// to test caching interactions. +func multiRun(t *testing.T, frames ...frameT) { + // draw a few times and check that it is correct each time, to + // ensure any caching effects still generate the correct images. + var img *image.RGBA + var err error + sz := image.Point{X: 128, Y: 128} + w := newWindow(t, sz.X, sz.Y) + ops := new(op.Ops) + for i := range frames { + ops.Reset() + frames[i].f(ops) + if err := w.Frame(ops); err != nil { + t.Errorf("rendering failed: %v", err) + continue + } + img, err = w.Screenshot() + if err != nil { + t.Errorf("screenshot failed: %v", err) + continue + } + // Check for a reference image and make sure they are identical. + ok := verifyRef(t, img, i) + if frames[i].c != nil { + frames[i].c(result{t: t, img: img}) + } + if *dumpImages || !ok { + name := t.Name() + ".png" + if i != 0 { + name = t.Name() + "_" + strconv.Itoa(i) + ".png" + } + if err := saveImage(name, img); err != nil { + t.Error(err) + } + } + } + +} + +func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) { + // ensure identical to ref data + path := filepath.Join("refs", t.Name()+".png") + if frame != 0 { + path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png") + } + b, err := ioutil.ReadFile(path) + if err != nil { + t.Error("could not open ref:", err) + return + } + r, err := png.Decode(bytes.NewReader(b)) + if err != nil { + t.Error("could not decode ref:", err) + return + } + if img.Bounds() != r.Bounds() { + t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds()) + return false + } + var ref *image.RGBA + switch r := r.(type) { + case *image.RGBA: + ref = r + case *image.NRGBA: + ref = image.NewRGBA(r.Bounds()) + bnd := r.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y))) + } + } + default: + t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA", r) + } + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + exp := ref.RGBAAt(x, y) + got := img.RGBAAt(x, y) + if !colorsClose(exp, got) { + t.Error("not equal to ref at", x, y, " ", got, exp) + return false + } + } + } + return true +} + +func colorsClose(c1, c2 color.RGBA) bool { + const delta = 0.01 // magic value obtained from experimentation. + return yiqEqApprox(c1, c2, delta) +} + +// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space, +// as described in: +// +// Measuring perceived color difference using YIQ NTSC +// transmission color space in mobile applications. +// Yuriy Kotsarenko, Fernando Ramos. +// +// An electronic version is available at: +// +// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf +func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool { + const max = 35215.0 // difference between 2 maximally different pixels. + + var ( + r1 = float64(c1.R) + g1 = float64(c1.G) + b1 = float64(c1.B) + + r2 = float64(c2.R) + g2 = float64(c2.G) + b2 = float64(c2.B) + + y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223 + i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189 + q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694 + + y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223 + i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189 + q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694 + + y = y1 - y2 + i = i1 - i2 + q = q1 - q2 + + diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q + ) + return diff <= max*d2 +} + +func (r result) expect(x, y int, col color.RGBA) { + r.t.Helper() + if r.img == nil { + return + } + c := r.img.RGBAAt(x, y) + if !colorsClose(c, col) { + r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c) + } +} + +type result struct { + t *testing.T + img *image.RGBA +} + +func saveImage(file string, img *image.RGBA) error { + // Only NRGBA images are losslessly encoded by png.Encode. + nrgba := image.NewNRGBA(img.Bounds()) + bnd := img.Bounds() + for x := bnd.Min.X; x < bnd.Max.X; x++ { + for y := bnd.Min.Y; y < bnd.Max.Y; y++ { + nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y))) + } + } + var buf bytes.Buffer + if err := png.Encode(&buf, nrgba); err != nil { + return err + } + return ioutil.WriteFile(file, buf.Bytes(), 0666) +} + +func newWindow(t testing.TB, width, height int) *headless.Window { + w, err := headless.NewWindow(width, height) + if err != nil { + t.Skipf("failed to create headless window, skipping: %v", err) + } + t.Cleanup(w.Release) + return w +} + +func scale(sx, sy float32) op.TransformOp { + return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy))) +} diff --git a/pkg/gel/gio/gpu/pack.go b/pkg/gel/gio/gpu/pack.go new file mode 100644 index 0000000..c4dbaad --- /dev/null +++ b/pkg/gel/gio/gpu/pack.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "image" +) + +// packer packs a set of many smaller rectangles into +// much fewer larger atlases. +type packer struct { + maxDim int + spaces []image.Rectangle + + sizes []image.Point + pos image.Point +} + +type placement struct { + Idx int + Pos image.Point +} + +// add adds the given rectangle to the atlases and +// return the allocated position. +func (p *packer) add(s image.Point) (placement, bool) { + if place, ok := p.tryAdd(s); ok { + return place, true + } + p.newPage() + return p.tryAdd(s) +} + +func (p *packer) clear() { + p.sizes = p.sizes[:0] + p.spaces = p.spaces[:0] +} + +func (p *packer) newPage() { + p.pos = image.Point{} + p.sizes = append(p.sizes, image.Point{}) + p.spaces = p.spaces[:0] + p.spaces = append(p.spaces, image.Rectangle{ + Max: image.Point{X: p.maxDim, Y: p.maxDim}, + }) +} + +func (p *packer) tryAdd(s image.Point) (placement, bool) { + // Go backwards to prioritize smaller spaces first. + for i := len(p.spaces) - 1; i >= 0; i-- { + space := p.spaces[i] + rightSpace := space.Dx() - s.X + bottomSpace := space.Dy() - s.Y + if rightSpace >= 0 && bottomSpace >= 0 { + // Remove space. + p.spaces[i] = p.spaces[len(p.spaces)-1] + p.spaces = p.spaces[:len(p.spaces)-1] + // Put s in the top left corner and add the (at most) + // two smaller spaces. + pos := space.Min + if bottomSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, + Max: image.Point{X: space.Max.X, Y: space.Max.Y}, + }) + } + if rightSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X + s.X, Y: pos.Y}, + Max: image.Point{X: space.Max.X, Y: pos.Y + s.Y}, + }) + } + idx := len(p.sizes) - 1 + size := &p.sizes[idx] + if x := pos.X + s.X; x > size.X { + size.X = x + } + if y := pos.Y + s.Y; y > size.Y { + size.Y = y + } + return placement{Idx: idx, Pos: pos}, true + } + } + return placement{}, false +} diff --git a/pkg/gel/gio/gpu/path.go b/pkg/gel/gio/gpu/path.go new file mode 100644 index 0000000..13d58a4 --- /dev/null +++ b/pkg/gel/gio/gpu/path.go @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +// GPU accelerated path drawing using the algorithms from +// Pathfinder (https://github.com/servo/pathfinder). + +import ( + "encoding/binary" + "image" + "math" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" +) + +type pather struct { + ctx driver.Device + + viewport image.Point + + stenciler *stenciler + coverer *coverer +} + +type coverer struct { + ctx driver.Device + prog [3]*program + texUniforms *coverTexUniforms + colUniforms *coverColUniforms + linearGradientUniforms *coverLinearGradientUniforms + layout driver.InputLayout +} + +type coverTexUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } +} + +type coverColUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } + frag struct { + colorUniforms + } +} + +type coverLinearGradientUniforms struct { + vert struct { + coverUniforms + _ [12]byte // Padding to multiple of 16. + } + frag struct { + gradientUniforms + } +} + +type coverUniforms struct { + transform [4]float32 + uvCoverTransform [4]float32 + uvTransformR1 [4]float32 + uvTransformR2 [4]float32 + z float32 +} + +type stenciler struct { + ctx driver.Device + prog struct { + prog *program + uniforms *stencilUniforms + layout driver.InputLayout + } + iprog struct { + prog *program + uniforms *intersectUniforms + layout driver.InputLayout + } + fbos fboSet + intersections fboSet + indexBuf driver.Buffer +} + +type stencilUniforms struct { + vert struct { + transform [4]float32 + pathOffset [2]float32 + _ [8]byte // Padding to multiple of 16. + } +} + +type intersectUniforms struct { + vert struct { + uvTransform [4]float32 + subUVTransform [4]float32 + } +} + +type fboSet struct { + fbos []stencilFBO +} + +type stencilFBO struct { + size image.Point + fbo driver.Framebuffer + tex driver.Texture +} + +type pathData struct { + ncurves int + data driver.Buffer +} + +// vertex data suitable for passing to vertex programs. +type vertex struct { + // Corner encodes the corner: +0.5 for south, +.25 for east. + Corner float32 + MaxY float32 + FromX, FromY float32 + CtrlX, CtrlY float32 + ToX, ToY float32 +} + +func (v vertex) encode(d []byte, maxy uint32) { + bo := binary.LittleEndian + bo.PutUint32(d[0:], math.Float32bits(v.Corner)) + bo.PutUint32(d[4:], maxy) + bo.PutUint32(d[8:], math.Float32bits(v.FromX)) + bo.PutUint32(d[12:], math.Float32bits(v.FromY)) + bo.PutUint32(d[16:], math.Float32bits(v.CtrlX)) + bo.PutUint32(d[20:], math.Float32bits(v.CtrlY)) + bo.PutUint32(d[24:], math.Float32bits(v.ToX)) + bo.PutUint32(d[28:], math.Float32bits(v.ToY)) +} + +const ( + // Number of path quads per draw batch. + pathBatchSize = 10000 + // Size of a vertex as sent to gpu + vertStride = 8 * 4 +) + +func newPather(ctx driver.Device) *pather { + return &pather{ + ctx: ctx, + stenciler: newStenciler(ctx), + coverer: newCoverer(ctx), + } +} + +func newCoverer(ctx driver.Device) *coverer { + c := &coverer{ + ctx: ctx, + } + c.colUniforms = new(coverColUniforms) + c.texUniforms = new(coverTexUniforms) + c.linearGradientUniforms = new(coverLinearGradientUniforms) + prog, layout, err := createColorPrograms(ctx, shader_cover_vert, shader_cover_frag, + [3]interface{}{&c.colUniforms.vert, &c.linearGradientUniforms.vert, &c.texUniforms.vert}, + [3]interface{}{&c.colUniforms.frag, &c.linearGradientUniforms.frag, nil}, + ) + if err != nil { + panic(err) + } + c.prog = prog + c.layout = layout + return c +} + +func newStenciler(ctx driver.Device) *stenciler { + // Allocate a suitably large index buffer for drawing paths. + indices := make([]uint16, pathBatchSize*6) + for i := 0; i < pathBatchSize; i++ { + i := uint16(i) + indices[i*6+0] = i*4 + 0 + indices[i*6+1] = i*4 + 1 + indices[i*6+2] = i*4 + 2 + indices[i*6+3] = i*4 + 2 + indices[i*6+4] = i*4 + 1 + indices[i*6+5] = i*4 + 3 + } + indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices, byteslice.Slice(indices)) + if err != nil { + panic(err) + } + progLayout, err := ctx.NewInputLayout(shader_stencil_vert, []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))}, + {Type: driver.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))}, + {Type: driver.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))}, + {Type: driver.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))}, + {Type: driver.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))}, + }) + if err != nil { + panic(err) + } + iprogLayout, err := ctx.NewInputLayout(shader_intersect_vert, []driver.InputDesc{ + {Type: driver.DataTypeFloat, Size: 2, Offset: 0}, + {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2}, + }) + if err != nil { + panic(err) + } + st := &stenciler{ + ctx: ctx, + indexBuf: indexBuf, + } + prog, err := ctx.NewProgram(shader_stencil_vert, shader_stencil_frag) + if err != nil { + panic(err) + } + st.prog.uniforms = new(stencilUniforms) + vertUniforms := newUniformBuffer(ctx, &st.prog.uniforms.vert) + st.prog.prog = newProgram(prog, vertUniforms, nil) + st.prog.layout = progLayout + iprog, err := ctx.NewProgram(shader_intersect_vert, shader_intersect_frag) + if err != nil { + panic(err) + } + st.iprog.uniforms = new(intersectUniforms) + vertUniforms = newUniformBuffer(ctx, &st.iprog.uniforms.vert) + st.iprog.prog = newProgram(iprog, vertUniforms, nil) + st.iprog.layout = iprogLayout + return st +} + +func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) { + // Add fbos. + for i := len(s.fbos); i < len(sizes); i++ { + s.fbos = append(s.fbos, stencilFBO{}) + } + // Resize fbos. + for i, sz := range sizes { + f := &s.fbos[i] + // Resizing or recreating FBOs can introduce rendering stalls. + // Avoid if the space waste is not too high. + resize := sz.X > f.size.X || sz.Y > f.size.Y + waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) + resize = resize || waste > 1.2 + if resize { + if f.fbo != nil { + f.fbo.Release() + f.tex.Release() + } + tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest, + driver.BufferBindingTexture|driver.BufferBindingFramebuffer) + if err != nil { + panic(err) + } + fbo, err := ctx.NewFramebuffer(tex, 0) + if err != nil { + panic(err) + } + f.size = sz + f.tex = tex + f.fbo = fbo + } + } + // Delete extra fbos. + s.delete(ctx, len(sizes)) +} + +func (s *fboSet) invalidate(ctx driver.Device) { + for _, f := range s.fbos { + f.fbo.Invalidate() + } +} + +func (s *fboSet) delete(ctx driver.Device, idx int) { + for i := idx; i < len(s.fbos); i++ { + f := s.fbos[i] + f.fbo.Release() + f.tex.Release() + } + s.fbos = s.fbos[:idx] +} + +func (s *stenciler) release() { + s.fbos.delete(s.ctx, 0) + s.prog.layout.Release() + s.prog.prog.Release() + s.iprog.layout.Release() + s.iprog.prog.Release() + s.indexBuf.Release() +} + +func (p *pather) release() { + p.stenciler.release() + p.coverer.release() +} + +func (c *coverer) release() { + for _, p := range c.prog { + p.Release() + } + c.layout.Release() +} + +func buildPath(ctx driver.Device, p []byte) pathData { + buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p) + if err != nil { + panic(err) + } + return pathData{ + ncurves: len(p) / vertStride, + data: buf, + } +} + +func (p pathData) release() { + p.data.Release() +} + +func (p *pather) begin(sizes []image.Point) { + p.stenciler.begin(sizes) +} + +func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) { + p.stenciler.stencilPath(bounds, offset, uv, data) +} + +func (s *stenciler) beginIntersect(sizes []image.Point) { + s.ctx.BlendFunc(driver.BlendFactorDstColor, driver.BlendFactorZero) + // 8 bit coverage is enough, but OpenGL ES only supports single channel + // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if + // no floating point support is available. + s.intersections.resize(s.ctx, sizes) + s.ctx.BindProgram(s.iprog.prog.prog) +} + +func (s *stenciler) invalidateFBO() { + s.intersections.invalidate(s.ctx) + s.fbos.invalidate(s.ctx) +} + +func (s *stenciler) cover(idx int) stencilFBO { + return s.fbos.fbos[idx] +} + +func (s *stenciler) begin(sizes []image.Point) { + s.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOne) + s.fbos.resize(s.ctx, sizes) + s.ctx.BindProgram(s.prog.prog.prog) + s.ctx.BindInputLayout(s.prog.layout) + s.ctx.BindIndexBuffer(s.indexBuf) +} + +func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) { + s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) + // Transform UI coordinates to OpenGL coordinates. + texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} + scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y} + orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} + s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y} + s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y} + s.prog.prog.UploadUniforms() + // Draw in batches that fit in uint16 indices. + start := 0 + nquads := data.ncurves / 4 + for start < nquads { + batch := nquads - start + if max := pathBatchSize; batch > max { + batch = max + } + off := vertStride * start * 4 + s.ctx.BindVertexBuffer(data.data, vertStride, off) + s.ctx.DrawElements(driver.DrawModeTriangles, 0, batch*6) + start += batch + } +} + +func (p *pather) cover(z float32, mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { + p.coverer.cover(z, mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff) +} + +func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { + p := c.prog[mat] + c.ctx.BindProgram(p.prog) + var uniforms *coverUniforms + switch mat { + case materialColor: + c.colUniforms.frag.color = col + uniforms = &c.colUniforms.vert.coverUniforms + case materialLinearGradient: + c.linearGradientUniforms.frag.color1 = col1 + c.linearGradientUniforms.frag.color2 = col2 + + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + c.linearGradientUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0} + c.linearGradientUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &c.linearGradientUniforms.vert.coverUniforms + case materialTexture: + t1, t2, t3, t4, t5, t6 := uvTrans.Elems() + c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0} + c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0} + uniforms = &c.texUniforms.vert.coverUniforms + } + uniforms.z = z + uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} + uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} + p.UploadUniforms() + c.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4) +} + +func init() { + // Check that struct vertex has the expected size and + // that it contains no padding. + if unsafe.Sizeof(*(*vertex)(nil)) != vertStride { + panic("unexpected struct size") + } +} diff --git a/pkg/gel/gio/gpu/shaders.go b/pkg/gel/gio/gpu/shaders.go new file mode 100644 index 0000000..b8e6fea --- /dev/null +++ b/pkg/gel/gio/gpu/shaders.go @@ -0,0 +1,6694 @@ +// Code generated by build.go. DO NOT EDIT. + +package gpu + +import "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" + +var ( + shader_backdrop_comp = driver.ShaderSources{ + Name: "backdrop.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _77; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _191; + +shared uint sh_row_width[128]; +shared Alloc sh_row_alloc[128]; +shared uint sh_row_count[128]; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _77.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _77.memory[offset] = val; +} + +void main() +{ + if (_77.mem_error != 0u) + { + return; + } + uint th_ix = gl_LocalInvocationID.x; + uint element_ix = gl_GlobalInvocationID.x; + AnnotatedRef ref = AnnotatedRef(_191.conf.anno_alloc.offset + (element_ix * 32u)); + uint row_count = 0u; + if (element_ix < _191.conf.n_elements) + { + Alloc param; + param.offset = _191.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + AnnotatedTag tag = Annotated_tag(param, param_1); + switch (tag.tag) + { + case 2u: + case 3u: + case 1u: + { + uint param_2 = tag.flags; + if (fill_mode_from_flags(param_2) != 0u) + { + break; + } + PathRef path_ref = PathRef(_191.conf.tile_alloc.offset + (element_ix * 12u)); + Alloc param_3; + param_3.offset = _191.conf.tile_alloc.offset; + PathRef param_4 = path_ref; + Path path = Path_read(param_3, param_4); + sh_row_width[th_ix] = path.bbox.z - path.bbox.x; + row_count = path.bbox.w - path.bbox.y; + bool _267 = row_count == 1u; + bool _273; + if (_267) + { + _273 = path.bbox.y > 0u; + } + else + { + _273 = _267; + } + if (_273) + { + row_count = 0u; + } + uint param_5 = path.tiles.offset; + uint param_6 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_5, param_6); + sh_row_alloc[th_ix] = path_alloc; + break; + } + } + } + sh_row_count[th_ix] = row_count; + for (uint i = 0u; i < 7u; i++) + { + barrier(); + if (th_ix >= uint(1 << int(i))) + { + row_count += sh_row_count[th_ix - uint(1 << int(i))]; + } + barrier(); + sh_row_count[th_ix] = row_count; + } + barrier(); + uint total_rows = sh_row_count[127]; + uint _395; + for (uint row = th_ix; row < total_rows; row += 128u) + { + uint el_ix = 0u; + for (uint i_1 = 0u; i_1 < 7u; i_1++) + { + uint probe = el_ix + uint(64 >> int(i_1)); + if (row >= sh_row_count[probe - 1u]) + { + el_ix = probe; + } + } + uint width = sh_row_width[el_ix]; + if (width > 0u) + { + Alloc tiles_alloc = sh_row_alloc[el_ix]; + if (el_ix > 0u) + { + _395 = sh_row_count[el_ix - 1u]; + } + else + { + _395 = 0u; + } + uint seq_ix = row - _395; + uint tile_el_ix = ((tiles_alloc.offset >> uint(2)) + 1u) + ((seq_ix * 2u) * width); + Alloc param_7 = tiles_alloc; + uint param_8 = tile_el_ix; + uint sum = read_mem(param_7, param_8); + for (uint x = 1u; x < width; x++) + { + tile_el_ix += 2u; + Alloc param_9 = tiles_alloc; + uint param_10 = tile_el_ix; + sum += read_mem(param_9, param_10); + Alloc param_11 = tiles_alloc; + uint param_12 = tile_el_ix; + uint param_13 = sum; + write_mem(param_11, param_12, param_13); + } + } + } +} + +`, + } + shader_binning_comp = driver.ShaderSources{ + Name: "binning.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct BinInstanceRef +{ + uint offset; +}; + +struct BinInstance +{ + uint element_ix; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _88; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _254; + +shared uint bitmaps[4][128]; +shared bool sh_alloc_failed; +shared uint count[4][128]; +shared Alloc sh_chunk_alloc[128]; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _88.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + AnnoEndClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u); + return AnnoEndClip_read(param, param_1); +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _94 = atomicAdd(_88.mem_offset, size); + uint offset = _94; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_88.memory.length())) * 4)) + { + r.failed = true; + uint _115 = atomicMax(_88.mem_error, 1u); + return r; + } + return r; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _88.memory[offset] = val; +} + +void BinInstance_write(Alloc a, BinInstanceRef ref, BinInstance s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.element_ix; + write_mem(param, param_1, param_2); +} + +void main() +{ + if (_88.mem_error != 0u) + { + return; + } + uint my_n_elements = _254.conf.n_elements; + uint my_partition = gl_WorkGroupID.x; + for (uint i = 0u; i < 4u; i++) + { + bitmaps[i][gl_LocalInvocationID.x] = 0u; + } + if (gl_LocalInvocationID.x == 0u) + { + sh_alloc_failed = false; + } + barrier(); + uint element_ix = (my_partition * 128u) + gl_LocalInvocationID.x; + AnnotatedRef ref = AnnotatedRef(_254.conf.anno_alloc.offset + (element_ix * 32u)); + uint tag = 0u; + if (element_ix < my_n_elements) + { + Alloc param; + param.offset = _254.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + tag = Annotated_tag(param, param_1).tag; + } + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + Alloc param_2; + param_2.offset = _254.conf.anno_alloc.offset; + AnnotatedRef param_3 = ref; + AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3); + x0 = int(floor(clip.bbox.x * 0.001953125)); + y0 = int(floor(clip.bbox.y * 0.00390625)); + x1 = int(ceil(clip.bbox.z * 0.001953125)); + y1 = int(ceil(clip.bbox.w * 0.00390625)); + break; + } + } + uint width_in_bins = ((_254.conf.width_in_tiles + 16u) - 1u) / 16u; + uint height_in_bins = ((_254.conf.height_in_tiles + 8u) - 1u) / 8u; + x0 = clamp(x0, 0, int(width_in_bins)); + x1 = clamp(x1, x0, int(width_in_bins)); + y0 = clamp(y0, 0, int(height_in_bins)); + y1 = clamp(y1, y0, int(height_in_bins)); + if (x0 == x1) + { + y1 = y0; + } + int x = x0; + int y = y0; + uint my_slice = gl_LocalInvocationID.x / 32u; + uint my_mask = uint(1 << int(gl_LocalInvocationID.x & 31u)); + while (y < y1) + { + uint _438 = atomicOr(bitmaps[my_slice][(uint(y) * width_in_bins) + uint(x)], my_mask); + x++; + if (x == x1) + { + x = x0; + y++; + } + } + barrier(); + uint element_count = 0u; + for (uint i_1 = 0u; i_1 < 4u; i_1++) + { + element_count += uint(bitCount(bitmaps[i_1][gl_LocalInvocationID.x])); + count[i_1][gl_LocalInvocationID.x] = element_count; + } + uint param_4 = 0u; + uint param_5 = 0u; + Alloc chunk_alloc = new_alloc(param_4, param_5); + if (element_count != 0u) + { + uint param_6 = element_count * 4u; + MallocResult _487 = malloc(param_6); + MallocResult chunk = _487; + chunk_alloc = chunk.alloc; + sh_chunk_alloc[gl_LocalInvocationID.x] = chunk_alloc; + if (chunk.failed) + { + sh_alloc_failed = true; + } + } + uint out_ix = (_254.conf.bin_alloc.offset >> uint(2)) + (((my_partition * 128u) + gl_LocalInvocationID.x) * 2u); + Alloc param_7; + param_7.offset = _254.conf.bin_alloc.offset; + uint param_8 = out_ix; + uint param_9 = element_count; + write_mem(param_7, param_8, param_9); + Alloc param_10; + param_10.offset = _254.conf.bin_alloc.offset; + uint param_11 = out_ix + 1u; + uint param_12 = chunk_alloc.offset; + write_mem(param_10, param_11, param_12); + barrier(); + if (sh_alloc_failed) + { + return; + } + x = x0; + y = y0; + while (y < y1) + { + uint bin_ix = (uint(y) * width_in_bins) + uint(x); + uint out_mask = bitmaps[my_slice][bin_ix]; + if ((out_mask & my_mask) != 0u) + { + uint idx = uint(bitCount(out_mask & (my_mask - 1u))); + if (my_slice > 0u) + { + idx += count[my_slice - 1u][bin_ix]; + } + Alloc out_alloc = sh_chunk_alloc[bin_ix]; + uint out_offset = out_alloc.offset + (idx * 4u); + Alloc param_13 = out_alloc; + BinInstanceRef param_14 = BinInstanceRef(out_offset); + BinInstance param_15 = BinInstance(element_ix); + BinInstance_write(param_13, param_14, param_15); + } + x++; + if (x == x1) + { + x = x0; + y++; + } + } +} + +`, + } + shader_blit_frag = [...]driver.ShaderSources{ + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}}, + Size: 16, + }, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = _color.color; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Color +{ + vec4 color; +} _color; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Color +{ + vec4 color; +} _color; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; +} + +`, + HLSL: "DXBC,\xc1\x9c\x85P\xbc\xab\x8a.\x9e\b\xdd\xf7\xd2\x18\xa2\x01\x00\x00\x00t\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x84\x00\x00\x00\xcc\x00\x00\x00H\x01\x00\x00\f\x02\x00\x00@\x02\x00\x00Aon9D\x00\x00\x00D\x00\x00\x00\x00\x02\xff\xff\x14\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR@\x00\x00\x00@\x00\x00\x00\x10\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\x06\xf2 \x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xbc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x94\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Color\x00\xab\xab<\x00\x00\x00\x01\x00\x00\x00\\\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x84\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); +} + +`, + HLSL: "DXBCdZ\xb9AA\xb2\xa5-Σc\xb9\xdc\xfd]\xae\x01\x00\x00\x00P\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00t\x01\x00\x00\xf0\x01\x00\x00\xe8\x02\x00\x00\x1c\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xff\\\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x01\x00\x00\x02\x00\x00\x18\x80\x00\x00\x00\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\x0f\x80\x00\x00\xff\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa0\x00\x00\x00@\x00\x00\x00(\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00b\x10\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc5\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Gradient\x00\xab\xab\xab<\x00\x00\x00\x02\x00\x00\x00`\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x01\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "blit.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = texture2D(tex, vUV); +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; +in vec2 vUV; + +void main() +{ + fragColor = texture(tex, vUV); +} + +`, + HLSL: "DXBC\xb7?\x1d\xb1\x80̀\xa3W\t\xfbZ\x9fV\xd6\xda\x01\x00\x00\x00\x94\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xa4\x00\x00\x00\x10\x01\x00\x00\x8c\x01\x00\x00,\x02\x00\x00`\x02\x00\x00Aon9d\x00\x00\x00d\x00\x00\x00\x00\x02\xff\xff<\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRd\x00\x00\x00@\x00\x00\x00\x19\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00E\x00\x00\t\xf2 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + } + shader_blit_vert = driver.ShaderSources{ + Name: "blit.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 48}}, + Size: 52, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +attribute vec2 pos; +varying vec2 vUV; +attribute vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +layout(location = 0) in vec2 pos; +out vec2 vUV; +layout(location = 1) in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec2 p = (pos * _block.transform.xy) + _block.transform.zw; + vec4 param = vec4(p, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; +} + +`, + HLSL: "DXBC\x80\xa7\xa0\x9e\xbb\xa1\xa3\x1b\x85\xac\xb6\xe9\xfb\xe6W\x03\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00$\x01\x00\x00T\x02\x00\x00\xd0\x02\x00\x00$\x04\x00\x00p\x04\x00\x00Aon9\xe4\x00\x00\x00\xe4\x00\x00\x00\x00\x02\xfe\xff\xb0\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x04\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x05\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Đ\x05\x00Р\x05\x00Š\b\x00\x00\x03\x00\x00\x01\xe0\x02\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x02\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\a\x80\x05\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x04\x00\x00\xa0\x00\x00d\x80\x00\x00$\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x01\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x04\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x10\x00\x00\b\x12 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x10\x00\x00\b\" \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x03\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFL\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00$\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x04\x00\x00\x00\\\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xf5\x00\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\n\x01\x00\x000\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_coarse_comp = driver.ShaderSources{ + Name: "coarse.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoImageRef +{ + uint offset; +}; + +struct AnnoImage +{ + vec4 bbox; + float linewidth; + uint index; + ivec2 offset; +}; + +struct AnnoColorRef +{ + uint offset; +}; + +struct AnnoColor +{ + vec4 bbox; + float linewidth; + uint rgba_color; +}; + +struct AnnoBeginClipRef +{ + uint offset; +}; + +struct AnnoBeginClip +{ + vec4 bbox; + float linewidth; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct BinInstanceRef +{ + uint offset; +}; + +struct BinInstance +{ + uint element_ix; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct Tile +{ + TileSegRef tile; + int backdrop; +}; + +struct CmdStrokeRef +{ + uint offset; +}; + +struct CmdStroke +{ + uint tile_ref; + float half_width; +}; + +struct CmdFillRef +{ + uint offset; +}; + +struct CmdFill +{ + uint tile_ref; + int backdrop; +}; + +struct CmdColorRef +{ + uint offset; +}; + +struct CmdColor +{ + uint rgba_color; +}; + +struct CmdImageRef +{ + uint offset; +}; + +struct CmdImage +{ + uint index; + ivec2 offset; +}; + +struct CmdJumpRef +{ + uint offset; +}; + +struct CmdJump +{ + uint new_ref; +}; + +struct CmdRef +{ + uint offset; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _276; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _1066; + +shared uint sh_bitmaps[4][128]; +shared Alloc sh_part_elements[128]; +shared uint sh_part_count[128]; +shared uint sh_elements[128]; +shared uint sh_tile_stride[128]; +shared uint sh_tile_width[128]; +shared uint sh_tile_x0[128]; +shared uint sh_tile_y0[128]; +shared uint sh_tile_base[128]; +shared uint sh_tile_count[128]; + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _276.memory[offset]; + return v; +} + +BinInstanceRef BinInstance_index(BinInstanceRef ref, uint index) +{ + return BinInstanceRef(ref.offset + (index * 4u)); +} + +BinInstance BinInstance_read(Alloc a, BinInstanceRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + BinInstance s; + s.element_ix = raw0; + return s; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +void write_tile_alloc(uint el_ix, Alloc a) +{ +} + +Alloc read_tile_alloc(uint el_ix) +{ + uint param = 0u; + uint param_1 = uint(int(uint(_276.memory.length())) * 4); + return new_alloc(param, param_1); +} + +Tile Tile_read(Alloc a, TileRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Tile s; + s.tile = TileSegRef(raw0); + s.backdrop = int(raw1); + return s; +} + +AnnoColor AnnoColor_read(Alloc a, AnnoColorRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + AnnoColor s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + s.rgba_color = raw5; + return s; +} + +AnnoColor Annotated_Color_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoColorRef param_1 = AnnoColorRef(ref.offset + 4u); + return AnnoColor_read(param, param_1); +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _282 = atomicAdd(_276.mem_offset, size); + uint offset = _282; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_276.memory.length())) * 4)) + { + r.failed = true; + uint _303 = atomicMax(_276.mem_error, 1u); + return r; + } + return r; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _276.memory[offset] = val; +} + +void CmdJump_write(Alloc a, CmdJumpRef ref, CmdJump s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.new_ref; + write_mem(param, param_1, param_2); +} + +void Cmd_Jump_write(Alloc a, CmdRef ref, CmdJump s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 9u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdJumpRef param_4 = CmdJumpRef(ref.offset + 4u); + CmdJump param_5 = s; + CmdJump_write(param_3, param_4, param_5); +} + +bool alloc_cmd(inout Alloc cmd_alloc, inout CmdRef cmd_ref, inout uint cmd_limit) +{ + if (cmd_ref.offset < cmd_limit) + { + return true; + } + uint param = 1024u; + MallocResult _968 = malloc(param); + MallocResult new_cmd = _968; + if (new_cmd.failed) + { + return false; + } + CmdJump jump = CmdJump(new_cmd.alloc.offset); + Alloc param_1 = cmd_alloc; + CmdRef param_2 = cmd_ref; + CmdJump param_3 = jump; + Cmd_Jump_write(param_1, param_2, param_3); + cmd_alloc = new_cmd.alloc; + cmd_ref = CmdRef(cmd_alloc.offset); + cmd_limit = (cmd_alloc.offset + 1024u) - 36u; + return true; +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +void CmdFill_write(Alloc a, CmdFillRef ref, CmdFill s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.tile_ref; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = uint(s.backdrop); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Fill_write(Alloc a, CmdRef ref, CmdFill s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdFillRef param_4 = CmdFillRef(ref.offset + 4u); + CmdFill param_5 = s; + CmdFill_write(param_3, param_4, param_5); +} + +void Cmd_Solid_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 3u; + write_mem(param, param_1, param_2); +} + +void CmdStroke_write(Alloc a, CmdStrokeRef ref, CmdStroke s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.tile_ref; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.half_width); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Stroke_write(Alloc a, CmdRef ref, CmdStroke s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 2u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdStrokeRef param_4 = CmdStrokeRef(ref.offset + 4u); + CmdStroke param_5 = s; + CmdStroke_write(param_3, param_4, param_5); +} + +void write_fill(Alloc alloc, inout CmdRef cmd_ref, uint flags, Tile tile, float linewidth) +{ + uint param = flags; + if (fill_mode_from_flags(param) == 0u) + { + if (tile.tile.offset != 0u) + { + CmdFill cmd_fill = CmdFill(tile.tile.offset, tile.backdrop); + Alloc param_1 = alloc; + CmdRef param_2 = cmd_ref; + CmdFill param_3 = cmd_fill; + Cmd_Fill_write(param_1, param_2, param_3); + cmd_ref.offset += 12u; + } + else + { + Alloc param_4 = alloc; + CmdRef param_5 = cmd_ref; + Cmd_Solid_write(param_4, param_5); + cmd_ref.offset += 4u; + } + } + else + { + CmdStroke cmd_stroke = CmdStroke(tile.tile.offset, 0.5 * linewidth); + Alloc param_6 = alloc; + CmdRef param_7 = cmd_ref; + CmdStroke param_8 = cmd_stroke; + Cmd_Stroke_write(param_6, param_7, param_8); + cmd_ref.offset += 12u; + } +} + +void CmdColor_write(Alloc a, CmdColorRef ref, CmdColor s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.rgba_color; + write_mem(param, param_1, param_2); +} + +void Cmd_Color_write(Alloc a, CmdRef ref, CmdColor s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 5u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdColorRef param_4 = CmdColorRef(ref.offset + 4u); + CmdColor param_5 = s; + CmdColor_write(param_3, param_4, param_5); +} + +AnnoImage AnnoImage_read(Alloc a, AnnoImageRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 6u; + uint raw6 = read_mem(param_12, param_13); + AnnoImage s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + s.index = raw5; + s.offset = ivec2(int(raw6 << uint(16)) >> 16, int(raw6) >> 16); + return s; +} + +AnnoImage Annotated_Image_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoImageRef param_1 = AnnoImageRef(ref.offset + 4u); + return AnnoImage_read(param, param_1); +} + +void CmdImage_write(Alloc a, CmdImageRef ref, CmdImage s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.index; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16)); + write_mem(param_3, param_4, param_5); +} + +void Cmd_Image_write(Alloc a, CmdRef ref, CmdImage s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 6u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + CmdImageRef param_4 = CmdImageRef(ref.offset + 4u); + CmdImage param_5 = s; + CmdImage_write(param_3, param_4, param_5); +} + +AnnoBeginClip AnnoBeginClip_read(Alloc a, AnnoBeginClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + AnnoBeginClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.linewidth = uintBitsToFloat(raw4); + return s; +} + +AnnoBeginClip Annotated_BeginClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoBeginClipRef param_1 = AnnoBeginClipRef(ref.offset + 4u); + return AnnoBeginClip_read(param, param_1); +} + +void Cmd_BeginClip_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 7u; + write_mem(param, param_1, param_2); +} + +void Cmd_EndClip_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 8u; + write_mem(param, param_1, param_2); +} + +void Cmd_End_write(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 0u; + write_mem(param, param_1, param_2); +} + +void alloc_write(Alloc a, uint offset, Alloc alloc) +{ + Alloc param = a; + uint param_1 = offset >> uint(2); + uint param_2 = alloc.offset; + write_mem(param, param_1, param_2); +} + +void main() +{ + if (_276.mem_error != 0u) + { + return; + } + uint width_in_bins = ((_1066.conf.width_in_tiles + 16u) - 1u) / 16u; + uint bin_ix = (width_in_bins * gl_WorkGroupID.y) + gl_WorkGroupID.x; + uint partition_ix = 0u; + uint n_partitions = ((_1066.conf.n_elements + 128u) - 1u) / 128u; + uint th_ix = gl_LocalInvocationID.x; + uint bin_tile_x = 16u * gl_WorkGroupID.x; + uint bin_tile_y = 8u * gl_WorkGroupID.y; + uint tile_x = gl_LocalInvocationID.x % 16u; + uint tile_y = gl_LocalInvocationID.x / 16u; + uint this_tile_ix = (((bin_tile_y + tile_y) * _1066.conf.width_in_tiles) + bin_tile_x) + tile_x; + Alloc param; + param.offset = _1066.conf.ptcl_alloc.offset; + uint param_1 = this_tile_ix * 1024u; + uint param_2 = 1024u; + Alloc cmd_alloc = slice_mem(param, param_1, param_2); + CmdRef cmd_ref = CmdRef(cmd_alloc.offset); + uint cmd_limit = (cmd_ref.offset + 1024u) - 36u; + uint clip_depth = 0u; + uint clip_zero_depth = 0u; + uint clip_one_mask = 0u; + uint rd_ix = 0u; + uint wr_ix = 0u; + uint part_start_ix = 0u; + uint ready_ix = 0u; + Alloc param_3 = cmd_alloc; + uint param_4 = 0u; + uint param_5 = 8u; + Alloc scratch_alloc = slice_mem(param_3, param_4, param_5); + cmd_ref.offset += 8u; + uint num_begin_slots = 0u; + uint begin_slot = 0u; + Alloc param_6; + Alloc param_8; + uint _1354; + uint element_ix; + AnnotatedRef ref; + Alloc param_16; + Alloc param_18; + uint tile_count; + Alloc param_24; + uint _1667; + bool include_tile; + Alloc param_29; + Tile tile_1; + Alloc param_34; + Alloc param_50; + Alloc param_66; + while (true) + { + for (uint i = 0u; i < 4u; i++) + { + sh_bitmaps[i][th_ix] = 0u; + } + bool _1406; + for (;;) + { + if ((ready_ix == wr_ix) && (partition_ix < n_partitions)) + { + part_start_ix = ready_ix; + uint count = 0u; + bool _1204 = th_ix < 128u; + bool _1212; + if (_1204) + { + _1212 = (partition_ix + th_ix) < n_partitions; + } + else + { + _1212 = _1204; + } + if (_1212) + { + uint in_ix = (_1066.conf.bin_alloc.offset >> uint(2)) + ((((partition_ix + th_ix) * 128u) + bin_ix) * 2u); + param_6.offset = _1066.conf.bin_alloc.offset; + uint param_7 = in_ix; + count = read_mem(param_6, param_7); + param_8.offset = _1066.conf.bin_alloc.offset; + uint param_9 = in_ix + 1u; + uint offset = read_mem(param_8, param_9); + uint param_10 = offset; + uint param_11 = count * 4u; + sh_part_elements[th_ix] = new_alloc(param_10, param_11); + } + for (uint i_1 = 0u; i_1 < 7u; i_1++) + { + if (th_ix < 128u) + { + sh_part_count[th_ix] = count; + } + barrier(); + if (th_ix < 128u) + { + if (th_ix >= uint(1 << int(i_1))) + { + count += sh_part_count[th_ix - uint(1 << int(i_1))]; + } + } + barrier(); + } + if (th_ix < 128u) + { + sh_part_count[th_ix] = part_start_ix + count; + } + barrier(); + ready_ix = sh_part_count[127]; + partition_ix += 128u; + } + uint ix = rd_ix + th_ix; + if ((ix >= wr_ix) && (ix < ready_ix)) + { + uint part_ix = 0u; + for (uint i_2 = 0u; i_2 < 7u; i_2++) + { + uint probe = part_ix + uint(64 >> int(i_2)); + if (ix >= sh_part_count[probe - 1u]) + { + part_ix = probe; + } + } + if (part_ix > 0u) + { + _1354 = sh_part_count[part_ix - 1u]; + } + else + { + _1354 = part_start_ix; + } + ix -= _1354; + Alloc bin_alloc = sh_part_elements[part_ix]; + BinInstanceRef inst_ref = BinInstanceRef(bin_alloc.offset); + BinInstanceRef param_12 = inst_ref; + uint param_13 = ix; + Alloc param_14 = bin_alloc; + BinInstanceRef param_15 = BinInstance_index(param_12, param_13); + BinInstance inst = BinInstance_read(param_14, param_15); + sh_elements[th_ix] = inst.element_ix; + } + barrier(); + wr_ix = min((rd_ix + 128u), ready_ix); + bool _1396 = (wr_ix - rd_ix) < 128u; + if (_1396) + { + _1406 = (wr_ix < ready_ix) || (partition_ix < n_partitions); + } + else + { + _1406 = _1396; + } + if (_1406) + { + continue; + } + else + { + break; + } + } + uint tag = 0u; + if ((th_ix + rd_ix) < wr_ix) + { + element_ix = sh_elements[th_ix]; + ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix * 32u)); + param_16.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_17 = ref; + tag = Annotated_tag(param_16, param_17).tag; + } + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + uint path_ix = element_ix; + param_18.offset = _1066.conf.tile_alloc.offset; + PathRef param_19 = PathRef(_1066.conf.tile_alloc.offset + (path_ix * 12u)); + Path path = Path_read(param_18, param_19); + uint stride = path.bbox.z - path.bbox.x; + sh_tile_stride[th_ix] = stride; + int dx = int(path.bbox.x) - int(bin_tile_x); + int dy = int(path.bbox.y) - int(bin_tile_y); + int x0 = clamp(dx, 0, 16); + int y0 = clamp(dy, 0, 8); + int x1 = clamp(int(path.bbox.z) - int(bin_tile_x), 0, 16); + int y1 = clamp(int(path.bbox.w) - int(bin_tile_y), 0, 8); + sh_tile_width[th_ix] = uint(x1 - x0); + sh_tile_x0[th_ix] = uint(x0); + sh_tile_y0[th_ix] = uint(y0); + tile_count = uint(x1 - x0) * uint(y1 - y0); + uint base = path.tiles.offset - (((uint(dy) * stride) + uint(dx)) * 8u); + sh_tile_base[th_ix] = base; + uint param_20 = path.tiles.offset; + uint param_21 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_20, param_21); + uint param_22 = th_ix; + Alloc param_23 = path_alloc; + write_tile_alloc(param_22, param_23); + break; + } + default: + { + tile_count = 0u; + break; + } + } + sh_tile_count[th_ix] = tile_count; + for (uint i_3 = 0u; i_3 < 7u; i_3++) + { + barrier(); + if (th_ix >= uint(1 << int(i_3))) + { + tile_count += sh_tile_count[th_ix - uint(1 << int(i_3))]; + } + barrier(); + sh_tile_count[th_ix] = tile_count; + } + barrier(); + uint total_tile_count = sh_tile_count[127]; + for (uint ix_1 = th_ix; ix_1 < total_tile_count; ix_1 += 128u) + { + uint el_ix = 0u; + for (uint i_4 = 0u; i_4 < 7u; i_4++) + { + uint probe_1 = el_ix + uint(64 >> int(i_4)); + if (ix_1 >= sh_tile_count[probe_1 - 1u]) + { + el_ix = probe_1; + } + } + AnnotatedRef ref_1 = AnnotatedRef(_1066.conf.anno_alloc.offset + (sh_elements[el_ix] * 32u)); + param_24.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_25 = ref_1; + uint tag_1 = Annotated_tag(param_24, param_25).tag; + if (el_ix > 0u) + { + _1667 = sh_tile_count[el_ix - 1u]; + } + else + { + _1667 = 0u; + } + uint seq_ix = ix_1 - _1667; + uint width = sh_tile_width[el_ix]; + uint x = sh_tile_x0[el_ix] + (seq_ix % width); + uint y = sh_tile_y0[el_ix] + (seq_ix / width); + if ((tag_1 == 3u) || (tag_1 == 4u)) + { + include_tile = true; + } + else + { + uint param_26 = el_ix; + Alloc param_27 = read_tile_alloc(param_26); + TileRef param_28 = TileRef(sh_tile_base[el_ix] + (((sh_tile_stride[el_ix] * y) + x) * 8u)); + Tile tile = Tile_read(param_27, param_28); + bool _1728 = tile.tile.offset != 0u; + bool _1735; + if (!_1728) + { + _1735 = tile.backdrop != 0; + } + else + { + _1735 = _1728; + } + include_tile = _1735; + } + if (include_tile) + { + uint el_slice = el_ix / 32u; + uint el_mask = uint(1 << int(el_ix & 31u)); + uint _1755 = atomicOr(sh_bitmaps[el_slice][(y * 16u) + x], el_mask); + } + } + barrier(); + uint slice_ix = 0u; + uint bitmap = sh_bitmaps[0][th_ix]; + while (true) + { + if (bitmap == 0u) + { + slice_ix++; + if (slice_ix == 4u) + { + break; + } + bitmap = sh_bitmaps[slice_ix][th_ix]; + if (bitmap == 0u) + { + continue; + } + } + uint element_ref_ix = (slice_ix * 32u) + uint(findLSB(bitmap)); + uint element_ix_1 = sh_elements[element_ref_ix]; + bitmap &= (bitmap - 1u); + ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix_1 * 32u)); + param_29.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_30 = ref; + AnnotatedTag tag_2 = Annotated_tag(param_29, param_30); + if (clip_zero_depth == 0u) + { + switch (tag_2.tag) + { + case 1u: + { + uint param_31 = element_ref_ix; + Alloc param_32 = read_tile_alloc(param_31); + TileRef param_33 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_32, param_33); + param_34.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_35 = ref; + AnnoColor fill = Annotated_Color_read(param_34, param_35); + Alloc param_36 = cmd_alloc; + CmdRef param_37 = cmd_ref; + uint param_38 = cmd_limit; + bool _1865 = alloc_cmd(param_36, param_37, param_38); + cmd_alloc = param_36; + cmd_ref = param_37; + cmd_limit = param_38; + if (!_1865) + { + break; + } + Alloc param_39 = cmd_alloc; + CmdRef param_40 = cmd_ref; + uint param_41 = tag_2.flags; + Tile param_42 = tile_1; + float param_43 = fill.linewidth; + write_fill(param_39, param_40, param_41, param_42, param_43); + cmd_ref = param_40; + Alloc param_44 = cmd_alloc; + CmdRef param_45 = cmd_ref; + CmdColor param_46 = CmdColor(fill.rgba_color); + Cmd_Color_write(param_44, param_45, param_46); + cmd_ref.offset += 8u; + break; + } + case 2u: + { + uint param_47 = element_ref_ix; + Alloc param_48 = read_tile_alloc(param_47); + TileRef param_49 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_48, param_49); + param_50.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_51 = ref; + AnnoImage fill_img = Annotated_Image_read(param_50, param_51); + Alloc param_52 = cmd_alloc; + CmdRef param_53 = cmd_ref; + uint param_54 = cmd_limit; + bool _1935 = alloc_cmd(param_52, param_53, param_54); + cmd_alloc = param_52; + cmd_ref = param_53; + cmd_limit = param_54; + if (!_1935) + { + break; + } + Alloc param_55 = cmd_alloc; + CmdRef param_56 = cmd_ref; + uint param_57 = tag_2.flags; + Tile param_58 = tile_1; + float param_59 = fill_img.linewidth; + write_fill(param_55, param_56, param_57, param_58, param_59); + cmd_ref = param_56; + Alloc param_60 = cmd_alloc; + CmdRef param_61 = cmd_ref; + CmdImage param_62 = CmdImage(fill_img.index, fill_img.offset); + Cmd_Image_write(param_60, param_61, param_62); + cmd_ref.offset += 12u; + break; + } + case 3u: + { + uint param_63 = element_ref_ix; + Alloc param_64 = read_tile_alloc(param_63); + TileRef param_65 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u)); + tile_1 = Tile_read(param_64, param_65); + bool _1994 = tile_1.tile.offset == 0u; + bool _2000; + if (_1994) + { + _2000 = tile_1.backdrop == 0; + } + else + { + _2000 = _1994; + } + if (_2000) + { + clip_zero_depth = clip_depth + 1u; + } + else + { + if ((tile_1.tile.offset == 0u) && (clip_depth < 32u)) + { + clip_one_mask |= uint(1 << int(clip_depth)); + } + else + { + param_66.offset = _1066.conf.anno_alloc.offset; + AnnotatedRef param_67 = ref; + AnnoBeginClip begin_clip = Annotated_BeginClip_read(param_66, param_67); + Alloc param_68 = cmd_alloc; + CmdRef param_69 = cmd_ref; + uint param_70 = cmd_limit; + bool _2035 = alloc_cmd(param_68, param_69, param_70); + cmd_alloc = param_68; + cmd_ref = param_69; + cmd_limit = param_70; + if (!_2035) + { + break; + } + Alloc param_71 = cmd_alloc; + CmdRef param_72 = cmd_ref; + uint param_73 = tag_2.flags; + Tile param_74 = tile_1; + float param_75 = begin_clip.linewidth; + write_fill(param_71, param_72, param_73, param_74, param_75); + cmd_ref = param_72; + Alloc param_76 = cmd_alloc; + CmdRef param_77 = cmd_ref; + Cmd_BeginClip_write(param_76, param_77); + cmd_ref.offset += 4u; + if (clip_depth < 32u) + { + clip_one_mask &= uint(~(1 << int(clip_depth))); + } + begin_slot++; + num_begin_slots = max(num_begin_slots, begin_slot); + } + } + clip_depth++; + break; + } + case 4u: + { + clip_depth--; + bool _2087 = clip_depth >= 32u; + bool _2097; + if (!_2087) + { + _2097 = (clip_one_mask & uint(1 << int(clip_depth))) == 0u; + } + else + { + _2097 = _2087; + } + if (_2097) + { + Alloc param_78 = cmd_alloc; + CmdRef param_79 = cmd_ref; + uint param_80 = cmd_limit; + bool _2106 = alloc_cmd(param_78, param_79, param_80); + cmd_alloc = param_78; + cmd_ref = param_79; + cmd_limit = param_80; + if (!_2106) + { + break; + } + Alloc param_81 = cmd_alloc; + CmdRef param_82 = cmd_ref; + Cmd_Solid_write(param_81, param_82); + cmd_ref.offset += 4u; + begin_slot--; + Alloc param_83 = cmd_alloc; + CmdRef param_84 = cmd_ref; + Cmd_EndClip_write(param_83, param_84); + cmd_ref.offset += 4u; + } + break; + } + } + } + else + { + switch (tag_2.tag) + { + case 3u: + { + clip_depth++; + break; + } + case 4u: + { + if (clip_depth == clip_zero_depth) + { + clip_zero_depth = 0u; + } + clip_depth--; + break; + } + } + } + } + barrier(); + rd_ix += 128u; + if ((rd_ix >= ready_ix) && (partition_ix >= n_partitions)) + { + break; + } + } + bool _2171 = (bin_tile_x + tile_x) < _1066.conf.width_in_tiles; + bool _2180; + if (_2171) + { + _2180 = (bin_tile_y + tile_y) < _1066.conf.height_in_tiles; + } + else + { + _2180 = _2171; + } + if (_2180) + { + Alloc param_85 = cmd_alloc; + CmdRef param_86 = cmd_ref; + Cmd_End_write(param_85, param_86); + if (num_begin_slots > 0u) + { + uint scratch_size = (((num_begin_slots * 32u) * 32u) * 2u) * 4u; + uint param_87 = scratch_size; + MallocResult _2201 = malloc(param_87); + MallocResult scratch = _2201; + Alloc param_88 = scratch_alloc; + uint param_89 = scratch_alloc.offset; + Alloc param_90 = scratch.alloc; + alloc_write(param_88, param_89, param_90); + } + } +} + +`, + } + shader_copy_frag = driver.ShaderSources{ + Name: "copy.frag", + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +layout(location = 0) out highp vec4 fragColor; + +highp vec3 sRGBtoRGB(highp vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + highp vec3 below = rgb / vec3(12.9200000762939453125); + highp vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + highp vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + highp vec3 param = texel.xyz; + highp vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; + +vec3 sRGBtoRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + vec3 below = rgb / vec3(12.9200000762939453125); + vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + vec3 param = texel.xyz; + vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +out vec4 fragColor; + +vec3 sRGBtoRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375)); + vec3 below = rgb / vec3(12.9200000762939453125); + vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0); + vec3 param = texel.xyz; + vec3 rgb = sRGBtoRGB(param); + fragColor = vec4(rgb, texel.w); +} + +`, + HLSL: "DXBC\xe6\x89_t\x8b\xfc\xea8\xd9'\xad5.Èk\x01\x00\x00\x00H\x03\x00\x00\x05\x00\x00\x004\x00\x00\x00\xa4\x00\x00\x00\xd8\x00\x00\x00\f\x01\x00\x00\xcc\x02\x00\x00RDEFh\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00@\x00\x00\x00<\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x03\x00\x00SV_Position\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xabSHDR\xb8\x01\x00\x00@\x00\x00\x00n\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00d \x00\x042\x10\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00\x1b\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\a\xf2\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xaeGa=\xaeGa=\xaeGa=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00o\xa7r?o\xa7r?o\xa7r?\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00\x9a\x99\x19@\x9a\x99\x19@\x9a\x99\x19@\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xe6\xae%=\xe6\xae%=\xe6\xae%=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x91\x83\x9e=\x91\x83\x9e=\x91\x83\x9e=\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\r\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } + shader_copy_vert = driver.ShaderSources{ + Name: "copy.vert", + GLSL100ES: `#version 100 + +void main() +{ + for (int spvDummy6 = 0; spvDummy6 < 1; spvDummy6++) + { + if (gl_VertexID == 0) + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 1) + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 2) + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + else if (gl_VertexID == 3) + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL300ES: `#version 300 es + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +void main() +{ + switch (gl_VertexID) + { + case 0: + { + gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); + break; + } + case 1: + { + gl_Position = vec4(1.0, 1.0, 0.0, 1.0); + break; + } + case 2: + { + gl_Position = vec4(-1.0, -1.0, 0.0, 1.0); + break; + } + case 3: + { + gl_Position = vec4(1.0, -1.0, 0.0, 1.0); + break; + } + } +} + +`, + HLSL: "DXBC\x99\xb4[\xef]IX\xa2Qh\x9f\xb6!\x1cR\xe7\x01\x00\x00\x00\xc0\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00D\x02\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDRT\x01\x00\x00@\x00\x01\x00U\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00L\x00\x00\x03\n\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x03\x01@\x00\x00\x00\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x01\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x02\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x03\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\n\x00\x00\x016\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\x17\x00\x00\x016\x00\x00\x05\xb2 \x10\x00\x00\x00\x00\x00F\b\x10\x00\x00\x00\x00\x006\x00\x00\x05B \x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + } + shader_cover_frag = [...]driver.ShaderSources{ + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}}, + Size: 16, + }, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +uniform mediump sampler2D cover; + +varying highp vec2 vCoverUV; +varying vec2 vUV; + +void main() +{ + gl_FragData[0] = _color.color; + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Color +{ + vec4 color; +} _color; + +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in highp vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Color +{ + vec4 color; +}; + +uniform Color _color; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Color +{ + vec4 color; +} _color; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vCoverUV; +in vec2 vUV; + +void main() +{ + fragColor = _color.color; + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBC\x88\x01{\x0f\x94\xca3\xeb\xab߸\xa1\xbfL1\xbf\x01\x00\x00\x00\xa4\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00\x90\x01\x00\x00\f\x02\x00\x00$\x03\x00\x00p\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xffX\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xbc\x00\x00\x00@\x00\x00\x00/\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x10\x01\x00\x00\x01\x00\x00\x00\x98\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xe8\x00\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Color\x00\xab\x91\x00\x00\x00\x01\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +uniform mediump sampler2D cover; + +varying vec2 vUV; +varying highp vec2 vCoverUV; + +void main() +{ + gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +layout(std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; +in highp vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Gradient +{ + vec4 color1; + vec4 color2; +}; + +uniform Gradient _gradient; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Gradient +{ + vec4 color1; + vec4 color2; +} _gradient; + +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0))); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBCj\xa0\x9e\x8d\x1eÌO\rJ\xea\x8f\x17\x11o\x98\x01\x00\x00\x00\x80\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\b\x01\x00\x008\x02\x00\x00\xb4\x02\x00\x00\x00\x04\x00\x00L\x04\x00\x00Aon9\xc8\x00\x00\x00\xc8\x00\x00\x00\x00\x02\xff\xff\x94\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\x00\x12\x80\x00\x00\xff\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x01\x00\x0f\x80\x00\x00U\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x01\x00\xe4\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x00\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03B\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00*\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x01\x00\x00\x01\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x19\x01\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Gradient\x00\xab\xab\x91\x00\x00\x00\x02\x00\x00\x00\xb4\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00\b\x01\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x04\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + { + Name: "cover.frag", + Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}, {Name: "cover", Binding: 1}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; +uniform mediump sampler2D cover; + +varying vec2 vUV; +varying highp vec2 vCoverUV; + +void main() +{ + gl_FragData[0] = texture2D(tex, vUV); + float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0); + gl_FragData[0] *= cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; +uniform mediump sampler2D cover; + +layout(location = 0) out vec4 fragColor; +in vec2 vUV; +in highp vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; +layout(binding = 1) uniform sampler2D cover; + +out vec4 fragColor; +in vec2 vUV; +in vec2 vCoverUV; + +void main() +{ + fragColor = texture(tex, vUV); + float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0); + fragColor *= cover_1; +} + +`, + HLSL: "DXBC\x99\x16l`\xf6:k\xa2Y$\xa1,\xfd\xcdJE\x01\x00\x00\x00\xd8\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xec\x00\x00\x00\xe8\x01\x00\x00d\x02\x00\x00X\x03\x00\x00\xa4\x03\x00\x00Aon9\xac\x00\x00\x00\xac\x00\x00\x00\x00\x02\xff\xff\x80\x00\x00\x00,\x00\x00\x00\x00\x00,\x00\x00\x00,\x00\x00\x00,\x00\x02\x00$\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x01\x01\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0\x1f\x00\x00\x02\x00\x00\x00\x90\x01\b\x0f\xa0\x01\x00\x00\x02\x00\x00\x03\x80\x00\x00\x1b\xb0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x00\b\xe4\xa0B\x00\x00\x03\x01\x00\x0f\x80\x00\x00\xe4\xb0\x01\b\xe4\xa0#\x00\x00\x02\x01\x00\x11\x80\x01\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x01\x00\x00\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xf4\x00\x00\x00@\x00\x00\x00=\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00\xe6\x1a\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x008\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xec\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc2\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xa9\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb8\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\xbc\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00_cover_sampler\x00tex\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + }, + } + shader_cover_vert = driver.ShaderSources{ + Name: "cover.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 64}}, + Size: 68, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +attribute vec2 pos; +varying vec2 vUV; +attribute vec2 uv; +varying vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +layout(location = 0) in vec2 pos; +out vec2 vUV; +layout(location = 1) in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +}; + +uniform Block _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec4 uvCoverTransform; + vec4 uvTransformR1; + vec4 uvTransformR2; + float z; +} _block; + +in vec2 pos; +out vec2 vUV; +in vec2 uv; +out vec2 vCoverUV; + +vec4 toClipSpace(vec4 pos_1) +{ + return pos_1; +} + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0); + gl_Position = toClipSpace(param); + m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz); + vec3 param_2 = vec3(uv, 1.0); + vUV = transform3x2(param_1, param_2).xy; + m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_4 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_3, param_4); + vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy; +} + +`, + HLSL: "DXBCx\xefn{F\v\x88%\xc6\x05\x8f4h\xe4\xaaP\x01\x00\x00\x00\xd8\x05\x00\x00\x06\x00\x00\x008\x00\x00\x00x\x01\x00\x00\x1c\x03\x00\x00\x98\x03\x00\x00\x1c\x05\x00\x00h\x05\x00\x00Aon98\x01\x00\x008\x01\x00\x00\x00\x02\xfe\xff\x04\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x06\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00?\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Đ\x06\x00Р\x06\x00Š\b\x00\x00\x03\x00\x00\b\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x04\xe0\x04\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00\xe1\x90\x06\x00\xe4\xa0\x06\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x06\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\v\x80\x06\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x05\x00\x00\xa0\x00\x00t\x80\x00\x004\x80\xff\xff\x00\x00SHDR\x9c\x01\x00\x00@\x00\x01\x00g\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x05\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\"\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\bB \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x10\x00\x00\b\x82 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x03\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x04\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\v\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF|\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00T\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x05\x00\x00\x00\\\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\x10\x01\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00%\x01\x00\x000\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00:\x01\x00\x00@\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00D\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvCoverTransform\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNh\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00Y\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_elements_comp = driver.ShaderSources{ + Name: "elements.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct ElementRef +{ + uint offset; +}; + +struct LineSegRef +{ + uint offset; +}; + +struct LineSeg +{ + vec2 p0; + vec2 p1; +}; + +struct QuadSegRef +{ + uint offset; +}; + +struct QuadSeg +{ + vec2 p0; + vec2 p1; + vec2 p2; +}; + +struct CubicSegRef +{ + uint offset; +}; + +struct CubicSeg +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; +}; + +struct FillColorRef +{ + uint offset; +}; + +struct FillColor +{ + uint rgba_color; +}; + +struct FillImageRef +{ + uint offset; +}; + +struct FillImage +{ + uint index; + ivec2 offset; +}; + +struct SetLineWidthRef +{ + uint offset; +}; + +struct SetLineWidth +{ + float width; +}; + +struct TransformRef +{ + uint offset; +}; + +struct Transform +{ + vec4 mat; + vec2 translate; +}; + +struct ClipRef +{ + uint offset; +}; + +struct Clip +{ + vec4 bbox; +}; + +struct SetFillModeRef +{ + uint offset; +}; + +struct SetFillMode +{ + uint fill_mode; +}; + +struct ElementTag +{ + uint tag; + uint flags; +}; + +struct StateRef +{ + uint offset; +}; + +struct State +{ + vec4 mat; + vec2 translate; + vec4 bbox; + float linewidth; + uint flags; + uint path_count; + uint pathseg_count; + uint trans_count; +}; + +struct AnnoImageRef +{ + uint offset; +}; + +struct AnnoImage +{ + vec4 bbox; + float linewidth; + uint index; + ivec2 offset; +}; + +struct AnnoColorRef +{ + uint offset; +}; + +struct AnnoColor +{ + vec4 bbox; + float linewidth; + uint rgba_color; +}; + +struct AnnoBeginClipRef +{ + uint offset; +}; + +struct AnnoBeginClip +{ + vec4 bbox; + float linewidth; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct PathCubicRef +{ + uint offset; +}; + +struct PathCubic +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; + uint path_ix; + uint trans_ix; + vec2 stroke; +}; + +struct PathSegRef +{ + uint offset; +}; + +struct TransformSegRef +{ + uint offset; +}; + +struct TransformSeg +{ + vec4 mat; + vec2 translate; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _294; + +layout(binding = 2, std430) readonly buffer SceneBuf +{ + uint scene[]; +} _323; + +layout(binding = 3, std430) coherent buffer StateBuf +{ + uint part_counter; + uint state[]; +} _779; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _2435; + +shared uint sh_part_ix; +shared State sh_state[32]; +shared State sh_prefix; + +ElementTag Element_tag(ElementRef ref) +{ + uint tag_and_flags = _323.scene[ref.offset >> uint(2)]; + return ElementTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +LineSeg LineSeg_read(LineSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + LineSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +LineSeg Element_Line_read(ElementRef ref) +{ + LineSegRef param = LineSegRef(ref.offset + 4u); + return LineSeg_read(param); +} + +QuadSeg QuadSeg_read(QuadSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + QuadSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +QuadSeg Element_Quad_read(ElementRef ref) +{ + QuadSegRef param = QuadSegRef(ref.offset + 4u); + return QuadSeg_read(param); +} + +CubicSeg CubicSeg_read(CubicSegRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + uint raw6 = _323.scene[ix + 6u]; + uint raw7 = _323.scene[ix + 7u]; + CubicSeg s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7)); + return s; +} + +CubicSeg Element_Cubic_read(ElementRef ref) +{ + CubicSegRef param = CubicSegRef(ref.offset + 4u); + return CubicSeg_read(param); +} + +SetLineWidth SetLineWidth_read(SetLineWidthRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + SetLineWidth s; + s.width = uintBitsToFloat(raw0); + return s; +} + +SetLineWidth Element_SetLineWidth_read(ElementRef ref) +{ + SetLineWidthRef param = SetLineWidthRef(ref.offset + 4u); + return SetLineWidth_read(param); +} + +Transform Transform_read(TransformRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + uint raw4 = _323.scene[ix + 4u]; + uint raw5 = _323.scene[ix + 5u]; + Transform s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +Transform Element_Transform_read(ElementRef ref) +{ + TransformRef param = TransformRef(ref.offset + 4u); + return Transform_read(param); +} + +SetFillMode SetFillMode_read(SetFillModeRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + SetFillMode s; + s.fill_mode = raw0; + return s; +} + +SetFillMode Element_SetFillMode_read(ElementRef ref) +{ + SetFillModeRef param = SetFillModeRef(ref.offset + 4u); + return SetFillMode_read(param); +} + +State map_element(ElementRef ref) +{ + ElementRef param = ref; + uint tag = Element_tag(param).tag; + State c; + c.bbox = vec4(0.0); + c.mat = vec4(1.0, 0.0, 0.0, 1.0); + c.translate = vec2(0.0); + c.linewidth = 1.0; + c.flags = 0u; + c.path_count = 0u; + c.pathseg_count = 0u; + c.trans_count = 0u; + switch (tag) + { + case 1u: + { + ElementRef param_1 = ref; + LineSeg line = Element_Line_read(param_1); + vec2 _1919 = min(line.p0, line.p1); + c.bbox = vec4(_1919.x, _1919.y, c.bbox.z, c.bbox.w); + vec2 _1927 = max(line.p0, line.p1); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1927.x, _1927.y); + c.pathseg_count = 1u; + break; + } + case 2u: + { + ElementRef param_2 = ref; + QuadSeg quad = Element_Quad_read(param_2); + vec2 _1944 = min(min(quad.p0, quad.p1), quad.p2); + c.bbox = vec4(_1944.x, _1944.y, c.bbox.z, c.bbox.w); + vec2 _1955 = max(max(quad.p0, quad.p1), quad.p2); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1955.x, _1955.y); + c.pathseg_count = 1u; + break; + } + case 3u: + { + ElementRef param_3 = ref; + CubicSeg cubic = Element_Cubic_read(param_3); + vec2 _1975 = min(min(cubic.p0, cubic.p1), min(cubic.p2, cubic.p3)); + c.bbox = vec4(_1975.x, _1975.y, c.bbox.z, c.bbox.w); + vec2 _1989 = max(max(cubic.p0, cubic.p1), max(cubic.p2, cubic.p3)); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1989.x, _1989.y); + c.pathseg_count = 1u; + break; + } + case 4u: + case 9u: + case 7u: + { + c.flags = 4u; + c.path_count = 1u; + break; + } + case 8u: + { + c.path_count = 1u; + break; + } + case 5u: + { + ElementRef param_4 = ref; + SetLineWidth lw = Element_SetLineWidth_read(param_4); + c.linewidth = lw.width; + c.flags = 1u; + break; + } + case 6u: + { + ElementRef param_5 = ref; + Transform t = Element_Transform_read(param_5); + c.mat = t.mat; + c.translate = t.translate; + c.trans_count = 1u; + break; + } + case 10u: + { + ElementRef param_6 = ref; + SetFillMode fm = Element_SetFillMode_read(param_6); + c.flags = 8u | (fm.fill_mode << uint(4)); + break; + } + } + return c; +} + +ElementRef Element_index(ElementRef ref, uint index) +{ + return ElementRef(ref.offset + (index * 36u)); +} + +State combine_state(State a, State b) +{ + State c; + c.bbox.x = (min(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + min(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x; + c.bbox.y = (min(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + min(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y; + c.bbox.z = (max(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + max(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x; + c.bbox.w = (max(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + max(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y; + bool _1657 = (a.flags & 4u) == 0u; + bool _1665; + if (_1657) + { + _1665 = b.bbox.z <= b.bbox.x; + } + else + { + _1665 = _1657; + } + bool _1673; + if (_1665) + { + _1673 = b.bbox.w <= b.bbox.y; + } + else + { + _1673 = _1665; + } + if (_1673) + { + c.bbox = a.bbox; + } + else + { + bool _1683 = (a.flags & 4u) == 0u; + bool _1690; + if (_1683) + { + _1690 = (b.flags & 2u) == 0u; + } + else + { + _1690 = _1683; + } + bool _1707; + if (_1690) + { + bool _1697 = a.bbox.z > a.bbox.x; + bool _1706; + if (!_1697) + { + _1706 = a.bbox.w > a.bbox.y; + } + else + { + _1706 = _1697; + } + _1707 = _1706; + } + else + { + _1707 = _1690; + } + if (_1707) + { + vec2 _1716 = min(a.bbox.xy, c.bbox.xy); + c.bbox = vec4(_1716.x, _1716.y, c.bbox.z, c.bbox.w); + vec2 _1726 = max(a.bbox.zw, c.bbox.zw); + c.bbox = vec4(c.bbox.x, c.bbox.y, _1726.x, _1726.y); + } + } + c.mat.x = (a.mat.x * b.mat.x) + (a.mat.z * b.mat.y); + c.mat.y = (a.mat.y * b.mat.x) + (a.mat.w * b.mat.y); + c.mat.z = (a.mat.x * b.mat.z) + (a.mat.z * b.mat.w); + c.mat.w = (a.mat.y * b.mat.z) + (a.mat.w * b.mat.w); + c.translate.x = ((a.mat.x * b.translate.x) + (a.mat.z * b.translate.y)) + a.translate.x; + c.translate.y = ((a.mat.y * b.translate.x) + (a.mat.w * b.translate.y)) + a.translate.y; + float _1812; + if ((b.flags & 1u) == 0u) + { + _1812 = a.linewidth; + } + else + { + _1812 = b.linewidth; + } + c.linewidth = _1812; + c.flags = (a.flags & 11u) | b.flags; + c.flags |= ((a.flags & 4u) >> uint(1)); + uint _1842; + if ((b.flags & 8u) == 0u) + { + _1842 = a.flags; + } + else + { + _1842 = b.flags; + } + uint fill_mode = _1842; + fill_mode &= 16u; + c.flags = (c.flags & 4294967279u) | fill_mode; + c.path_count = a.path_count + b.path_count; + c.pathseg_count = a.pathseg_count + b.pathseg_count; + c.trans_count = a.trans_count + b.trans_count; + return c; +} + +StateRef state_aggregate_ref(uint partition_ix) +{ + return StateRef(4u + (partition_ix * 124u)); +} + +void State_write(StateRef ref, State s) +{ + uint ix = ref.offset >> uint(2); + _779.state[ix + 0u] = floatBitsToUint(s.mat.x); + _779.state[ix + 1u] = floatBitsToUint(s.mat.y); + _779.state[ix + 2u] = floatBitsToUint(s.mat.z); + _779.state[ix + 3u] = floatBitsToUint(s.mat.w); + _779.state[ix + 4u] = floatBitsToUint(s.translate.x); + _779.state[ix + 5u] = floatBitsToUint(s.translate.y); + _779.state[ix + 6u] = floatBitsToUint(s.bbox.x); + _779.state[ix + 7u] = floatBitsToUint(s.bbox.y); + _779.state[ix + 8u] = floatBitsToUint(s.bbox.z); + _779.state[ix + 9u] = floatBitsToUint(s.bbox.w); + _779.state[ix + 10u] = floatBitsToUint(s.linewidth); + _779.state[ix + 11u] = s.flags; + _779.state[ix + 12u] = s.path_count; + _779.state[ix + 13u] = s.pathseg_count; + _779.state[ix + 14u] = s.trans_count; +} + +StateRef state_prefix_ref(uint partition_ix) +{ + return StateRef((4u + (partition_ix * 124u)) + 60u); +} + +uint state_flag_index(uint partition_ix) +{ + return partition_ix * 31u; +} + +State State_read(StateRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _779.state[ix + 0u]; + uint raw1 = _779.state[ix + 1u]; + uint raw2 = _779.state[ix + 2u]; + uint raw3 = _779.state[ix + 3u]; + uint raw4 = _779.state[ix + 4u]; + uint raw5 = _779.state[ix + 5u]; + uint raw6 = _779.state[ix + 6u]; + uint raw7 = _779.state[ix + 7u]; + uint raw8 = _779.state[ix + 8u]; + uint raw9 = _779.state[ix + 9u]; + uint raw10 = _779.state[ix + 10u]; + uint raw11 = _779.state[ix + 11u]; + uint raw12 = _779.state[ix + 12u]; + uint raw13 = _779.state[ix + 13u]; + uint raw14 = _779.state[ix + 14u]; + State s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.bbox = vec4(uintBitsToFloat(raw6), uintBitsToFloat(raw7), uintBitsToFloat(raw8), uintBitsToFloat(raw9)); + s.linewidth = uintBitsToFloat(raw10); + s.flags = raw11; + s.path_count = raw12; + s.pathseg_count = raw13; + s.trans_count = raw14; + return s; +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +vec2 get_linewidth(State st) +{ + return vec2(length(st.mat.xz), length(st.mat.yw)) * (0.5 * st.linewidth); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _294.memory[offset] = val; +} + +void PathCubic_write(Alloc a, PathCubicRef ref, PathCubic s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.p0.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.p0.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.p1.x); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.p1.y); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.p2.x); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = floatBitsToUint(s.p2.y); + write_mem(param_15, param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 6u; + uint param_20 = floatBitsToUint(s.p3.x); + write_mem(param_18, param_19, param_20); + Alloc param_21 = a; + uint param_22 = ix + 7u; + uint param_23 = floatBitsToUint(s.p3.y); + write_mem(param_21, param_22, param_23); + Alloc param_24 = a; + uint param_25 = ix + 8u; + uint param_26 = s.path_ix; + write_mem(param_24, param_25, param_26); + Alloc param_27 = a; + uint param_28 = ix + 9u; + uint param_29 = s.trans_ix; + write_mem(param_27, param_28, param_29); + Alloc param_30 = a; + uint param_31 = ix + 10u; + uint param_32 = floatBitsToUint(s.stroke.x); + write_mem(param_30, param_31, param_32); + Alloc param_33 = a; + uint param_34 = ix + 11u; + uint param_35 = floatBitsToUint(s.stroke.y); + write_mem(param_33, param_34, param_35); +} + +void PathSeg_Cubic_write(Alloc a, PathSegRef ref, uint flags, PathCubic s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + PathCubicRef param_4 = PathCubicRef(ref.offset + 4u); + PathCubic param_5 = s; + PathCubic_write(param_3, param_4, param_5); +} + +FillColor FillColor_read(FillColorRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + FillColor s; + s.rgba_color = raw0; + return s; +} + +FillColor Element_FillColor_read(ElementRef ref) +{ + FillColorRef param = FillColorRef(ref.offset + 4u); + return FillColor_read(param); +} + +void AnnoColor_write(Alloc a, AnnoColorRef ref, AnnoColor s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.rgba_color; + write_mem(param_15, param_16, param_17); +} + +void Annotated_Color_write(Alloc a, AnnotatedRef ref, uint flags, AnnoColor s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 1u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoColorRef param_4 = AnnoColorRef(ref.offset + 4u); + AnnoColor param_5 = s; + AnnoColor_write(param_3, param_4, param_5); +} + +FillImage FillImage_read(FillImageRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + FillImage s; + s.index = raw0; + s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16); + return s; +} + +FillImage Element_FillImage_read(ElementRef ref) +{ + FillImageRef param = FillImageRef(ref.offset + 4u); + return FillImage_read(param); +} + +void AnnoImage_write(Alloc a, AnnoImageRef ref, AnnoImage s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.index; + write_mem(param_15, param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 6u; + uint param_20 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16)); + write_mem(param_18, param_19, param_20); +} + +void Annotated_Image_write(Alloc a, AnnotatedRef ref, uint flags, AnnoImage s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 2u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoImageRef param_4 = AnnoImageRef(ref.offset + 4u); + AnnoImage param_5 = s; + AnnoImage_write(param_3, param_4, param_5); +} + +Clip Clip_read(ClipRef ref) +{ + uint ix = ref.offset >> uint(2); + uint raw0 = _323.scene[ix + 0u]; + uint raw1 = _323.scene[ix + 1u]; + uint raw2 = _323.scene[ix + 2u]; + uint raw3 = _323.scene[ix + 3u]; + Clip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +Clip Element_BeginClip_read(ElementRef ref) +{ + ClipRef param = ClipRef(ref.offset + 4u); + return Clip_read(param); +} + +void AnnoBeginClip_write(Alloc a, AnnoBeginClipRef ref, AnnoBeginClip s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.linewidth); + write_mem(param_12, param_13, param_14); +} + +void Annotated_BeginClip_write(Alloc a, AnnotatedRef ref, uint flags, AnnoBeginClip s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = (flags << uint(16)) | 3u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoBeginClipRef param_4 = AnnoBeginClipRef(ref.offset + 4u); + AnnoBeginClip param_5 = s; + AnnoBeginClip_write(param_3, param_4, param_5); +} + +Clip Element_EndClip_read(ElementRef ref) +{ + ClipRef param = ClipRef(ref.offset + 4u); + return Clip_read(param); +} + +void AnnoEndClip_write(Alloc a, AnnoEndClipRef ref, AnnoEndClip s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.bbox.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.bbox.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.bbox.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.bbox.w); + write_mem(param_9, param_10, param_11); +} + +void Annotated_EndClip_write(Alloc a, AnnotatedRef ref, AnnoEndClip s) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint param_2 = 4u; + write_mem(param, param_1, param_2); + Alloc param_3 = a; + AnnoEndClipRef param_4 = AnnoEndClipRef(ref.offset + 4u); + AnnoEndClip param_5 = s; + AnnoEndClip_write(param_3, param_4, param_5); +} + +void TransformSeg_write(Alloc a, TransformSegRef ref, TransformSeg s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.mat.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.mat.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.mat.z); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.mat.w); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.translate.x); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = floatBitsToUint(s.translate.y); + write_mem(param_15, param_16, param_17); +} + +void main() +{ + if (_294.mem_error != 0u) + { + return; + } + if (gl_LocalInvocationID.x == 0u) + { + uint _2069 = atomicAdd(_779.part_counter, 1u); + sh_part_ix = _2069; + } + barrier(); + uint part_ix = sh_part_ix; + uint ix = (part_ix * 128u) + (gl_LocalInvocationID.x * 4u); + ElementRef ref = ElementRef(ix * 36u); + ElementRef param = ref; + State th_state[4]; + th_state[0] = map_element(param); + for (uint i = 1u; i < 4u; i++) + { + ElementRef param_1 = ref; + uint param_2 = i; + ElementRef param_3 = Element_index(param_1, param_2); + State param_4 = th_state[i - 1u]; + State param_5 = map_element(param_3); + th_state[i] = combine_state(param_4, param_5); + } + State agg = th_state[3]; + sh_state[gl_LocalInvocationID.x] = agg; + for (uint i_1 = 0u; i_1 < 5u; i_1++) + { + barrier(); + if (gl_LocalInvocationID.x >= uint(1 << int(i_1))) + { + State other = sh_state[gl_LocalInvocationID.x - uint(1 << int(i_1))]; + State param_6 = other; + State param_7 = agg; + agg = combine_state(param_6, param_7); + } + barrier(); + sh_state[gl_LocalInvocationID.x] = agg; + } + State exclusive; + exclusive.bbox = vec4(0.0); + exclusive.mat = vec4(1.0, 0.0, 0.0, 1.0); + exclusive.translate = vec2(0.0); + exclusive.linewidth = 1.0; + exclusive.flags = 0u; + exclusive.path_count = 0u; + exclusive.pathseg_count = 0u; + exclusive.trans_count = 0u; + if (gl_LocalInvocationID.x == 31u) + { + uint param_8 = part_ix; + StateRef param_9 = state_aggregate_ref(param_8); + State param_10 = agg; + State_write(param_9, param_10); + uint flag = 1u; + memoryBarrierBuffer(); + if (part_ix == 0u) + { + uint param_11 = part_ix; + StateRef param_12 = state_prefix_ref(param_11); + State param_13 = agg; + State_write(param_12, param_13); + flag = 2u; + } + uint param_14 = part_ix; + _779.state[state_flag_index(param_14)] = flag; + if (part_ix != 0u) + { + uint look_back_ix = part_ix - 1u; + uint their_ix = 0u; + State their_agg; + while (true) + { + uint param_15 = look_back_ix; + flag = _779.state[state_flag_index(param_15)]; + if (flag == 2u) + { + uint param_16 = look_back_ix; + StateRef param_17 = state_prefix_ref(param_16); + State their_prefix = State_read(param_17); + State param_18 = their_prefix; + State param_19 = exclusive; + exclusive = combine_state(param_18, param_19); + break; + } + else + { + if (flag == 1u) + { + uint param_20 = look_back_ix; + StateRef param_21 = state_aggregate_ref(param_20); + their_agg = State_read(param_21); + State param_22 = their_agg; + State param_23 = exclusive; + exclusive = combine_state(param_22, param_23); + look_back_ix--; + their_ix = 0u; + continue; + } + } + ElementRef ref_1 = ElementRef(((look_back_ix * 128u) + their_ix) * 36u); + ElementRef param_24 = ref_1; + State s = map_element(param_24); + if (their_ix == 0u) + { + their_agg = s; + } + else + { + State param_25 = their_agg; + State param_26 = s; + their_agg = combine_state(param_25, param_26); + } + their_ix++; + if (their_ix == 128u) + { + State param_27 = their_agg; + State param_28 = exclusive; + exclusive = combine_state(param_27, param_28); + if (look_back_ix == 0u) + { + break; + } + look_back_ix--; + their_ix = 0u; + } + } + State param_29 = exclusive; + State param_30 = agg; + State inclusive_prefix = combine_state(param_29, param_30); + sh_prefix = exclusive; + uint param_31 = part_ix; + StateRef param_32 = state_prefix_ref(param_31); + State param_33 = inclusive_prefix; + State_write(param_32, param_33); + memoryBarrierBuffer(); + flag = 2u; + uint param_34 = part_ix; + _779.state[state_flag_index(param_34)] = flag; + } + } + barrier(); + if (part_ix != 0u) + { + exclusive = sh_prefix; + } + State row = exclusive; + if (gl_LocalInvocationID.x > 0u) + { + State other_1 = sh_state[gl_LocalInvocationID.x - 1u]; + State param_35 = row; + State param_36 = other_1; + row = combine_state(param_35, param_36); + } + PathCubic path_cubic; + PathSegRef path_out_ref; + Alloc param_45; + Alloc param_51; + Alloc param_57; + AnnoColor anno_fill; + AnnotatedRef out_ref; + Alloc param_63; + AnnoImage anno_img; + Alloc param_69; + AnnoBeginClip anno_begin_clip; + Alloc param_75; + Alloc param_80; + Alloc param_83; + for (uint i_2 = 0u; i_2 < 4u; i_2++) + { + State param_37 = row; + State param_38 = th_state[i_2]; + State st = combine_state(param_37, param_38); + ElementRef param_39 = ref; + uint param_40 = i_2; + ElementRef this_ref = Element_index(param_39, param_40); + ElementRef param_41 = this_ref; + ElementTag tag = Element_tag(param_41); + uint param_42 = st.flags >> uint(4); + uint fill_mode = fill_mode_from_flags(param_42); + bool is_stroke = fill_mode == 1u; + switch (tag.tag) + { + case 1u: + { + ElementRef param_43 = this_ref; + LineSeg line = Element_Line_read(param_43); + path_cubic.p0 = line.p0; + path_cubic.p1 = mix(line.p0, line.p1, vec2(0.3333333432674407958984375)); + path_cubic.p2 = mix(line.p1, line.p0, vec2(0.3333333432674407958984375)); + path_cubic.p3 = line.p1; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_44 = st; + path_cubic.stroke = get_linewidth(param_44); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_45.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_46 = path_out_ref; + uint param_47 = fill_mode; + PathCubic param_48 = path_cubic; + PathSeg_Cubic_write(param_45, param_46, param_47, param_48); + break; + } + case 2u: + { + ElementRef param_49 = this_ref; + QuadSeg quad = Element_Quad_read(param_49); + path_cubic.p0 = quad.p0; + path_cubic.p1 = mix(quad.p1, quad.p0, vec2(0.3333333432674407958984375)); + path_cubic.p2 = mix(quad.p1, quad.p2, vec2(0.3333333432674407958984375)); + path_cubic.p3 = quad.p2; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_50 = st; + path_cubic.stroke = get_linewidth(param_50); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_51.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_52 = path_out_ref; + uint param_53 = fill_mode; + PathCubic param_54 = path_cubic; + PathSeg_Cubic_write(param_51, param_52, param_53, param_54); + break; + } + case 3u: + { + ElementRef param_55 = this_ref; + CubicSeg cubic = Element_Cubic_read(param_55); + path_cubic.p0 = cubic.p0; + path_cubic.p1 = cubic.p1; + path_cubic.p2 = cubic.p2; + path_cubic.p3 = cubic.p3; + path_cubic.path_ix = st.path_count; + path_cubic.trans_ix = st.trans_count; + if (is_stroke) + { + State param_56 = st; + path_cubic.stroke = get_linewidth(param_56); + } + else + { + path_cubic.stroke = vec2(0.0); + } + path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u)); + param_57.offset = _2435.conf.pathseg_alloc.offset; + PathSegRef param_58 = path_out_ref; + uint param_59 = fill_mode; + PathCubic param_60 = path_cubic; + PathSeg_Cubic_write(param_57, param_58, param_59, param_60); + break; + } + case 4u: + { + ElementRef param_61 = this_ref; + FillColor fill = Element_FillColor_read(param_61); + anno_fill.rgba_color = fill.rgba_color; + if (is_stroke) + { + State param_62 = st; + vec2 lw = get_linewidth(param_62); + anno_fill.bbox = st.bbox + vec4(-lw, lw); + anno_fill.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_fill.bbox = st.bbox; + anno_fill.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_63.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_64 = out_ref; + uint param_65 = fill_mode; + AnnoColor param_66 = anno_fill; + Annotated_Color_write(param_63, param_64, param_65, param_66); + break; + } + case 9u: + { + ElementRef param_67 = this_ref; + FillImage fill_img = Element_FillImage_read(param_67); + anno_img.index = fill_img.index; + anno_img.offset = fill_img.offset; + if (is_stroke) + { + State param_68 = st; + vec2 lw_1 = get_linewidth(param_68); + anno_img.bbox = st.bbox + vec4(-lw_1, lw_1); + anno_img.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_img.bbox = st.bbox; + anno_img.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_69.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_70 = out_ref; + uint param_71 = fill_mode; + AnnoImage param_72 = anno_img; + Annotated_Image_write(param_69, param_70, param_71, param_72); + break; + } + case 7u: + { + ElementRef param_73 = this_ref; + Clip begin_clip = Element_BeginClip_read(param_73); + anno_begin_clip.bbox = begin_clip.bbox; + if (is_stroke) + { + State param_74 = st; + vec2 lw_2 = get_linewidth(param_74); + anno_begin_clip.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z))); + } + else + { + anno_fill.linewidth = 0.0; + } + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_75.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_76 = out_ref; + uint param_77 = fill_mode; + AnnoBeginClip param_78 = anno_begin_clip; + Annotated_BeginClip_write(param_75, param_76, param_77, param_78); + break; + } + case 8u: + { + ElementRef param_79 = this_ref; + Clip end_clip = Element_EndClip_read(param_79); + AnnoEndClip anno_end_clip = AnnoEndClip(end_clip.bbox); + out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u)); + param_80.offset = _2435.conf.anno_alloc.offset; + AnnotatedRef param_81 = out_ref; + AnnoEndClip param_82 = anno_end_clip; + Annotated_EndClip_write(param_80, param_81, param_82); + break; + } + case 6u: + { + TransformSeg transform = TransformSeg(st.mat, st.translate); + TransformSegRef trans_ref = TransformSegRef(_2435.conf.trans_alloc.offset + ((st.trans_count - 1u) * 24u)); + param_83.offset = _2435.conf.trans_alloc.offset; + TransformSegRef param_84 = trans_ref; + TransformSeg param_85 = transform; + TransformSeg_write(param_83, param_84, param_85); + break; + } + } + } +} + +`, + } + shader_intersect_frag = driver.ShaderSources{ + Name: "intersect.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "cover", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D cover; + +varying highp vec2 vUV; + +void main() +{ + float cover_1 = abs(texture2D(cover, vUV).x); + gl_FragData[0].x = cover_1; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D cover; + +in highp vec2 vUV; +layout(location = 0) out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D cover; + +in vec2 vUV; +out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D cover; + +in vec2 vUV; +out vec4 fragColor; + +void main() +{ + float cover_1 = abs(texture(cover, vUV).x); + fragColor.x = cover_1; +} + +`, + HLSL: "DXBC\xe0\xe4\x03\x8c\xacVF\x82l\xe7|\xc3T\xa6'\xef\x01\x00\x00\x00\b\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xd4\x00\x00\x00\x80\x01\x00\x00\xfc\x01\x00\x00\xa0\x02\x00\x00\xd4\x02\x00\x00Aon9\x94\x00\x00\x00\x94\x00\x00\x00\x00\x02\xff\xffl\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x01\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\x00\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa4\x00\x00\x00@\x00\x00\x00)\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x006\x00\x00\x06\x12 \x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00q\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00k\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_cover_sampler\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_intersect_vert = driver.ShaderSources{ + Name: "intersect.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.uvTransform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.subUVTransform", Type: 0x0, Size: 4, Offset: 16}}, + Size: 32, + }, + GLSL100ES: `#version 100 + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 uvTransform; + vec4 subUVTransform; +}; + +uniform Block _block; + +attribute vec2 pos; +attribute vec2 uv; +varying vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL300ES: `#version 300 es + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(std140) uniform Block +{ + vec4 uvTransform; + vec4 subUVTransform; +} _block; + +layout(location = 0) in vec2 pos; +layout(location = 1) in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +struct Block +{ + vec4 uvTransform; + vec4 subUVTransform; +}; + +uniform Block _block; + +in vec2 pos; +in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct m3x2 +{ + vec3 r0; + vec3 r1; +}; + +layout(binding = 0, std140) uniform Block +{ + vec4 uvTransform; + vec4 subUVTransform; +} _block; + +in vec2 pos; +in vec2 uv; +out vec2 vUV; + +vec3 transform3x2(m3x2 t, vec3 v) +{ + return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); +} + +void main() +{ + m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0)); + vec3 param_1 = vec3(pos, 1.0); + vec3 p = transform3x2(param, param_1); + gl_Position = vec4(p, 1.0); + m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_3 = vec3(uv, 1.0); + vec3 uv3 = transform3x2(param_2, param_3); + vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw; + m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)); + vec3 param_5 = vec3(vUV, 1.0); + vUV = transform3x2(param_4, param_5).xy; + vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw; +} + +`, + HLSL: "DXBCxH\xc4I\xbe\x0f[|\nl\x899\xe0\xb8\xcb?\x01\x00\x00\x00\xdc\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x01\x00\x00\xc4\x02\x00\x00@\x03\x00\x008\x04\x00\x00\x84\x04\x00\x00Aon9\f\x01\x00\x00\f\x01\x00\x00\x00\x02\xfe\xff\xd8\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00U\x90\x03\x00\xe4\xa0\x03\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x03\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x01\x00\x00\x02\x00\x00\x04\x80\x03\x00\x00\xa0\b\x00\x00\x03\x00\x00\b\x80\x03\x00ɠ\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xec\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00\x00\xa0\xff\xff\x00\x00SHDRp\x01\x00\x00@\x00\x01\x00\\\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?6\x00\x00\x05R\x00\x10\x00\x00\x00\x00\x00V\x14\x10\x00\x01\x00\x00\x00\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x002\x00\x00\v2\x00\x10\x00\x00\x00\x00\x00\xe6\n\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00\xc6\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xc6\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00_block_uvTransform\x00\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_subUVTransform\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_kernel4_comp = driver.ShaderSources{ + Name: "kernel4.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 16, local_size_y = 8, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct CmdStrokeRef +{ + uint offset; +}; + +struct CmdStroke +{ + uint tile_ref; + float half_width; +}; + +struct CmdFillRef +{ + uint offset; +}; + +struct CmdFill +{ + uint tile_ref; + int backdrop; +}; + +struct CmdColorRef +{ + uint offset; +}; + +struct CmdColor +{ + uint rgba_color; +}; + +struct CmdImageRef +{ + uint offset; +}; + +struct CmdImage +{ + uint index; + ivec2 offset; +}; + +struct CmdAlphaRef +{ + uint offset; +}; + +struct CmdAlpha +{ + float alpha; +}; + +struct CmdJumpRef +{ + uint offset; +}; + +struct CmdJump +{ + uint new_ref; +}; + +struct CmdRef +{ + uint offset; +}; + +struct CmdTag +{ + uint tag; + uint flags; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct TileSeg +{ + vec2 origin; + vec2 vector; + float y_edge; + TileSegRef next; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _196; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _693; + +layout(binding = 3, rgba8) uniform readonly highp image2D images[1]; +layout(binding = 2, rgba8) uniform writeonly highp image2D image; + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _196.memory[offset]; + return v; +} + +Alloc alloc_read(Alloc a, uint offset) +{ + Alloc param = a; + uint param_1 = offset >> uint(2); + Alloc alloc; + alloc.offset = read_mem(param, param_1); + return alloc; +} + +CmdTag Cmd_tag(Alloc a, CmdRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return CmdTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +CmdStroke CmdStroke_read(Alloc a, CmdStrokeRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdStroke s; + s.tile_ref = raw0; + s.half_width = uintBitsToFloat(raw1); + return s; +} + +CmdStroke Cmd_Stroke_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdStrokeRef param_1 = CmdStrokeRef(ref.offset + 4u); + return CmdStroke_read(param, param_1); +} + +TileSeg TileSeg_read(Alloc a, TileSegRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + TileSeg s; + s.origin = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.vector = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.y_edge = uintBitsToFloat(raw4); + s.next = TileSegRef(raw5); + return s; +} + +uvec2 chunk_offset(uint i) +{ + return uvec2((i % 2u) * 16u, (i / 2u) * 8u); +} + +CmdFill CmdFill_read(Alloc a, CmdFillRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdFill s; + s.tile_ref = raw0; + s.backdrop = int(raw1); + return s; +} + +CmdFill Cmd_Fill_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdFillRef param_1 = CmdFillRef(ref.offset + 4u); + return CmdFill_read(param, param_1); +} + +CmdAlpha CmdAlpha_read(Alloc a, CmdAlphaRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdAlpha s; + s.alpha = uintBitsToFloat(raw0); + return s; +} + +CmdAlpha Cmd_Alpha_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdAlphaRef param_1 = CmdAlphaRef(ref.offset + 4u); + return CmdAlpha_read(param, param_1); +} + +CmdColor CmdColor_read(Alloc a, CmdColorRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdColor s; + s.rgba_color = raw0; + return s; +} + +CmdColor Cmd_Color_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdColorRef param_1 = CmdColorRef(ref.offset + 4u); + return CmdColor_read(param, param_1); +} + +vec3 fromsRGB(vec3 srgb) +{ + bvec3 cutoff = greaterThanEqual(srgb, vec3(0.040449999272823333740234375)); + vec3 below = srgb / vec3(12.9200000762939453125); + vec3 above = pow((srgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625)); + return mix(below, above, cutoff); +} + +vec4 unpacksRGB(uint srgba) +{ + vec4 color = unpackUnorm4x8(srgba).wzyx; + vec3 param = color.xyz; + return vec4(fromsRGB(param), color.w); +} + +CmdImage CmdImage_read(Alloc a, CmdImageRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + CmdImage s; + s.index = raw0; + s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16); + return s; +} + +CmdImage Cmd_Image_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdImageRef param_1 = CmdImageRef(ref.offset + 4u); + return CmdImage_read(param, param_1); +} + +vec4[8] fillImage(uvec2 xy, CmdImage cmd_img) +{ + vec4 rgba[8]; + for (uint i = 0u; i < 8u; i++) + { + uint param = i; + ivec2 uv = ivec2(xy + chunk_offset(param)) + cmd_img.offset; + vec4 fg_rgba = imageLoad(images[0], uv); + vec3 param_1 = fg_rgba.xyz; + vec3 _663 = fromsRGB(param_1); + fg_rgba = vec4(_663.x, _663.y, _663.z, fg_rgba.w); + rgba[i] = fg_rgba; + } + return rgba; +} + +vec3 tosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return mix(below, above, cutoff); +} + +uint packsRGB(inout vec4 rgba) +{ + vec3 param = rgba.xyz; + rgba = vec4(tosRGB(param), rgba.w); + return packUnorm4x8(rgba.wzyx); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _196.memory[offset] = val; +} + +CmdJump CmdJump_read(Alloc a, CmdJumpRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + CmdJump s; + s.new_ref = raw0; + return s; +} + +CmdJump Cmd_Jump_read(Alloc a, CmdRef ref) +{ + Alloc param = a; + CmdJumpRef param_1 = CmdJumpRef(ref.offset + 4u); + return CmdJump_read(param, param_1); +} + +void main() +{ + if (_196.mem_error != 0u) + { + return; + } + uint tile_ix = (gl_WorkGroupID.y * _693.conf.width_in_tiles) + gl_WorkGroupID.x; + Alloc param; + param.offset = _693.conf.ptcl_alloc.offset; + uint param_1 = tile_ix * 1024u; + uint param_2 = 1024u; + Alloc cmd_alloc = slice_mem(param, param_1, param_2); + CmdRef cmd_ref = CmdRef(cmd_alloc.offset); + Alloc param_3 = cmd_alloc; + uint param_4 = cmd_ref.offset; + Alloc scratch_alloc = alloc_read(param_3, param_4); + cmd_ref.offset += 8u; + uvec2 xy_uint = uvec2(gl_LocalInvocationID.x + (32u * gl_WorkGroupID.x), gl_LocalInvocationID.y + (32u * gl_WorkGroupID.y)); + vec2 xy = vec2(xy_uint); + vec4 rgba[8]; + for (uint i = 0u; i < 8u; i++) + { + rgba[i] = vec4(0.0); + } + uint clip_depth = 0u; + float df[8]; + TileSegRef tile_seg_ref; + float area[8]; + uint base_ix; + while (true) + { + Alloc param_5 = cmd_alloc; + CmdRef param_6 = cmd_ref; + uint tag = Cmd_tag(param_5, param_6).tag; + if (tag == 0u) + { + break; + } + switch (tag) + { + case 2u: + { + Alloc param_7 = cmd_alloc; + CmdRef param_8 = cmd_ref; + CmdStroke stroke = Cmd_Stroke_read(param_7, param_8); + for (uint k = 0u; k < 8u; k++) + { + df[k] = 1000000000.0; + } + tile_seg_ref = TileSegRef(stroke.tile_ref); + do + { + uint param_9 = tile_seg_ref.offset; + uint param_10 = 24u; + Alloc param_11 = new_alloc(param_9, param_10); + TileSegRef param_12 = tile_seg_ref; + TileSeg seg = TileSeg_read(param_11, param_12); + vec2 line_vec = seg.vector; + for (uint k_1 = 0u; k_1 < 8u; k_1++) + { + vec2 dpos = (xy + vec2(0.5)) - seg.origin; + uint param_13 = k_1; + dpos += vec2(chunk_offset(param_13)); + float t = clamp(dot(line_vec, dpos) / dot(line_vec, line_vec), 0.0, 1.0); + df[k_1] = min(df[k_1], length((line_vec * t) - dpos)); + } + tile_seg_ref = seg.next; + } while (tile_seg_ref.offset != 0u); + for (uint k_2 = 0u; k_2 < 8u; k_2++) + { + area[k_2] = clamp((stroke.half_width + 0.5) - df[k_2], 0.0, 1.0); + } + cmd_ref.offset += 12u; + break; + } + case 1u: + { + Alloc param_14 = cmd_alloc; + CmdRef param_15 = cmd_ref; + CmdFill fill = Cmd_Fill_read(param_14, param_15); + for (uint k_3 = 0u; k_3 < 8u; k_3++) + { + area[k_3] = float(fill.backdrop); + } + tile_seg_ref = TileSegRef(fill.tile_ref); + do + { + uint param_16 = tile_seg_ref.offset; + uint param_17 = 24u; + Alloc param_18 = new_alloc(param_16, param_17); + TileSegRef param_19 = tile_seg_ref; + TileSeg seg_1 = TileSeg_read(param_18, param_19); + for (uint k_4 = 0u; k_4 < 8u; k_4++) + { + uint param_20 = k_4; + vec2 my_xy = xy + vec2(chunk_offset(param_20)); + vec2 start = seg_1.origin - my_xy; + vec2 end = start + seg_1.vector; + vec2 window = clamp(vec2(start.y, end.y), vec2(0.0), vec2(1.0)); + if (!(window.x == window.y)) + { + vec2 t_1 = (window - vec2(start.y)) / vec2(seg_1.vector.y); + vec2 xs = vec2(mix(start.x, end.x, t_1.x), mix(start.x, end.x, t_1.y)); + float xmin = min(min(xs.x, xs.y), 1.0) - 9.9999999747524270787835121154785e-07; + float xmax = max(xs.x, xs.y); + float b = min(xmax, 1.0); + float c = max(b, 0.0); + float d = max(xmin, 0.0); + float a = ((b + (0.5 * ((d * d) - (c * c)))) - xmin) / (xmax - xmin); + area[k_4] += (a * (window.x - window.y)); + } + area[k_4] += (sign(seg_1.vector.x) * clamp((my_xy.y - seg_1.y_edge) + 1.0, 0.0, 1.0)); + } + tile_seg_ref = seg_1.next; + } while (tile_seg_ref.offset != 0u); + for (uint k_5 = 0u; k_5 < 8u; k_5++) + { + area[k_5] = min(abs(area[k_5]), 1.0); + } + cmd_ref.offset += 12u; + break; + } + case 3u: + { + for (uint k_6 = 0u; k_6 < 8u; k_6++) + { + area[k_6] = 1.0; + } + cmd_ref.offset += 4u; + break; + } + case 4u: + { + Alloc param_21 = cmd_alloc; + CmdRef param_22 = cmd_ref; + CmdAlpha alpha = Cmd_Alpha_read(param_21, param_22); + for (uint k_7 = 0u; k_7 < 8u; k_7++) + { + area[k_7] = alpha.alpha; + } + cmd_ref.offset += 8u; + break; + } + case 5u: + { + Alloc param_23 = cmd_alloc; + CmdRef param_24 = cmd_ref; + CmdColor color = Cmd_Color_read(param_23, param_24); + uint param_25 = color.rgba_color; + vec4 fg = unpacksRGB(param_25); + for (uint k_8 = 0u; k_8 < 8u; k_8++) + { + vec4 fg_k = fg * area[k_8]; + rgba[k_8] = (rgba[k_8] * (1.0 - fg_k.w)) + fg_k; + } + cmd_ref.offset += 8u; + break; + } + case 6u: + { + Alloc param_26 = cmd_alloc; + CmdRef param_27 = cmd_ref; + CmdImage fill_img = Cmd_Image_read(param_26, param_27); + uvec2 param_28 = xy_uint; + CmdImage param_29 = fill_img; + vec4 img[8] = fillImage(param_28, param_29); + for (uint k_9 = 0u; k_9 < 8u; k_9++) + { + vec4 fg_k_1 = img[k_9] * area[k_9]; + rgba[k_9] = (rgba[k_9] * (1.0 - fg_k_1.w)) + fg_k_1; + } + cmd_ref.offset += 12u; + break; + } + case 7u: + { + base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y))); + for (uint k_10 = 0u; k_10 < 8u; k_10++) + { + uint param_30 = k_10; + uvec2 offset = chunk_offset(param_30); + vec4 param_31 = vec4(rgba[k_10]); + uint _1286 = packsRGB(param_31); + uint srgb = _1286; + float alpha_1 = clamp(abs(area[k_10]), 0.0, 1.0); + Alloc param_32 = scratch_alloc; + uint param_33 = (base_ix + 0u) + (2u * (offset.x + (offset.y * 32u))); + uint param_34 = srgb; + write_mem(param_32, param_33, param_34); + Alloc param_35 = scratch_alloc; + uint param_36 = (base_ix + 1u) + (2u * (offset.x + (offset.y * 32u))); + uint param_37 = floatBitsToUint(alpha_1); + write_mem(param_35, param_36, param_37); + rgba[k_10] = vec4(0.0); + } + clip_depth++; + cmd_ref.offset += 4u; + break; + } + case 8u: + { + clip_depth--; + base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y))); + for (uint k_11 = 0u; k_11 < 8u; k_11++) + { + uint param_38 = k_11; + uvec2 offset_1 = chunk_offset(param_38); + Alloc param_39 = scratch_alloc; + uint param_40 = (base_ix + 0u) + (2u * (offset_1.x + (offset_1.y * 32u))); + uint srgb_1 = read_mem(param_39, param_40); + Alloc param_41 = scratch_alloc; + uint param_42 = (base_ix + 1u) + (2u * (offset_1.x + (offset_1.y * 32u))); + uint alpha_2 = read_mem(param_41, param_42); + uint param_43 = srgb_1; + vec4 bg = unpacksRGB(param_43); + vec4 fg_1 = (rgba[k_11] * area[k_11]) * uintBitsToFloat(alpha_2); + rgba[k_11] = (bg * (1.0 - fg_1.w)) + fg_1; + } + cmd_ref.offset += 4u; + break; + } + case 9u: + { + Alloc param_44 = cmd_alloc; + CmdRef param_45 = cmd_ref; + cmd_ref = CmdRef(Cmd_Jump_read(param_44, param_45).new_ref); + cmd_alloc.offset = cmd_ref.offset; + break; + } + } + } + for (uint i_1 = 0u; i_1 < 8u; i_1++) + { + uint param_46 = i_1; + vec3 param_47 = rgba[i_1].xyz; + imageStore(image, ivec2(xy_uint + chunk_offset(param_46)), vec4(tosRGB(param_47), rgba[i_1].w)); + } +} + +`, + } + shader_material_frag = driver.ShaderSources{ + Name: "material.frag", + Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, + Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +varying vec2 vUV; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture2D(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + gl_FragData[0] = texel; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +uniform mediump sampler2D tex; + +in vec2 vUV; +layout(location = 0) out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +in vec2 vUV; +out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0) uniform sampler2D tex; + +in vec2 vUV; +out vec4 fragColor; + +vec3 RGBtosRGB(vec3 rgb) +{ + bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375)); + vec3 below = vec3(12.9200000762939453125) * rgb; + vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875); + return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z); +} + +void main() +{ + vec4 texel = texture(tex, vUV); + vec3 param = texel.xyz; + vec3 _59 = RGBtosRGB(param); + texel = vec4(_59.x, _59.y, _59.z, texel.w); + fragColor = texel; +} + +`, + HLSL: "DXBC\x9e\x87LD\xf3\x17\n\x06\\\xb7\x98\x94\xa9PKe\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\xbc\x01\x00\x00D\x03\x00\x00\xc0\x03\x00\x00`\x04\x00\x00\x94\x04\x00\x00Aon9|\x01\x00\x00|\x01\x00\x00\x00\x02\xff\xffT\x01\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0=\n\x87?\xaeGa\xbd\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x05\x01\x00\x0f\xa0\x1c.M\xbbR\xb8NAvT\xd5>\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x0f\x00\x00\x02\x01\x00\x01\x80\x00\x00\x00\x80\x0f\x00\x00\x02\x01\x00\x02\x80\x00\x00U\x80\x0f\x00\x00\x02\x01\x00\x04\x80\x00\x00\xaa\x80\x05\x00\x00\x03\x01\x00\a\x80\x01\x00\xe4\x80\x01\x00\xaa\xa0\x0e\x00\x00\x02\x02\x00\x01\x80\x01\x00\x00\x80\x0e\x00\x00\x02\x02\x00\x02\x80\x01\x00U\x80\x0e\x00\x00\x02\x02\x00\x04\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\a\x80\x02\x00\xe4\x80\x00\x00\x00\xa0\x00\x00U\xa0\x02\x00\x00\x03\x01\x00\b\x80\x00\x00\x00\x80\x01\x00\x00\xa0\x05\x00\x00\x03\x02\x00\a\x80\x00\x00\xe4\x80\x01\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x01\x00\xff\x80\x01\x00\x00\x80\x02\x00\x00\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00U\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00U\x80\x02\x00U\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00\xaa\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x04\x80\x01\x00\x00\x80\x01\x00\xaa\x80\x02\x00\xaa\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\x80\x01\x00\x00@\x00\x00\x00`\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00vT\xd5>vT\xd5>vT\xd5>\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\x0fr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00=\n\x87?=\n\x87?=\n\x87?\x00\x00\x00\x00\x02@\x00\x00\xaeGa\xbd\xaeGa\xbd\xaeGa\xbd\x00\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x1c.M;\x1c.M;\x1c.M;\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00R\xb8NAR\xb8NAR\xb8NA\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_material_vert = driver.ShaderSources{ + Name: "material.vert", + Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, + GLSL100ES: `#version 100 + +varying vec2 vUV; +attribute vec2 uv; +attribute vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +out vec2 vUV; +layout(location = 1) in vec2 uv; +layout(location = 0) in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec2 vUV; +in vec2 uv; +in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +out vec2 vUV; +in vec2 uv; +in vec2 pos; + +void main() +{ + vUV = uv; + gl_Position = vec4(pos, 0.0, 1.0); +} + +`, + HLSL: "DXBCg\xc0\xae\x16\xd8\xe1\xbdl~ń\xf1\xc4\xf6dV\x01\x00\x00\x00\xc4\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xc8\x00\x00\x00X\x01\x00\x00\xd4\x01\x00\x00 \x02\x00\x00l\x02\x00\x00Aon9\x88\x00\x00\x00\x88\x00\x00\x00\x00\x02\xfe\xff`\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x01\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\x03\xe0\x01\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x01\x00D\xa0\xff\xff\x00\x00SHDR\x88\x00\x00\x00@\x00\x01\x00\"\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_path_coarse_comp = driver.ShaderSources{ + Name: "path_coarse.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct PathCubicRef +{ + uint offset; +}; + +struct PathCubic +{ + vec2 p0; + vec2 p1; + vec2 p2; + vec2 p3; + uint path_ix; + uint trans_ix; + vec2 stroke; +}; + +struct PathSegRef +{ + uint offset; +}; + +struct PathSegTag +{ + uint tag; + uint flags; +}; + +struct TileRef +{ + uint offset; +}; + +struct PathRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct TileSegRef +{ + uint offset; +}; + +struct TileSeg +{ + vec2 origin; + vec2 vector; + float y_edge; + TileSegRef next; +}; + +struct TransformSegRef +{ + uint offset; +}; + +struct TransformSeg +{ + vec4 mat; + vec2 translate; +}; + +struct SubdivResult +{ + float val; + float a0; + float a2; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _149; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _788; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _149.memory[offset]; + return v; +} + +PathSegTag PathSeg_tag(Alloc a, PathSegRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return PathSegTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +PathCubic PathCubic_read(Alloc a, PathCubicRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 6u; + uint raw6 = read_mem(param_12, param_13); + Alloc param_14 = a; + uint param_15 = ix + 7u; + uint raw7 = read_mem(param_14, param_15); + Alloc param_16 = a; + uint param_17 = ix + 8u; + uint raw8 = read_mem(param_16, param_17); + Alloc param_18 = a; + uint param_19 = ix + 9u; + uint raw9 = read_mem(param_18, param_19); + Alloc param_20 = a; + uint param_21 = ix + 10u; + uint raw10 = read_mem(param_20, param_21); + Alloc param_22 = a; + uint param_23 = ix + 11u; + uint raw11 = read_mem(param_22, param_23); + PathCubic s; + s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1)); + s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7)); + s.path_ix = raw8; + s.trans_ix = raw9; + s.stroke = vec2(uintBitsToFloat(raw10), uintBitsToFloat(raw11)); + return s; +} + +PathCubic PathSeg_Cubic_read(Alloc a, PathSegRef ref) +{ + Alloc param = a; + PathCubicRef param_1 = PathCubicRef(ref.offset + 4u); + return PathCubic_read(param, param_1); +} + +TransformSeg TransformSeg_read(Alloc a, TransformSegRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + Alloc param_8 = a; + uint param_9 = ix + 4u; + uint raw4 = read_mem(param_8, param_9); + Alloc param_10 = a; + uint param_11 = ix + 5u; + uint raw5 = read_mem(param_10, param_11); + TransformSeg s; + s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5)); + return s; +} + +vec2 eval_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t) +{ + float mt = 1.0 - t; + return (p0 * ((mt * mt) * mt)) + (((p1 * ((mt * mt) * 3.0)) + (((p2 * (mt * 3.0)) + (p3 * t)) * t)) * t); +} + +float approx_parabola_integral(float x) +{ + return x * inversesqrt(sqrt(0.3300000131130218505859375 + (0.201511204242706298828125 + ((0.25 * x) * x)))); +} + +SubdivResult estimate_subdiv(vec2 p0, vec2 p1, vec2 p2, float sqrt_tol) +{ + vec2 d01 = p1 - p0; + vec2 d12 = p2 - p1; + vec2 dd = d01 - d12; + float _cross = ((p2.x - p0.x) * dd.y) - ((p2.y - p0.y) * dd.x); + float x0 = ((d01.x * dd.x) + (d01.y * dd.y)) / _cross; + float x2 = ((d12.x * dd.x) + (d12.y * dd.y)) / _cross; + float scale = abs(_cross / (length(dd) * (x2 - x0))); + float param = x0; + float a0 = approx_parabola_integral(param); + float param_1 = x2; + float a2 = approx_parabola_integral(param_1); + float val = 0.0; + if (scale < 1000000000.0) + { + float da = abs(a2 - a0); + float sqrt_scale = sqrt(scale); + if (sign(x0) == sign(x2)) + { + val = da * sqrt_scale; + } + else + { + float xmin = sqrt_tol / sqrt_scale; + float param_2 = xmin; + val = (sqrt_tol * da) / approx_parabola_integral(param_2); + } + } + return SubdivResult(val, a0, a2); +} + +uint fill_mode_from_flags(uint flags) +{ + return flags & 1u; +} + +Path Path_read(Alloc a, PathRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Path s; + s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16)); + s.tiles = TileRef(raw2); + return s; +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +float approx_parabola_inv_integral(float x) +{ + return x * sqrt(0.61000001430511474609375 + (0.1520999968051910400390625 + ((0.25 * x) * x))); +} + +vec2 eval_quad(vec2 p0, vec2 p1, vec2 p2, float t) +{ + float mt = 1.0 - t; + return (p0 * (mt * mt)) + (((p1 * (mt * 2.0)) + (p2 * t)) * t); +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _155 = atomicAdd(_149.mem_offset, size); + uint offset = _155; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_149.memory.length())) * 4)) + { + r.failed = true; + uint _176 = atomicMax(_149.mem_error, 1u); + return r; + } + return r; +} + +TileRef Tile_index(TileRef ref, uint index) +{ + return TileRef(ref.offset + (index * 8u)); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _149.memory[offset] = val; +} + +void TileSeg_write(Alloc a, TileSegRef ref, TileSeg s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = floatBitsToUint(s.origin.x); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = floatBitsToUint(s.origin.y); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = floatBitsToUint(s.vector.x); + write_mem(param_6, param_7, param_8); + Alloc param_9 = a; + uint param_10 = ix + 3u; + uint param_11 = floatBitsToUint(s.vector.y); + write_mem(param_9, param_10, param_11); + Alloc param_12 = a; + uint param_13 = ix + 4u; + uint param_14 = floatBitsToUint(s.y_edge); + write_mem(param_12, param_13, param_14); + Alloc param_15 = a; + uint param_16 = ix + 5u; + uint param_17 = s.next.offset; + write_mem(param_15, param_16, param_17); +} + +void main() +{ + if (_149.mem_error != 0u) + { + return; + } + uint element_ix = gl_GlobalInvocationID.x; + PathSegRef ref = PathSegRef(_788.conf.pathseg_alloc.offset + (element_ix * 52u)); + PathSegTag tag = PathSegTag(0u, 0u); + if (element_ix < _788.conf.n_pathseg) + { + Alloc param; + param.offset = _788.conf.pathseg_alloc.offset; + PathSegRef param_1 = ref; + tag = PathSeg_tag(param, param_1); + } + switch (tag.tag) + { + case 1u: + { + Alloc param_2; + param_2.offset = _788.conf.pathseg_alloc.offset; + PathSegRef param_3 = ref; + PathCubic cubic = PathSeg_Cubic_read(param_2, param_3); + uint trans_ix = cubic.trans_ix; + if (trans_ix > 0u) + { + TransformSegRef trans_ref = TransformSegRef(_788.conf.trans_alloc.offset + ((trans_ix - 1u) * 24u)); + Alloc param_4; + param_4.offset = _788.conf.trans_alloc.offset; + TransformSegRef param_5 = trans_ref; + TransformSeg trans = TransformSeg_read(param_4, param_5); + cubic.p0 = ((trans.mat.xy * cubic.p0.x) + (trans.mat.zw * cubic.p0.y)) + trans.translate; + cubic.p1 = ((trans.mat.xy * cubic.p1.x) + (trans.mat.zw * cubic.p1.y)) + trans.translate; + cubic.p2 = ((trans.mat.xy * cubic.p2.x) + (trans.mat.zw * cubic.p2.y)) + trans.translate; + cubic.p3 = ((trans.mat.xy * cubic.p3.x) + (trans.mat.zw * cubic.p3.y)) + trans.translate; + } + vec2 err_v = (((cubic.p2 - cubic.p1) * 3.0) + cubic.p0) - cubic.p3; + float err = (err_v.x * err_v.x) + (err_v.y * err_v.y); + uint n_quads = max(uint(ceil(pow(err * 3.7037036418914794921875, 0.16666667163372039794921875))), 1u); + float val = 0.0; + vec2 qp0 = cubic.p0; + float _step = 1.0 / float(n_quads); + for (uint i = 0u; i < n_quads; i++) + { + float t = float(i + 1u) * _step; + vec2 param_6 = cubic.p0; + vec2 param_7 = cubic.p1; + vec2 param_8 = cubic.p2; + vec2 param_9 = cubic.p3; + float param_10 = t; + vec2 qp2 = eval_cubic(param_6, param_7, param_8, param_9, param_10); + vec2 param_11 = cubic.p0; + vec2 param_12 = cubic.p1; + vec2 param_13 = cubic.p2; + vec2 param_14 = cubic.p3; + float param_15 = t - (0.5 * _step); + vec2 qp1 = eval_cubic(param_11, param_12, param_13, param_14, param_15); + qp1 = (qp1 * 2.0) - ((qp0 + qp2) * 0.5); + vec2 param_16 = qp0; + vec2 param_17 = qp1; + vec2 param_18 = qp2; + float param_19 = 0.4743416607379913330078125; + SubdivResult params = estimate_subdiv(param_16, param_17, param_18, param_19); + val += params.val; + qp0 = qp2; + } + uint n = max(uint(ceil((val * 0.5) / 0.4743416607379913330078125)), 1u); + uint param_20 = tag.flags; + bool is_stroke = fill_mode_from_flags(param_20) == 1u; + uint path_ix = cubic.path_ix; + Alloc param_21; + param_21.offset = _788.conf.tile_alloc.offset; + PathRef param_22 = PathRef(_788.conf.tile_alloc.offset + (path_ix * 12u)); + Path path = Path_read(param_21, param_22); + uint param_23 = path.tiles.offset; + uint param_24 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u; + Alloc path_alloc = new_alloc(param_23, param_24); + ivec4 bbox = ivec4(path.bbox); + vec2 p0 = cubic.p0; + qp0 = cubic.p0; + float v_step = val / float(n); + int n_out = 1; + float val_sum = 0.0; + vec2 p1; + float _1309; + TileSeg tile_seg; + for (uint i_1 = 0u; i_1 < n_quads; i_1++) + { + float t_1 = float(i_1 + 1u) * _step; + vec2 param_25 = cubic.p0; + vec2 param_26 = cubic.p1; + vec2 param_27 = cubic.p2; + vec2 param_28 = cubic.p3; + float param_29 = t_1; + vec2 qp2_1 = eval_cubic(param_25, param_26, param_27, param_28, param_29); + vec2 param_30 = cubic.p0; + vec2 param_31 = cubic.p1; + vec2 param_32 = cubic.p2; + vec2 param_33 = cubic.p3; + float param_34 = t_1 - (0.5 * _step); + vec2 qp1_1 = eval_cubic(param_30, param_31, param_32, param_33, param_34); + qp1_1 = (qp1_1 * 2.0) - ((qp0 + qp2_1) * 0.5); + vec2 param_35 = qp0; + vec2 param_36 = qp1_1; + vec2 param_37 = qp2_1; + float param_38 = 0.4743416607379913330078125; + SubdivResult params_1 = estimate_subdiv(param_35, param_36, param_37, param_38); + float param_39 = params_1.a0; + float u0 = approx_parabola_inv_integral(param_39); + float param_40 = params_1.a2; + float u2 = approx_parabola_inv_integral(param_40); + float uscale = 1.0 / (u2 - u0); + float target = float(n_out) * v_step; + for (;;) + { + bool _1202 = uint(n_out) == n; + bool _1212; + if (!_1202) + { + _1212 = target < (val_sum + params_1.val); + } + else + { + _1212 = _1202; + } + if (_1212) + { + if (uint(n_out) == n) + { + p1 = cubic.p3; + } + else + { + float u = (target - val_sum) / params_1.val; + float a = mix(params_1.a0, params_1.a2, u); + float param_41 = a; + float au = approx_parabola_inv_integral(param_41); + float t_2 = (au - u0) * uscale; + vec2 param_42 = qp0; + vec2 param_43 = qp1_1; + vec2 param_44 = qp2_1; + float param_45 = t_2; + p1 = eval_quad(param_42, param_43, param_44, param_45); + } + float xmin = min(p0.x, p1.x) - cubic.stroke.x; + float xmax = max(p0.x, p1.x) + cubic.stroke.x; + float ymin = min(p0.y, p1.y) - cubic.stroke.y; + float ymax = max(p0.y, p1.y) + cubic.stroke.y; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + if (abs(dy) < 9.999999717180685365747194737196e-10) + { + _1309 = 1000000000.0; + } + else + { + _1309 = dx / dy; + } + float invslope = _1309; + float c = (cubic.stroke.x + (abs(invslope) * (16.0 + cubic.stroke.y))) * 0.03125; + float b = invslope; + float a_1 = (p0.x - ((p0.y - 16.0) * b)) * 0.03125; + int x0 = int(floor(xmin * 0.03125)); + int x1 = int(floor(xmax * 0.03125) + 1.0); + int y0 = int(floor(ymin * 0.03125)); + int y1 = int(floor(ymax * 0.03125) + 1.0); + x0 = clamp(x0, bbox.x, bbox.z); + y0 = clamp(y0, bbox.y, bbox.w); + x1 = clamp(x1, bbox.x, bbox.z); + y1 = clamp(y1, bbox.y, bbox.w); + float xc = a_1 + (b * float(y0)); + int stride = bbox.z - bbox.x; + int base = ((y0 - bbox.y) * stride) - bbox.x; + uint n_tile_alloc = uint((x1 - x0) * (y1 - y0)); + uint param_46 = n_tile_alloc * 24u; + MallocResult _1424 = malloc(param_46); + MallocResult tile_alloc = _1424; + if (tile_alloc.failed) + { + return; + } + uint tile_offset = tile_alloc.alloc.offset; + int xray = int(floor(p0.x * 0.03125)); + int last_xray = int(floor(p1.x * 0.03125)); + if (p0.y > p1.y) + { + int tmp = xray; + xray = last_xray; + last_xray = tmp; + } + for (int y = y0; y < y1; y++) + { + float tile_y0 = float(y * 32); + int xbackdrop = max((xray + 1), bbox.x); + bool _1478 = !is_stroke; + bool _1488; + if (_1478) + { + _1488 = min(p0.y, p1.y) < tile_y0; + } + else + { + _1488 = _1478; + } + bool _1495; + if (_1488) + { + _1495 = xbackdrop < bbox.z; + } + else + { + _1495 = _1488; + } + if (_1495) + { + int backdrop = (p1.y < p0.y) ? 1 : (-1); + TileRef param_47 = path.tiles; + uint param_48 = uint(base + xbackdrop); + TileRef tile_ref = Tile_index(param_47, param_48); + uint tile_el = tile_ref.offset >> uint(2); + Alloc param_49 = path_alloc; + uint param_50 = tile_el + 1u; + if (touch_mem(param_49, param_50)) + { + uint _1533 = atomicAdd(_149.memory[tile_el + 1u], uint(backdrop)); + } + } + int next_xray = last_xray; + if (y < (y1 - 1)) + { + float tile_y1 = float((y + 1) * 32); + float x_edge = mix(p0.x, p1.x, (tile_y1 - p0.y) / dy); + next_xray = int(floor(x_edge * 0.03125)); + } + int min_xray = min(xray, next_xray); + int max_xray = max(xray, next_xray); + int xx0 = min(int(floor(xc - c)), min_xray); + int xx1 = max(int(ceil(xc + c)), (max_xray + 1)); + xx0 = clamp(xx0, x0, x1); + xx1 = clamp(xx1, x0, x1); + for (int x = xx0; x < xx1; x++) + { + float tile_x0 = float(x * 32); + TileRef param_51 = TileRef(path.tiles.offset); + uint param_52 = uint(base + x); + TileRef tile_ref_1 = Tile_index(param_51, param_52); + uint tile_el_1 = tile_ref_1.offset >> uint(2); + uint old = 0u; + Alloc param_53 = path_alloc; + uint param_54 = tile_el_1; + if (touch_mem(param_53, param_54)) + { + uint _1636 = atomicExchange(_149.memory[tile_el_1], tile_offset); + old = _1636; + } + tile_seg.origin = p0; + tile_seg.vector = p1 - p0; + float y_edge = 0.0; + if (!is_stroke) + { + y_edge = mix(p0.y, p1.y, (tile_x0 - p0.x) / dx); + if (min(p0.x, p1.x) < tile_x0) + { + vec2 p = vec2(tile_x0, y_edge); + if (p0.x > p1.x) + { + tile_seg.vector = p - p0; + } + else + { + tile_seg.origin = p; + tile_seg.vector = p1 - p; + } + if (tile_seg.vector.x == 0.0) + { + tile_seg.vector.x = sign(p1.x - p0.x) * 9.999999717180685365747194737196e-10; + } + } + if ((x <= min_xray) || (max_xray < x)) + { + y_edge = 1000000000.0; + } + } + tile_seg.y_edge = y_edge; + tile_seg.next.offset = old; + Alloc param_55 = tile_alloc.alloc; + TileSegRef param_56 = TileSegRef(tile_offset); + TileSeg param_57 = tile_seg; + TileSeg_write(param_55, param_56, param_57); + tile_offset += 24u; + } + xc += b; + base += stride; + xray = next_xray; + } + n_out++; + target += v_step; + p0 = p1; + continue; + } + else + { + break; + } + } + val_sum += params_1.val; + qp0 = qp2_1; + } + break; + } + } +} + +`, + } + shader_stencil_frag = driver.ShaderSources{ + Name: "stencil.frag", + Inputs: []driver.InputLocation{{Name: "vFrom", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vCtrl", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "vTo", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}}, + GLSL100ES: `#version 100 +precision mediump float; +precision highp int; + +varying vec2 vTo; +varying vec2 vFrom; +varying vec2 vCtrl; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + gl_FragData[0].x = area; +} + +`, + GLSL300ES: `#version 300 es +precision mediump float; +precision highp int; + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +layout(location = 0) out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +in vec2 vTo; +in vec2 vFrom; +in vec2 vCtrl; +out vec4 fragCover; + +void main() +{ + float dx = vTo.x - vFrom.x; + bool increasing = vTo.x >= vFrom.x; + bvec2 _35 = bvec2(increasing); + vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y); + bvec2 _41 = bvec2(increasing); + vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y); + vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5)); + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0))); + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + vec2 d_half = mix(p1, v, vec2(t)); + float dy = d_half.y / d_half.x; + float width = extent.y - extent.x; + dy = abs(dy * width); + vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy); + sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0)); + float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w)); + area *= width; + if (width == 0.0) + { + area = 0.0; + } + fragCover.x = area; +} + +`, + HLSL: "DXBC\x94!\xb9\x13L\xba\r\x11\x8f\xc7\xce\x0eAs\xec\xe1\x01\x00\x00\x00\\\n\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x03\x00\x00\xfc\b\x00\x00x\t\x00\x00\xc4\t\x00\x00(\n\x00\x00Aon9\\\x03\x00\x00\\\x03\x00\x00\x00\x02\xff\xff8\x03\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\xbf\x00\x00\x00?\x00\x00\x80?\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x80\x01\x00\x03\xb0\v\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\xb0\x00\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\xb0\x00\x00\x00\xa0\n\x00\x00\x03\x01\x00\x03\x80\x00\x00\xe4\x80\x00\x00U\xa0\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x81\x01\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\xa0\x01\x00\x00\x80\x01\x00\x00\x02\x01\x00\x03\x80\x00\x00\xe4\xb0\n\x00\x00\x03\x02\x00\x01\x80\x01\x00\x00\x80\x01\x00\x00\xb0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x81\v\x00\x00\x03\x03\x00\x01\x80\x01\x00\x00\xb0\x01\x00\x00\x80\x02\x00\x00\x03\x00\x00\x04\x80\x01\x00\x00\x81\x01\x00\x00\xb0X\x00\x00\x04\x03\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\xb0\x01\x00U\x80X\x00\x00\x04\x02\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\x80\x01\x00U\xb0\x02\x00\x00\x03\x00\x00\f\x80\x03\x00\x1b\x80\x00\x00\xe4\xb1\x02\x00\x00\x03\x01\x00\x03\x80\x02\x00\xe4\x81\x00\x00\x1b\xb0\x02\x00\x00\x03\x01\x00\x04\x80\x00\x00\xff\x80\x01\x00\x00\x81\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x01\x00\x00\x80\x01\x00\x00\x80\x01\x00\xaa\x80\a\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x04\x80\x01\x00\xaa\x80\x01\x00\x00\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x00\x00U\x80\x01\x00U\x80\x02\x00U\x80\x12\x00\x00\x04\x02\x00\x03\x80\x00\x00U\x80\x00\x00\x1b\x80\x01\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x00\x00\xaa\xb0\x12\x00\x00\x04\x02\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x02\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\x80#\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x04\x00\x00\x04\x01\x00\x01\x80\x00\x00U\x80\x00\x00U\xa0\x02\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x02\x80\x00\x00U\x80\x00\x00\x00\xa0\x02\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\f\x80\x02\x00\xaa\x81\x00\x00\x1b\xa0\x05\x00\x00\x03\x01\x00\b\x80\x00\x00U\x80\x00\x00\xff\x80\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x1f\x80\x01\x00\xe4\x80\x00\x00U\xa0\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\xaa\x80\x01\x00U\x81\x01\x00\xaa\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\xaa\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\x81\x00\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00\xff\x80\x00\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x00\x00\x00\x81\x00\x00\xff\xa0\x00\x00U\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\xff\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRX\x05\x00\x00@\x00\x00\x00V\x01\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x004\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\"\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\n\x00\x10\x00\x00\x00\x00\x003\x00\x00\a2\x00\x10\x00\x01\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x004\x00\x00\a2\x00\x10\x00\x02\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x1d\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x02\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\br\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00\xa6\x1b\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00V\t\x10\x80A\x00\x00\x00\x01\x00\x00\x00\xa6\x1e\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\xb2\x00\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\b\x10\x00\x02\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00K\x00\x00\x05\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x0e\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\xc2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\r\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x00\x00\x00\x00\x00\x0e\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x008\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00:\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\v2\x00\x10\x00\x01\x00\x00\x00\x06\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\r2\x00\x10\x00\x02\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x0e\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x04\x10\x00\x01\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x00 \x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?2\x00\x00\n\x12\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00:\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x18\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?7\x00\x00\t\x12 \x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00)\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\\\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00P\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab", + } + shader_stencil_vert = driver.ShaderSources{ + Name: "stencil.vert", + Inputs: []driver.InputLocation{{Name: "corner", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 3, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 4, Type: 0x0, Size: 2}}, + Uniforms: driver.UniformsReflection{ + Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}}, + Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.pathOffset", Type: 0x0, Size: 2, Offset: 16}}, + Size: 24, + }, + GLSL100ES: `#version 100 + +struct Block +{ + vec4 transform; + vec2 pathOffset; +}; + +uniform Block _block; + +attribute vec2 from; +attribute vec2 ctrl; +attribute vec2 to; +attribute float maxy; +attribute float corner; +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL300ES: `#version 300 es + +layout(std140) uniform Block +{ + vec4 transform; + vec2 pathOffset; +} _block; + +layout(location = 2) in vec2 from; +layout(location = 3) in vec2 ctrl; +layout(location = 4) in vec2 to; +layout(location = 1) in float maxy; +layout(location = 0) in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL130: `#version 130 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +struct Block +{ + vec4 transform; + vec2 pathOffset; +}; + +uniform Block _block; + +in vec2 from; +in vec2 ctrl; +in vec2 to; +in float maxy; +in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + GLSL150: `#version 150 +#ifdef GL_ARB_shading_language_420pack +#extension GL_ARB_shading_language_420pack : require +#endif + +layout(binding = 0, std140) uniform Block +{ + vec4 transform; + vec2 pathOffset; +} _block; + +in vec2 from; +in vec2 ctrl; +in vec2 to; +in float maxy; +in float corner; +out vec2 vFrom; +out vec2 vCtrl; +out vec2 vTo; + +void main() +{ + vec2 from_1 = from + _block.pathOffset; + vec2 ctrl_1 = ctrl + _block.pathOffset; + vec2 to_1 = to + _block.pathOffset; + float maxy_1 = maxy + _block.pathOffset.y; + float c = corner; + vec2 pos; + if (c >= 0.375) + { + c -= 0.5; + pos.y = maxy_1 + 1.0; + } + else + { + pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0; + } + if (c >= 0.125) + { + pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0; + } + else + { + pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0; + } + vFrom = from_1 - pos; + vCtrl = ctrl_1 - pos; + vTo = to_1 - pos; + pos = (pos * _block.transform.xy) + _block.transform.zw; + gl_Position = vec4(pos, 1.0, 1.0); +} + +`, + HLSL: "DXBC\xa5!\xd8\x10\xb4n\x90\xe3\xd9U\xdb\xe2\xb6~I0\x01\x00\x00\x00\x10\b\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x02\x00\x00t\x05\x00\x00\xf0\x05\x00\x00\xf4\x06\x00\x00\x88\a\x00\x00Aon9\f\x02\x00\x00\f\x02\x00\x00\x00\x02\xfe\xff\xd8\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\xc0>\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x00\xbfQ\x00\x00\x05\x04\x00\x0f\xa0\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x02\x80\x02\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x03\x80\x03\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x04\x80\x04\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x90\x02\x00U\xa0\x02\x00\x00\x03\x00\x00\x04\x80\x00\x00\x00\x80\x03\x00U\xa0\r\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x90\x03\x00\x00\xa0\x01\x00\x00\x02\x01\x00\x04\x80\x00\x00\x00\x90\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x90\x03\x00\xff\xa0\x02\x00\x00\x03\x02\x00\x03\x80\x02\x00\xe4\x90\x02\x00\xe4\xa0\x02\x00\x00\x03\x02\x00\f\x80\x03\x00\x14\x90\x02\x00\x14\xa0\n\x00\x00\x03\x03\x00\x03\x80\x02\x00\xee\x80\x02\x00\xe1\x80\x02\x00\x00\x03\x03\x00\f\x80\x04\x00D\x90\x02\x00D\xa0\n\x00\x00\x03\x03\x00\x03\x80\x03\x00\xeb\x80\x03\x00\xe4\x80\x02\x00\x00\x03\x01\x00\x03\x80\x03\x00\xe4\x80\x03\x00\xaa\xa0\x12\x00\x00\x04\x04\x00\x06\x80\x00\x00\x00\x80\x00\x00\xe4\x80\x01\x00Ȁ\r\x00\x00\x03\x00\x00\x01\x80\x04\x00U\x80\x04\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x02\x00\xff\x80\x02\x00\x00\x80\v\x00\x00\x03\x00\x00\x02\x80\x03\x00\xaa\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x03\x00U\xa0\x12\x00\x00\x04\x04\x00\x01\x80\x00\x00\x00\x80\x00\x00U\x80\x01\x00U\x80\x02\x00\x00\x03\x00\x00\x0f\xe0\x02\x00\xe4\x80\x04\x00(\x81\x02\x00\x00\x03\x01\x00\x03\xe0\x03\x00\xee\x80\x04\x00\xe8\x81\x04\x00\x00\x04\x00\x00\x03\x80\x04\x00\xe8\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00U\xa0\xff\xff\x00\x00SHDR \x03\x00\x00@\x00\x01\x00\xc8\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x01\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x03\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x04\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00e\x00\x00\x032 \x10\x00\x01\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x02\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\x1a\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x1d\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\xc0>\x00\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\xbf6\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b2\x00\x10\x00\x02\x00\x00\x00F\x10\x10\x00\x02\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x14\x10\x00\x03\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x02\x00\x00\x00\x16\x05\x10\x00\x02\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x03\x00\x00\x00\x06\x14\x10\x00\x04\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x03\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\n2\x00\x10\x00\x01\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\tb\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\x06\x10\x00\x00\x00\x00\x00\xa6\b\x10\x00\x01\x00\x00\x00\x1d\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00>4\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00\n\x00\x10\x00\x02\x00\x00\x004\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x03\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?7\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x86\b\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x00\x00\x00\b2 \x10\x00\x01\x00\x00\x00\x86\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\xe6\n\x10\x00\x03\x00\x00\x002\x00\x00\v2 \x10\x00\x02\x00\x00\x00\x86\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xfc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xd4\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\b\x00\x00\x00\x02\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_pathOffset\x00\xab\xab\x01\x00\x03\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\x8c\x00\x00\x00\x05\x00\x00\x00\b\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN\x80\x00\x00\x00\x04\x00\x00\x00\b\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00h\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00h\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\f\x00\x00q\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab", + } + shader_tile_alloc_comp = driver.ShaderSources{ + Name: "tile_alloc.comp", + GLSL310ES: `#version 310 es +layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in; + +struct Alloc +{ + uint offset; +}; + +struct MallocResult +{ + Alloc alloc; + bool failed; +}; + +struct AnnoEndClipRef +{ + uint offset; +}; + +struct AnnoEndClip +{ + vec4 bbox; +}; + +struct AnnotatedRef +{ + uint offset; +}; + +struct AnnotatedTag +{ + uint tag; + uint flags; +}; + +struct PathRef +{ + uint offset; +}; + +struct TileRef +{ + uint offset; +}; + +struct Path +{ + uvec4 bbox; + TileRef tiles; +}; + +struct Config +{ + uint n_elements; + uint n_pathseg; + uint width_in_tiles; + uint height_in_tiles; + Alloc tile_alloc; + Alloc bin_alloc; + Alloc ptcl_alloc; + Alloc pathseg_alloc; + Alloc anno_alloc; + Alloc trans_alloc; +}; + +layout(binding = 0, std430) buffer Memory +{ + uint mem_offset; + uint mem_error; + uint memory[]; +} _96; + +layout(binding = 1, std430) readonly buffer ConfigBuf +{ + Config conf; +} _309; + +shared uint sh_tile_count[128]; +shared MallocResult sh_tile_alloc; + +bool touch_mem(Alloc alloc, uint offset) +{ + return true; +} + +uint read_mem(Alloc alloc, uint offset) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return 0u; + } + uint v = _96.memory[offset]; + return v; +} + +AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + uint param_1 = ref.offset >> uint(2); + uint tag_and_flags = read_mem(param, param_1); + return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16)); +} + +AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint raw0 = read_mem(param, param_1); + Alloc param_2 = a; + uint param_3 = ix + 1u; + uint raw1 = read_mem(param_2, param_3); + Alloc param_4 = a; + uint param_5 = ix + 2u; + uint raw2 = read_mem(param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 3u; + uint raw3 = read_mem(param_6, param_7); + AnnoEndClip s; + s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3)); + return s; +} + +AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref) +{ + Alloc param = a; + AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u); + return AnnoEndClip_read(param, param_1); +} + +Alloc new_alloc(uint offset, uint size) +{ + Alloc a; + a.offset = offset; + return a; +} + +MallocResult malloc(uint size) +{ + MallocResult r; + r.failed = false; + uint _102 = atomicAdd(_96.mem_offset, size); + uint offset = _102; + uint param = offset; + uint param_1 = size; + r.alloc = new_alloc(param, param_1); + if ((offset + size) > uint(int(uint(_96.memory.length())) * 4)) + { + r.failed = true; + uint _123 = atomicMax(_96.mem_error, 1u); + return r; + } + return r; +} + +Alloc slice_mem(Alloc a, uint offset, uint size) +{ + uint param = a.offset + offset; + uint param_1 = size; + return new_alloc(param, param_1); +} + +void write_mem(Alloc alloc, uint offset, uint val) +{ + Alloc param = alloc; + uint param_1 = offset; + if (!touch_mem(param, param_1)) + { + return; + } + _96.memory[offset] = val; +} + +void Path_write(Alloc a, PathRef ref, Path s) +{ + uint ix = ref.offset >> uint(2); + Alloc param = a; + uint param_1 = ix + 0u; + uint param_2 = s.bbox.x | (s.bbox.y << uint(16)); + write_mem(param, param_1, param_2); + Alloc param_3 = a; + uint param_4 = ix + 1u; + uint param_5 = s.bbox.z | (s.bbox.w << uint(16)); + write_mem(param_3, param_4, param_5); + Alloc param_6 = a; + uint param_7 = ix + 2u; + uint param_8 = s.tiles.offset; + write_mem(param_6, param_7, param_8); +} + +void main() +{ + if (_96.mem_error != 0u) + { + return; + } + uint th_ix = gl_LocalInvocationID.x; + uint element_ix = gl_GlobalInvocationID.x; + PathRef path_ref = PathRef(_309.conf.tile_alloc.offset + (element_ix * 12u)); + AnnotatedRef ref = AnnotatedRef(_309.conf.anno_alloc.offset + (element_ix * 32u)); + uint tag = 0u; + if (element_ix < _309.conf.n_elements) + { + Alloc param; + param.offset = _309.conf.anno_alloc.offset; + AnnotatedRef param_1 = ref; + tag = Annotated_tag(param, param_1).tag; + } + int x0 = 0; + int y0 = 0; + int x1 = 0; + int y1 = 0; + switch (tag) + { + case 1u: + case 2u: + case 3u: + case 4u: + { + Alloc param_2; + param_2.offset = _309.conf.anno_alloc.offset; + AnnotatedRef param_3 = ref; + AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3); + x0 = int(floor(clip.bbox.x * 0.03125)); + y0 = int(floor(clip.bbox.y * 0.03125)); + x1 = int(ceil(clip.bbox.z * 0.03125)); + y1 = int(ceil(clip.bbox.w * 0.03125)); + break; + } + } + x0 = clamp(x0, 0, int(_309.conf.width_in_tiles)); + y0 = clamp(y0, 0, int(_309.conf.height_in_tiles)); + x1 = clamp(x1, 0, int(_309.conf.width_in_tiles)); + y1 = clamp(y1, 0, int(_309.conf.height_in_tiles)); + Path path; + path.bbox = uvec4(uint(x0), uint(y0), uint(x1), uint(y1)); + uint tile_count = uint((x1 - x0) * (y1 - y0)); + if (tag == 4u) + { + tile_count = 0u; + } + sh_tile_count[th_ix] = tile_count; + uint total_tile_count = tile_count; + for (uint i = 0u; i < 7u; i++) + { + barrier(); + if (th_ix >= uint(1 << int(i))) + { + total_tile_count += sh_tile_count[th_ix - uint(1 << int(i))]; + } + barrier(); + sh_tile_count[th_ix] = total_tile_count; + } + if (th_ix == 127u) + { + uint param_4 = total_tile_count * 8u; + MallocResult _482 = malloc(param_4); + sh_tile_alloc = _482; + } + barrier(); + MallocResult alloc_start = sh_tile_alloc; + if (alloc_start.failed) + { + return; + } + if (element_ix < _309.conf.n_elements) + { + uint _499; + if (th_ix > 0u) + { + _499 = sh_tile_count[th_ix - 1u]; + } + else + { + _499 = 0u; + } + uint tile_subix = _499; + Alloc param_5 = alloc_start.alloc; + uint param_6 = 8u * tile_subix; + uint param_7 = 8u * tile_count; + Alloc tiles_alloc = slice_mem(param_5, param_6, param_7); + path.tiles = TileRef(tiles_alloc.offset); + Alloc param_8; + param_8.offset = _309.conf.tile_alloc.offset; + PathRef param_9 = path_ref; + Path param_10 = path; + Path_write(param_8, param_9, param_10); + } + uint total_count = sh_tile_count[127] * 2u; + uint start_ix = alloc_start.alloc.offset >> uint(2); + for (uint i_1 = th_ix; i_1 < total_count; i_1 += 128u) + { + Alloc param_11 = alloc_start.alloc; + uint param_12 = start_ix + i_1; + uint param_13 = 0u; + write_mem(param_11, param_12, param_13); + } +} + +`, + } +) diff --git a/pkg/gel/gio/gpu/timer.go b/pkg/gel/gio/gpu/timer.go new file mode 100644 index 0000000..e266aeb --- /dev/null +++ b/pkg/gel/gio/gpu/timer.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "time" + + "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" +) + +type timers struct { + backend driver.Device + timers []*timer +} + +type timer struct { + Elapsed time.Duration + backend driver.Device + timer driver.Timer + state timerState +} + +type timerState uint8 + +const ( + timerIdle timerState = iota + timerRunning + timerWaiting +) + +func newTimers(b driver.Device) *timers { + return &timers{ + backend: b, + } +} + +func (t *timers) newTimer() *timer { + if t == nil { + return nil + } + tt := &timer{ + backend: t.backend, + timer: t.backend.NewTimer(), + } + t.timers = append(t.timers, tt) + return tt +} + +func (t *timer) begin() { + if t == nil || t.state != timerIdle { + return + } + t.timer.Begin() + t.state = timerRunning +} + +func (t *timer) end() { + if t == nil || t.state != timerRunning { + return + } + t.timer.End() + t.state = timerWaiting +} + +func (t *timers) ready() bool { + if t == nil { + return false + } + for _, tt := range t.timers { + switch tt.state { + case timerIdle: + continue + case timerRunning: + return false + } + d, ok := tt.timer.Duration() + if !ok { + return false + } + tt.state = timerIdle + tt.Elapsed = d + } + return t.backend.IsTimeContinuous() +} + +func (t *timers) release() { + if t == nil { + return + } + for _, tt := range t.timers { + tt.timer.Release() + } + t.timers = nil +} diff --git a/pkg/gel/gio/internal/byteslice/byteslice.go b/pkg/gel/gio/internal/byteslice/byteslice.go new file mode 100644 index 0000000..26ebdb2 --- /dev/null +++ b/pkg/gel/gio/internal/byteslice/byteslice.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package byteslice provides byte slice views of other Go values such as +// slices and structs. +package byteslice + +import ( + "reflect" + "unsafe" +) + +// Struct returns a byte slice view of a struct. +func Struct(s interface{}) []byte { + v := reflect.ValueOf(s).Elem() + sz := int(v.Type().Size()) + var res []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + h.Data = uintptr(unsafe.Pointer(v.UnsafeAddr())) + h.Cap = sz + h.Len = sz + return res +} + +// Uint32 returns a byte slice view of a uint32 slice. +func Uint32(s []uint32) []byte { + n := len(s) + if n == 0 { + return nil + } + blen := n * int(unsafe.Sizeof(s[0])) + return (*[1 << 30]byte)(unsafe.Pointer(&s[0]))[:blen:blen] +} + +// Slice returns a byte slice view of a slice. +func Slice(s interface{}) []byte { + v := reflect.ValueOf(s) + first := v.Index(0) + sz := int(first.Type().Size()) + var res []byte + h := (*reflect.SliceHeader)(unsafe.Pointer(&res)) + h.Data = first.UnsafeAddr() + h.Cap = v.Cap() * sz + h.Len = v.Len() * sz + return res +} diff --git a/pkg/gel/gio/internal/cocoainit/cocoa_darwin.go b/pkg/gel/gio/internal/cocoainit/cocoa_darwin.go new file mode 100644 index 0000000..2a34e57 --- /dev/null +++ b/pkg/gel/gio/internal/cocoainit/cocoa_darwin.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package cocoainit initializes support for multithreaded +// programs in Cocoa. +package cocoainit + +/* +#cgo CFLAGS: -xobjective-c -fmodules -fobjc-arc +#import + +static inline void activate_cocoa_multithreading() { + [[NSThread new] start]; +} +#pragma GCC visibility push(hidden) +*/ +import "C" + +func init() { + C.activate_cocoa_multithreading() +} diff --git a/pkg/gel/gio/internal/d3d11/d3d11_windows.go b/pkg/gel/gio/internal/d3d11/d3d11_windows.go new file mode 100644 index 0000000..58a9355 --- /dev/null +++ b/pkg/gel/gio/internal/d3d11/d3d11_windows.go @@ -0,0 +1,1434 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package d3d11 + +import ( + "fmt" + "math" + "syscall" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + + "golang.org/x/sys/windows" +) + +type DXGI_SWAP_CHAIN_DESC struct { + BufferDesc DXGI_MODE_DESC + SampleDesc DXGI_SAMPLE_DESC + BufferUsage uint32 + BufferCount uint32 + OutputWindow windows.Handle + Windowed uint32 + SwapEffect uint32 + Flags uint32 +} + +type DXGI_SAMPLE_DESC struct { + Count uint32 + Quality uint32 +} + +type DXGI_MODE_DESC struct { + Width uint32 + Height uint32 + RefreshRate DXGI_RATIONAL + Format uint32 + ScanlineOrdering uint32 + Scaling uint32 +} + +type DXGI_RATIONAL struct { + Numerator uint32 + Denominator uint32 +} + +type TEXTURE2D_DESC struct { + Width uint32 + Height uint32 + MipLevels uint32 + ArraySize uint32 + Format uint32 + SampleDesc DXGI_SAMPLE_DESC + Usage uint32 + BindFlags uint32 + CPUAccessFlags uint32 + MiscFlags uint32 +} + +type SAMPLER_DESC struct { + Filter uint32 + AddressU uint32 + AddressV uint32 + AddressW uint32 + MipLODBias float32 + MaxAnisotropy uint32 + ComparisonFunc uint32 + BorderColor [4]float32 + MinLOD float32 + MaxLOD float32 +} + +type SHADER_RESOURCE_VIEW_DESC_TEX2D struct { + SHADER_RESOURCE_VIEW_DESC + Texture2D TEX2D_SRV +} + +type SHADER_RESOURCE_VIEW_DESC struct { + Format uint32 + ViewDimension uint32 +} + +type TEX2D_SRV struct { + MostDetailedMip uint32 + MipLevels uint32 +} + +type INPUT_ELEMENT_DESC struct { + SemanticName *byte + SemanticIndex uint32 + Format uint32 + InputSlot uint32 + AlignedByteOffset uint32 + InputSlotClass uint32 + InstanceDataStepRate uint32 +} + +type IDXGISwapChain struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetDevice uintptr + Present uintptr + GetBuffer uintptr + SetFullscreenState uintptr + GetFullscreenState uintptr + GetDesc uintptr + ResizeBuffers uintptr + ResizeTarget uintptr + GetContainingOutput uintptr + GetFrameStatistics uintptr + GetLastPresentCount uintptr + } +} + +type Device struct { + Vtbl *struct { + _IUnknownVTbl + CreateBuffer uintptr + CreateTexture1D uintptr + CreateTexture2D uintptr + CreateTexture3D uintptr + CreateShaderResourceView uintptr + CreateUnorderedAccessView uintptr + CreateRenderTargetView uintptr + CreateDepthStencilView uintptr + CreateInputLayout uintptr + CreateVertexShader uintptr + CreateGeometryShader uintptr + CreateGeometryShaderWithStreamOutput uintptr + CreatePixelShader uintptr + CreateHullShader uintptr + CreateDomainShader uintptr + CreateComputeShader uintptr + CreateClassLinkage uintptr + CreateBlendState uintptr + CreateDepthStencilState uintptr + CreateRasterizerState uintptr + CreateSamplerState uintptr + CreateQuery uintptr + CreatePredicate uintptr + CreateCounter uintptr + CreateDeferredContext uintptr + OpenSharedResource uintptr + CheckFormatSupport uintptr + CheckMultisampleQualityLevels uintptr + CheckCounterInfo uintptr + CheckCounter uintptr + CheckFeatureSupport uintptr + GetPrivateData uintptr + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetFeatureLevel uintptr + GetCreationFlags uintptr + GetDeviceRemovedReason uintptr + GetImmediateContext uintptr + SetExceptionMode uintptr + GetExceptionMode uintptr + } +} + +type DeviceContext struct { + Vtbl *struct { + _IUnknownVTbl + GetDevice uintptr + GetPrivateData uintptr + SetPrivateData uintptr + SetPrivateDataInterface uintptr + VSSetConstantBuffers uintptr + PSSetShaderResources uintptr + PSSetShader uintptr + PSSetSamplers uintptr + VSSetShader uintptr + DrawIndexed uintptr + Draw uintptr + Map uintptr + Unmap uintptr + PSSetConstantBuffers uintptr + IASetInputLayout uintptr + IASetVertexBuffers uintptr + IASetIndexBuffer uintptr + DrawIndexedInstanced uintptr + DrawInstanced uintptr + GSSetConstantBuffers uintptr + GSSetShader uintptr + IASetPrimitiveTopology uintptr + VSSetShaderResources uintptr + VSSetSamplers uintptr + Begin uintptr + End uintptr + GetData uintptr + SetPredication uintptr + GSSetShaderResources uintptr + GSSetSamplers uintptr + OMSetRenderTargets uintptr + OMSetRenderTargetsAndUnorderedAccessViews uintptr + OMSetBlendState uintptr + OMSetDepthStencilState uintptr + SOSetTargets uintptr + DrawAuto uintptr + DrawIndexedInstancedIndirect uintptr + DrawInstancedIndirect uintptr + Dispatch uintptr + DispatchIndirect uintptr + RSSetState uintptr + RSSetViewports uintptr + RSSetScissorRects uintptr + CopySubresourceRegion uintptr + CopyResource uintptr + UpdateSubresource uintptr + CopyStructureCount uintptr + ClearRenderTargetView uintptr + ClearUnorderedAccessViewUint uintptr + ClearUnorderedAccessViewFloat uintptr + ClearDepthStencilView uintptr + GenerateMips uintptr + SetResourceMinLOD uintptr + GetResourceMinLOD uintptr + ResolveSubresource uintptr + ExecuteCommandList uintptr + HSSetShaderResources uintptr + HSSetShader uintptr + HSSetSamplers uintptr + HSSetConstantBuffers uintptr + DSSetShaderResources uintptr + DSSetShader uintptr + DSSetSamplers uintptr + DSSetConstantBuffers uintptr + CSSetShaderResources uintptr + CSSetUnorderedAccessViews uintptr + CSSetShader uintptr + CSSetSamplers uintptr + CSSetConstantBuffers uintptr + VSGetConstantBuffers uintptr + PSGetShaderResources uintptr + PSGetShader uintptr + PSGetSamplers uintptr + VSGetShader uintptr + PSGetConstantBuffers uintptr + IAGetInputLayout uintptr + IAGetVertexBuffers uintptr + IAGetIndexBuffer uintptr + GSGetConstantBuffers uintptr + GSGetShader uintptr + IAGetPrimitiveTopology uintptr + VSGetShaderResources uintptr + VSGetSamplers uintptr + GetPredication uintptr + GSGetShaderResources uintptr + GSGetSamplers uintptr + OMGetRenderTargets uintptr + OMGetRenderTargetsAndUnorderedAccessViews uintptr + OMGetBlendState uintptr + OMGetDepthStencilState uintptr + SOGetTargets uintptr + RSGetState uintptr + RSGetViewports uintptr + RSGetScissorRects uintptr + HSGetShaderResources uintptr + HSGetShader uintptr + HSGetSamplers uintptr + HSGetConstantBuffers uintptr + DSGetShaderResources uintptr + DSGetShader uintptr + DSGetSamplers uintptr + DSGetConstantBuffers uintptr + CSGetShaderResources uintptr + CSGetUnorderedAccessViews uintptr + CSGetShader uintptr + CSGetSamplers uintptr + CSGetConstantBuffers uintptr + ClearState uintptr + Flush uintptr + GetType uintptr + GetContextFlags uintptr + FinishCommandList uintptr + } +} + +type RenderTargetView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Resource struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Texture2D struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type Buffer struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type SamplerState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type PixelShader struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type ShaderResourceView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type DepthStencilView struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type BlendState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type DepthStencilState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type VertexShader struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type RasterizerState struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type InputLayout struct { + Vtbl *struct { + _IUnknownVTbl + GetBufferPointer uintptr + GetBufferSize uintptr + } +} + +type DEPTH_STENCIL_DESC struct { + DepthEnable uint32 + DepthWriteMask uint32 + DepthFunc uint32 + StencilEnable uint32 + StencilReadMask uint8 + StencilWriteMask uint8 + FrontFace DEPTH_STENCILOP_DESC + BackFace DEPTH_STENCILOP_DESC +} + +type DEPTH_STENCILOP_DESC struct { + StencilFailOp uint32 + StencilDepthFailOp uint32 + StencilPassOp uint32 + StencilFunc uint32 +} + +type DEPTH_STENCIL_VIEW_DESC_TEX2D struct { + Format uint32 + ViewDimension uint32 + Flags uint32 + Texture2D TEX2D_DSV +} + +type TEX2D_DSV struct { + MipSlice uint32 +} + +type BLEND_DESC struct { + AlphaToCoverageEnable uint32 + IndependentBlendEnable uint32 + RenderTarget [8]RENDER_TARGET_BLEND_DESC +} + +type RENDER_TARGET_BLEND_DESC struct { + BlendEnable uint32 + SrcBlend uint32 + DestBlend uint32 + BlendOp uint32 + SrcBlendAlpha uint32 + DestBlendAlpha uint32 + BlendOpAlpha uint32 + RenderTargetWriteMask uint8 +} + +type IDXGIObject struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + } +} + +type IDXGIAdapter struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + EnumOutputs uintptr + GetDesc uintptr + CheckInterfaceSupport uintptr + GetDesc1 uintptr + } +} + +type IDXGIFactory struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + EnumAdapters uintptr + MakeWindowAssociation uintptr + GetWindowAssociation uintptr + CreateSwapChain uintptr + CreateSoftwareAdapter uintptr + } +} + +type IDXGIDevice struct { + Vtbl *struct { + _IUnknownVTbl + SetPrivateData uintptr + SetPrivateDataInterface uintptr + GetPrivateData uintptr + GetParent uintptr + GetAdapter uintptr + CreateSurface uintptr + QueryResourceResidency uintptr + SetGPUThreadPriority uintptr + GetGPUThreadPriority uintptr + } +} + +type IUnknown struct { + Vtbl *struct { + _IUnknownVTbl + } +} + +type _IUnknownVTbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +type BUFFER_DESC struct { + ByteWidth uint32 + Usage uint32 + BindFlags uint32 + CPUAccessFlags uint32 + MiscFlags uint32 + StructureByteStride uint32 +} + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4_0 uint8 + Data4_1 uint8 + Data4_2 uint8 + Data4_3 uint8 + Data4_4 uint8 + Data4_5 uint8 + Data4_6 uint8 + Data4_7 uint8 +} + +type VIEWPORT struct { + TopLeftX float32 + TopLeftY float32 + Width float32 + Height float32 + MinDepth float32 + MaxDepth float32 +} + +type SUBRESOURCE_DATA struct { + pSysMem *byte +} + +type BOX struct { + Left uint32 + Top uint32 + Front uint32 + Right uint32 + Bottom uint32 + Back uint32 +} + +type MAPPED_SUBRESOURCE struct { + PData uintptr + RowPitch uint32 + DepthPitch uint32 +} + +type ErrorCode struct { + Name string + Code uint32 +} + +type RASTERIZER_DESC struct { + FillMode uint32 + CullMode uint32 + FrontCounterClockwise uint32 + DepthBias int32 + DepthBiasClamp float32 + SlopeScaledDepthBias float32 + DepthClipEnable uint32 + ScissorEnable uint32 + MultisampleEnable uint32 + AntialiasedLineEnable uint32 +} + +var ( + IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95, 0x35, 0xd3, 0x4f, 0x9c} + IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd, 0x5f, 0x44, 0xc8, 0x4c} + IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae, 0x32, 0x1a, 0xe3, 0x69} +) + +var ( + d3d11 = windows.NewLazySystemDLL("d3d11.dll") + + _D3D11CreateDevice = d3d11.NewProc("D3D11CreateDevice") + _D3D11CreateDeviceAndSwapChain = d3d11.NewProc("D3D11CreateDeviceAndSwapChain") +) + +const ( + SDK_VERSION = 7 + DRIVER_TYPE_HARDWARE = 1 + + DXGI_FORMAT_UNKNOWN = 0 + DXGI_FORMAT_R16_FLOAT = 54 + DXGI_FORMAT_R32_FLOAT = 41 + DXGI_FORMAT_R32G32_FLOAT = 16 + DXGI_FORMAT_R32G32B32_FLOAT = 6 + DXGI_FORMAT_R32G32B32A32_FLOAT = 2 + DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 + DXGI_FORMAT_R16_SINT = 59 + DXGI_FORMAT_R16G16_SINT = 38 + DXGI_FORMAT_R16_UINT = 57 + DXGI_FORMAT_D24_UNORM_S8_UINT = 45 + DXGI_FORMAT_R16G16_FLOAT = 34 + DXGI_FORMAT_R16G16B16A16_FLOAT = 10 + + FORMAT_SUPPORT_TEXTURE2D = 0x20 + FORMAT_SUPPORT_RENDER_TARGET = 0x4000 + + DXGI_USAGE_RENDER_TARGET_OUTPUT = 1 << (1 + 4) + + CPU_ACCESS_READ = 0x20000 + + MAP_READ = 1 + + DXGI_SWAP_EFFECT_DISCARD = 0 + + FEATURE_LEVEL_9_1 = 0x9100 + FEATURE_LEVEL_9_3 = 0x9300 + FEATURE_LEVEL_11_0 = 0xb000 + + USAGE_IMMUTABLE = 1 + USAGE_STAGING = 3 + + BIND_VERTEX_BUFFER = 0x1 + BIND_INDEX_BUFFER = 0x2 + BIND_CONSTANT_BUFFER = 0x4 + BIND_SHADER_RESOURCE = 0x8 + BIND_RENDER_TARGET = 0x20 + BIND_DEPTH_STENCIL = 0x40 + + PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4 + PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5 + + FILTER_MIN_MAG_LINEAR_MIP_POINT = 0x14 + FILTER_MIN_MAG_MIP_POINT = 0 + + TEXTURE_ADDRESS_MIRROR = 2 + TEXTURE_ADDRESS_CLAMP = 3 + TEXTURE_ADDRESS_WRAP = 1 + + SRV_DIMENSION_TEXTURE2D = 4 + + CREATE_DEVICE_DEBUG = 0x2 + + FILL_SOLID = 3 + + CULL_NONE = 1 + + CLEAR_DEPTH = 0x1 + CLEAR_STENCIL = 0x2 + + DSV_DIMENSION_TEXTURE2D = 3 + + DEPTH_WRITE_MASK_ALL = 1 + + COMPARISON_GREATER = 5 + COMPARISON_GREATER_EQUAL = 7 + + BLEND_OP_ADD = 1 + BLEND_ONE = 2 + BLEND_INV_SRC_ALPHA = 6 + BLEND_ZERO = 1 + BLEND_DEST_COLOR = 9 + BLEND_DEST_ALPHA = 7 + + COLOR_WRITE_ENABLE_ALL = 1 | 2 | 4 | 8 + + DXGI_STATUS_OCCLUDED = 0x087A0001 + DXGI_ERROR_DEVICE_RESET = 0x887A0007 + DXGI_ERROR_DEVICE_REMOVED = 0x887A0005 + D3DDDIERR_DEVICEREMOVED = 1<<31 | 0x876<<16 | 2160 +) + +func CreateDevice(driverType uint32, flags uint32) (*Device, *DeviceContext, uint32, error) { + var ( + dev *Device + ctx *DeviceContext + featLvl uint32 + ) + r, _, _ := _D3D11CreateDevice.Call( + 0, // pAdapter + uintptr(driverType), // driverType + 0, // Software + uintptr(flags), // Flags + 0, // pFeatureLevels + 0, // FeatureLevels + SDK_VERSION, // SDKVersion + uintptr(unsafe.Pointer(&dev)), // ppDevice + uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel + uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext + ) + if r != 0 { + return nil, nil, 0, ErrorCode{Name: "D3D11CreateDevice", Code: uint32(r)} + } + return dev, ctx, featLvl, nil +} + +func CreateDeviceAndSwapChain(driverType uint32, flags uint32, swapDesc *DXGI_SWAP_CHAIN_DESC) (*Device, *DeviceContext, *IDXGISwapChain, uint32, error) { + var ( + dev *Device + ctx *DeviceContext + swchain *IDXGISwapChain + featLvl uint32 + ) + r, _, _ := _D3D11CreateDeviceAndSwapChain.Call( + 0, // pAdapter + uintptr(driverType), // driverType + 0, // Software + uintptr(flags), // Flags + 0, // pFeatureLevels + 0, // FeatureLevels + SDK_VERSION, // SDKVersion + uintptr(unsafe.Pointer(swapDesc)), // pSwapChainDesc + uintptr(unsafe.Pointer(&swchain)), // ppSwapChain + uintptr(unsafe.Pointer(&dev)), // ppDevice + uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel + uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext + ) + if r != 0 { + return nil, nil, nil, 0, ErrorCode{Name: "D3D11CreateDeviceAndSwapChain", Code: uint32(r)} + } + return dev, ctx, swchain, featLvl, nil +} + +func (d *Device) CheckFormatSupport(format uint32) (uint32, error) { + var support uint32 + r, _, _ := syscall.Syscall( + d.Vtbl.CheckFormatSupport, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(format), + uintptr(unsafe.Pointer(&support)), + ) + if r != 0 { + return 0, ErrorCode{Name: "DeviceCheckFormatSupport", Code: uint32(r)} + } + return support, nil +} + +func (d *Device) CreateBuffer(desc *BUFFER_DESC, data []byte) (*Buffer, error) { + var dataDesc *SUBRESOURCE_DATA + if len(data) > 0 { + dataDesc = &SUBRESOURCE_DATA{ + pSysMem: &data[0], + } + } + var buf *Buffer + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateBuffer, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(dataDesc)), + uintptr(unsafe.Pointer(&buf)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateBuffer", Code: uint32(r)} + } + return buf, nil +} + +func (d *Device) CreateDepthStencilViewTEX2D(res *Resource, desc *DEPTH_STENCIL_VIEW_DESC_TEX2D) (*DepthStencilView, error) { + var view *DepthStencilView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateDepthStencilView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&view)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateDepthStencilView", Code: uint32(r)} + } + return view, nil +} + +func (d *Device) CreatePixelShader(bytecode []byte) (*PixelShader, error) { + var shader *PixelShader + r, _, _ := syscall.Syscall6( + d.Vtbl.CreatePixelShader, + 5, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + 0, // pClassLinkage + uintptr(unsafe.Pointer(&shader)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreatePixelShader", Code: uint32(r)} + } + return shader, nil +} + +func (d *Device) CreateVertexShader(bytecode []byte) (*VertexShader, error) { + var shader *VertexShader + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateVertexShader, + 5, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + 0, // pClassLinkage + uintptr(unsafe.Pointer(&shader)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateVertexShader", Code: uint32(r)} + } + return shader, nil +} + +func (d *Device) CreateShaderResourceViewTEX2D(res *Resource, desc *SHADER_RESOURCE_VIEW_DESC_TEX2D) (*ShaderResourceView, error) { + var resView *ShaderResourceView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateShaderResourceView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&resView)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateShaderResourceView", Code: uint32(r)} + } + return resView, nil +} + +func (d *Device) CreateRasterizerState(desc *RASTERIZER_DESC) (*RasterizerState, error) { + var state *RasterizerState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateRasterizerState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRasterizerState", Code: uint32(r)} + } + return state, nil +} + +func (d *Device) CreateInputLayout(descs []INPUT_ELEMENT_DESC, bytecode []byte) (*InputLayout, error) { + var pdesc *INPUT_ELEMENT_DESC + if len(descs) > 0 { + pdesc = &descs[0] + } + var layout *InputLayout + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateInputLayout, + 6, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(pdesc)), + uintptr(len(descs)), + uintptr(unsafe.Pointer(&bytecode[0])), + uintptr(len(bytecode)), + uintptr(unsafe.Pointer(&layout)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateInputLayout", Code: uint32(r)} + } + return layout, nil +} + +func (d *Device) CreateSamplerState(desc *SAMPLER_DESC) (*SamplerState, error) { + var sampler *SamplerState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateSamplerState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&sampler)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateSamplerState", Code: uint32(r)} + } + return sampler, nil +} + +func (d *Device) CreateTexture2D(desc *TEXTURE2D_DESC) (*Texture2D, error) { + var tex *Texture2D + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateTexture2D, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + 0, // pInitialData + uintptr(unsafe.Pointer(&tex)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "CreateTexture2D", Code: uint32(r)} + } + return tex, nil +} + +func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView, error) { + var target *RenderTargetView + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateRenderTargetView, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(res)), + 0, // pDesc + uintptr(unsafe.Pointer(&target)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateRenderTargetView", Code: uint32(r)} + } + return target, nil +} + +func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) { + var state *BlendState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateBlendState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateBlendState", Code: uint32(r)} + } + return state, nil +} + +func (d *Device) CreateDepthStencilState(desc *DEPTH_STENCIL_DESC) (*DepthStencilState, error) { + var state *DepthStencilState + r, _, _ := syscall.Syscall( + d.Vtbl.CreateDepthStencilState, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&state)), + ) + if r != 0 { + return nil, ErrorCode{Name: "DeviceCreateDepthStencilState", Code: uint32(r)} + } + return state, nil +} + +func (d *Device) GetFeatureLevel() int { + lvl, _, _ := syscall.Syscall( + d.Vtbl.GetFeatureLevel, + 1, + uintptr(unsafe.Pointer(d)), + 0, 0, + ) + return int(lvl) +} + +func (d *Device) GetImmediateContext() *DeviceContext { + var ctx *DeviceContext + syscall.Syscall( + d.Vtbl.GetImmediateContext, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&ctx)), + 0, + ) + return ctx +} + +func (s *IDXGISwapChain) GetDesc() (DXGI_SWAP_CHAIN_DESC, error) { + var desc DXGI_SWAP_CHAIN_DESC + r, _, _ := syscall.Syscall( + s.Vtbl.GetDesc, + 2, + uintptr(unsafe.Pointer(s)), + uintptr(unsafe.Pointer(&desc)), + 0, + ) + if r != 0 { + return DXGI_SWAP_CHAIN_DESC{}, ErrorCode{Name: "IDXGISwapChainGetDesc", Code: uint32(r)} + } + return desc, nil +} + +func (s *IDXGISwapChain) ResizeBuffers(buffers, width, height, newFormat, flags uint32) error { + r, _, _ := syscall.Syscall6( + s.Vtbl.ResizeBuffers, + 6, + uintptr(unsafe.Pointer(s)), + uintptr(buffers), + uintptr(width), + uintptr(height), + uintptr(newFormat), + uintptr(flags), + ) + if r != 0 { + return ErrorCode{Name: "IDXGISwapChainResizeBuffers", Code: uint32(r)} + } + return nil +} + +func (s *IDXGISwapChain) Present(SyncInterval int, Flags uint32) error { + r, _, _ := syscall.Syscall( + s.Vtbl.Present, + 3, + uintptr(unsafe.Pointer(s)), + uintptr(SyncInterval), + uintptr(Flags), + ) + if r != 0 { + return ErrorCode{Name: "IDXGISwapChainPresent", Code: uint32(r)} + } + return nil +} + +func (s *IDXGISwapChain) GetBuffer(index int, riid *GUID) (*IUnknown, error) { + var buf *IUnknown + r, _, _ := syscall.Syscall6( + s.Vtbl.GetBuffer, + 4, + uintptr(unsafe.Pointer(s)), + uintptr(index), + uintptr(unsafe.Pointer(riid)), + uintptr(unsafe.Pointer(&buf)), + 0, + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGISwapChainGetBuffer", Code: uint32(r)} + } + return buf, nil +} + +func (c *DeviceContext) Unmap(resource *Resource, subResource uint32) { + syscall.Syscall( + c.Vtbl.Unmap, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(resource)), + uintptr(subResource), + ) +} + +func (c *DeviceContext) Map(resource *Resource, subResource, mapType, mapFlags uint32) (MAPPED_SUBRESOURCE, error) { + var resMap MAPPED_SUBRESOURCE + r, _, _ := syscall.Syscall6( + c.Vtbl.Map, + 6, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(resource)), + uintptr(subResource), + uintptr(mapType), + uintptr(mapFlags), + uintptr(unsafe.Pointer(&resMap)), + ) + if r != 0 { + return resMap, ErrorCode{Name: "DeviceContextMap", Code: uint32(r)} + } + return resMap, nil +} + +func (c *DeviceContext) CopySubresourceRegion(dst *Resource, dstSubresource, dstX, dstY, dstZ uint32, src *Resource, srcSubresource uint32, srcBox *BOX) { + syscall.Syscall9( + c.Vtbl.CopySubresourceRegion, + 9, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(dst)), + uintptr(dstSubresource), + uintptr(dstX), + uintptr(dstY), + uintptr(dstZ), + uintptr(unsafe.Pointer(src)), + uintptr(srcSubresource), + uintptr(unsafe.Pointer(srcBox)), + ) +} + +func (c *DeviceContext) ClearDepthStencilView(target *DepthStencilView, flags uint32, depth float32, stencil uint8) { + syscall.Syscall6( + c.Vtbl.ClearDepthStencilView, + 5, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(target)), + uintptr(flags), + uintptr(math.Float32bits(depth)), + uintptr(stencil), + 0, + ) +} + +func (c *DeviceContext) ClearRenderTargetView(target *RenderTargetView, color *[4]float32) { + syscall.Syscall( + c.Vtbl.ClearRenderTargetView, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(target)), + uintptr(unsafe.Pointer(color)), + ) +} + +func (c *DeviceContext) RSSetViewports(viewport *VIEWPORT) { + syscall.Syscall( + c.Vtbl.RSSetViewports, + 3, + uintptr(unsafe.Pointer(c)), + 1, // NumViewports + uintptr(unsafe.Pointer(viewport)), + ) +} + +func (c *DeviceContext) VSSetShader(s *VertexShader) { + syscall.Syscall6( + c.Vtbl.VSSetShader, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(s)), + 0, // ppClassInstances + 0, // NumClassInstances + 0, 0, + ) +} + +func (c *DeviceContext) VSSetConstantBuffers(b *Buffer) { + syscall.Syscall6( + c.Vtbl.VSSetConstantBuffers, + 4, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers + uintptr(unsafe.Pointer(&b)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetConstantBuffers(b *Buffer) { + syscall.Syscall6( + c.Vtbl.PSSetConstantBuffers, + 4, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers + uintptr(unsafe.Pointer(&b)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetShaderResources(startSlot uint32, s *ShaderResourceView) { + syscall.Syscall6( + c.Vtbl.PSSetShaderResources, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(startSlot), + 1, // NumViews + uintptr(unsafe.Pointer(&s)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetSamplers(startSlot uint32, s *SamplerState) { + syscall.Syscall6( + c.Vtbl.PSSetSamplers, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(startSlot), + 1, // NumSamplers + uintptr(unsafe.Pointer(&s)), + 0, 0, + ) +} + +func (c *DeviceContext) PSSetShader(s *PixelShader) { + syscall.Syscall6( + c.Vtbl.PSSetShader, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(s)), + 0, // ppClassInstances + 0, // NumClassInstances + 0, 0, + ) +} + +func (c *DeviceContext) UpdateSubresource(res *Resource, dstBox *BOX, rowPitch, depthPitch uint32, data []byte) { + syscall.Syscall9( + c.Vtbl.UpdateSubresource, + 7, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(res)), + 0, // DstSubresource + uintptr(unsafe.Pointer(dstBox)), + uintptr(unsafe.Pointer(&data[0])), + uintptr(rowPitch), + uintptr(depthPitch), + 0, 0, + ) +} + +func (c *DeviceContext) RSSetState(state *RasterizerState) { + syscall.Syscall( + c.Vtbl.RSSetState, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + 0, + ) +} + +func (c *DeviceContext) IASetInputLayout(layout *InputLayout) { + syscall.Syscall( + c.Vtbl.IASetInputLayout, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(layout)), + 0, + ) +} + +func (c *DeviceContext) IASetIndexBuffer(buf *Buffer, format, offset uint32) { + syscall.Syscall6( + c.Vtbl.IASetIndexBuffer, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(buf)), + uintptr(format), + uintptr(offset), + 0, 0, + ) +} + +func (c *DeviceContext) IASetVertexBuffers(buf *Buffer, stride, offset uint32) { + syscall.Syscall6( + c.Vtbl.IASetVertexBuffers, + 6, + uintptr(unsafe.Pointer(c)), + 0, // StartSlot + 1, // NumBuffers, + uintptr(unsafe.Pointer(&buf)), + uintptr(unsafe.Pointer(&stride)), + uintptr(unsafe.Pointer(&offset)), + ) +} + +func (c *DeviceContext) IASetPrimitiveTopology(mode uint32) { + syscall.Syscall( + c.Vtbl.IASetPrimitiveTopology, + 2, + uintptr(unsafe.Pointer(c)), + uintptr(mode), + 0, + ) +} + +func (c *DeviceContext) OMGetRenderTargets() (*RenderTargetView, *DepthStencilView) { + var ( + target *RenderTargetView + depthStencilView *DepthStencilView + ) + syscall.Syscall6( + c.Vtbl.OMGetRenderTargets, + 4, + uintptr(unsafe.Pointer(c)), + 1, // NumViews + uintptr(unsafe.Pointer(&target)), + uintptr(unsafe.Pointer(&depthStencilView)), + 0, 0, + ) + return target, depthStencilView +} + +func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView, depthStencil *DepthStencilView) { + syscall.Syscall6( + c.Vtbl.OMSetRenderTargets, + 4, + uintptr(unsafe.Pointer(c)), + 1, // NumViews + uintptr(unsafe.Pointer(&target)), + uintptr(unsafe.Pointer(depthStencil)), + 0, 0, + ) +} + +func (c *DeviceContext) Draw(count, start uint32) { + syscall.Syscall( + c.Vtbl.Draw, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(count), + uintptr(start), + ) +} + +func (c *DeviceContext) DrawIndexed(count, start uint32, base int32) { + syscall.Syscall6( + c.Vtbl.DrawIndexed, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(count), + uintptr(start), + uintptr(base), + 0, 0, + ) +} + +func (c *DeviceContext) OMSetBlendState(state *BlendState, factor *f32color.RGBA, sampleMask uint32) { + syscall.Syscall6( + c.Vtbl.OMSetBlendState, + 4, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + uintptr(unsafe.Pointer(factor)), + uintptr(sampleMask), + 0, 0, + ) +} + +func (c *DeviceContext) OMSetDepthStencilState(state *DepthStencilState, stencilRef uint32) { + syscall.Syscall( + c.Vtbl.OMSetDepthStencilState, + 3, + uintptr(unsafe.Pointer(c)), + uintptr(unsafe.Pointer(state)), + uintptr(stencilRef), + ) +} + +func (d *IDXGIObject) GetParent(guid *GUID) (*IDXGIObject, error) { + var parent *IDXGIObject + r, _, _ := syscall.Syscall( + d.Vtbl.GetParent, + 3, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&parent)), + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIObjectGetParent", Code: uint32(r)} + } + return parent, nil +} + +func (d *IDXGIFactory) CreateSwapChain(device *IUnknown, desc *DXGI_SWAP_CHAIN_DESC) (*IDXGISwapChain, error) { + var swchain *IDXGISwapChain + r, _, _ := syscall.Syscall6( + d.Vtbl.CreateSwapChain, + 4, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(device)), + uintptr(unsafe.Pointer(desc)), + uintptr(unsafe.Pointer(&swchain)), + 0, 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIFactory", Code: uint32(r)} + } + return swchain, nil +} + +func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) { + var adapter *IDXGIAdapter + r, _, _ := syscall.Syscall( + d.Vtbl.GetAdapter, + 2, + uintptr(unsafe.Pointer(d)), + uintptr(unsafe.Pointer(&adapter)), + 0, + ) + if r != 0 { + return nil, ErrorCode{Name: "IDXGIDeviceGetAdapter", Code: uint32(r)} + } + return adapter, nil +} + +func IUnknownQueryInterface(obj unsafe.Pointer, queryInterfaceMethod uintptr, guid *GUID) (*IUnknown, error) { + var ref *IUnknown + r, _, _ := syscall.Syscall( + queryInterfaceMethod, + 3, + uintptr(obj), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(&ref)), + ) + if r != 0 { + return nil, ErrorCode{Name: "IUnknownQueryInterface", Code: uint32(r)} + } + return ref, nil +} + +func IUnknownRelease(obj unsafe.Pointer, releaseMethod uintptr) { + syscall.Syscall( + releaseMethod, + 1, + uintptr(obj), + 0, + 0, + ) +} + +func (e ErrorCode) Error() string { + return fmt.Sprintf("%s: %#x", e.Name, e.Code) +} + +func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain, error) { + dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev), dev.Vtbl.QueryInterface, &IID_IDXGIDevice) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + adapter, err := (*IDXGIDevice)(unsafe.Pointer(dxgiDev)).GetAdapter() + IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + dxgiFactory, err := (*IDXGIObject)(unsafe.Pointer(adapter)).GetParent(&IID_IDXGIFactory) + IUnknownRelease(unsafe.Pointer(adapter), adapter.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain( + (*IUnknown)(unsafe.Pointer(dev)), + &DXGI_SWAP_CHAIN_DESC{ + BufferDesc: DXGI_MODE_DESC{ + Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, + }, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + }, + BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount: 1, + OutputWindow: hwnd, + Windowed: 1, + SwapEffect: DXGI_SWAP_EFFECT_DISCARD, + }, + ) + IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release) + if err != nil { + return nil, fmt.Errorf("NewContext: %v", err) + } + return swchain, nil +} + +func CreateDepthView(d *Device, width, height, depthBits int) (*DepthStencilView, error) { + depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{ + Width: uint32(width), + Height: uint32(height), + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_D24_UNORM_S8_UINT, + SampleDesc: DXGI_SAMPLE_DESC{ + Count: 1, + Quality: 0, + }, + BindFlags: BIND_DEPTH_STENCIL, + }) + if err != nil { + return nil, err + } + depthView, err := d.CreateDepthStencilViewTEX2D( + (*Resource)(unsafe.Pointer(depthTex)), + &DEPTH_STENCIL_VIEW_DESC_TEX2D{ + Format: DXGI_FORMAT_D24_UNORM_S8_UINT, + ViewDimension: DSV_DIMENSION_TEXTURE2D, + }, + ) + IUnknownRelease(unsafe.Pointer(depthTex), depthTex.Vtbl.Release) + return depthView, err +} diff --git a/pkg/gel/gio/internal/egl/egl.go b/pkg/gel/gio/internal/egl/egl.go new file mode 100644 index 0000000..b35ce89 --- /dev/null +++ b/pkg/gel/gio/internal/egl/egl.go @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux windows freebsd openbsd + +package egl + +import ( + "errors" + "fmt" + "runtime" + "strings" + + "github.com/p9c/p9/pkg/gel/gio/gpu" + "github.com/p9c/p9/pkg/gel/gio/internal/gl" + "github.com/p9c/p9/pkg/gel/gio/internal/srgb" +) + +type Context struct { + c *gl.Functions + disp _EGLDisplay + eglCtx *eglContext + eglSurf _EGLSurface + width, height int + refreshFBO bool + // For sRGB emulation. + srgbFBO *srgb.FBO +} + +type eglContext struct { + config _EGLConfig + ctx _EGLContext + visualID int + srgb bool + surfaceless bool +} + +var ( + nilEGLDisplay _EGLDisplay + nilEGLSurface _EGLSurface + nilEGLContext _EGLContext + nilEGLConfig _EGLConfig + EGL_DEFAULT_DISPLAY NativeDisplayType +) + +const ( + _EGL_ALPHA_SIZE = 0x3021 + _EGL_BLUE_SIZE = 0x3022 + _EGL_CONFIG_CAVEAT = 0x3027 + _EGL_CONTEXT_CLIENT_VERSION = 0x3098 + _EGL_DEPTH_SIZE = 0x3025 + _EGL_GL_COLORSPACE_KHR = 0x309d + _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089 + _EGL_GREEN_SIZE = 0x3023 + _EGL_EXTENSIONS = 0x3055 + _EGL_NATIVE_VISUAL_ID = 0x302e + _EGL_NONE = 0x3038 + _EGL_OPENGL_ES2_BIT = 0x4 + _EGL_RED_SIZE = 0x3024 + _EGL_RENDERABLE_TYPE = 0x3040 + _EGL_SURFACE_TYPE = 0x3033 + _EGL_WINDOW_BIT = 0x4 +) + +func (c *Context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } + c.ReleaseSurface() + if c.eglCtx != nil { + eglDestroyContext(c.disp, c.eglCtx.ctx) + c.eglCtx = nil + } + c.disp = nilEGLDisplay +} + +func (c *Context) Present() error { + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if !eglSwapBuffers(c.disp, c.eglSurf) { + return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError()) + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + return nil +} + +func NewContext(disp NativeDisplayType) (*Context, error) { + if err := loadEGL(); err != nil { + return nil, err + } + eglDisp := eglGetDisplay(disp) + // eglGetDisplay can return EGL_NO_DISPLAY yet no error + // (EGL_SUCCESS), in which case a default EGL display might be + // available. + if eglDisp == nilEGLDisplay { + eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY) + } + if eglDisp == nilEGLDisplay { + return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError()) + } + eglCtx, err := createContext(eglDisp) + if err != nil { + return nil, err + } + f, err := gl.NewFunctions(nil) + if err != nil { + return nil, err + } + c := &Context{ + disp: eglDisp, + eglCtx: eglCtx, + c: f, + } + return c, nil +} + +func (c *Context) API() gpu.API { + return gpu.OpenGL{} +} + +func (c *Context) ReleaseSurface() { + if c.eglSurf == nilEGLSurface { + return + } + // Make sure any in-flight GL commands are complete. + c.c.Finish() + c.ReleaseCurrent() + eglDestroySurface(c.disp, c.eglSurf) + c.eglSurf = nilEGLSurface +} + +func (c *Context) VisualID() int { + return c.eglCtx.visualID +} + +func (c *Context) CreateSurface(win NativeWindowType, width, height int) error { + eglSurf, err := createSurface(c.disp, c.eglCtx, win) + c.eglSurf = eglSurf + c.width = width + c.height = height + c.refreshFBO = true + return err +} + +func (c *Context) ReleaseCurrent() { + if c.disp != nilEGLDisplay { + eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext) + } +} + +func (c *Context) MakeCurrent() error { + if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless { + return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported") + } + if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) { + return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError()) + } + if c.eglCtx.srgb || c.eglSurf == nilEGLSurface { + return nil + } + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = srgb.New(nil) + if err != nil { + c.ReleaseCurrent() + return err + } + } + if c.refreshFBO { + c.refreshFBO = false + if err := c.srgbFBO.Refresh(c.width, c.height); err != nil { + c.ReleaseCurrent() + return err + } + } + return nil +} + +func (c *Context) EnableVSync(enable bool) { + if enable { + eglSwapInterval(c.disp, 1) + } else { + eglSwapInterval(c.disp, 0) + } +} + +func hasExtension(exts []string, ext string) bool { + for _, e := range exts { + if ext == e { + return true + } + } + return false +} + +func createContext(disp _EGLDisplay) (*eglContext, error) { + major, minor, ret := eglInitialize(disp) + if !ret { + return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError()) + } + // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported. + exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ") + srgb := major > 1 || minor >= 5 || hasExtension(exts, "EGL_KHR_gl_colorspace") + attribs := []_EGLint{ + _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT, + _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT, + _EGL_BLUE_SIZE, 8, + _EGL_GREEN_SIZE, 8, + _EGL_RED_SIZE, 8, + _EGL_CONFIG_CAVEAT, _EGL_NONE, + } + if srgb { + if runtime.GOOS == "linux" || runtime.GOOS == "android" { + // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha. + // https://bugs.freedesktop.org/show_bug.cgi?id=107782. + // + // Also, some Android devices (Samsung S9) needs alpha for sRGB to work. + attribs = append(attribs, _EGL_ALPHA_SIZE, 8) + } + // Only request a depth buffer if we're going to render directly to the framebuffer. + attribs = append(attribs, _EGL_DEPTH_SIZE, 16) + } + attribs = append(attribs, _EGL_NONE) + eglCfg, ret := eglChooseConfig(disp, attribs) + if !ret { + return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError()) + } + if eglCfg == nilEGLConfig { + supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context") + if !supportsNoCfg { + return nil, errors.New("eglChooseConfig returned no configs") + } + } + var visID _EGLint + if eglCfg != nilEGLConfig { + var ok bool + visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID) + if !ok { + return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed") + } + } + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 3, + _EGL_NONE, + } + eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + // Fall back to OpenGL ES 2 and rely on extensions. + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 2, + _EGL_NONE, + } + eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + return nil, fmt.Errorf("eglCreateContext failed: 0x%x", eglGetError()) + } + } + return &eglContext{ + config: _EGLConfig(eglCfg), + ctx: _EGLContext(eglCtx), + visualID: int(visID), + srgb: srgb, + surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"), + }, nil +} + +func createSurface(disp _EGLDisplay, eglCtx *eglContext, win NativeWindowType) (_EGLSurface, error) { + var surfAttribs []_EGLint + if eglCtx.srgb { + surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR, _EGL_GL_COLORSPACE_SRGB_KHR) + } + surfAttribs = append(surfAttribs, _EGL_NONE) + eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) + if eglSurf == nilEGLSurface && eglCtx.srgb { + // Try again without sRGB + eglCtx.srgb = false + surfAttribs = []_EGLint{_EGL_NONE} + eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) + } + if eglSurf == nilEGLSurface { + return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)", eglGetError(), eglCtx.srgb) + } + return eglSurf, nil +} diff --git a/pkg/gel/gio/internal/egl/egl_unix.go b/pkg/gel/gio/internal/egl/egl_unix.go new file mode 100644 index 0000000..059dd55 --- /dev/null +++ b/pkg/gel/gio/internal/egl/egl_unix.go @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux freebsd openbsd + +package egl + +/* +#cgo linux,!android pkg-config: egl +#cgo freebsd openbsd android LDFLAGS: -lEGL +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib +#cgo openbsd CFLAGS: -I/usr/X11R6/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib +#cgo CFLAGS: -DEGL_NO_X11 + +#include +#include +*/ +import "C" + +type ( + _EGLint = C.EGLint + _EGLDisplay = C.EGLDisplay + _EGLConfig = C.EGLConfig + _EGLContext = C.EGLContext + _EGLSurface = C.EGLSurface + NativeDisplayType = C.EGLNativeDisplayType + NativeWindowType = C.EGLNativeWindowType +) + +func loadEGL() error { + return nil +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg C.EGLConfig + var ncfg C.EGLint + if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE { + return nilEGLConfig, false + } + return _EGLConfig(cfg), true +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { + ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0]) + return _EGLContext(ctx) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglDestroySurface(disp, surf) == C.EGL_TRUE +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { + var val _EGLint + ret := C.eglGetConfigAttrib(disp, cfg, attr, &val) + return val, ret == C.EGL_TRUE +} + +func eglGetError() _EGLint { + return C.eglGetError() +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min _EGLint + ret := C.eglInitialize(disp, &maj, &min) + return maj, min, ret == C.EGL_TRUE +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { + return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE +} + +func eglReleaseThread() bool { + return C.eglReleaseThread() == C.EGL_TRUE +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + return C.eglSwapInterval(disp, interval) == C.EGL_TRUE +} + +func eglTerminate(disp _EGLDisplay) bool { + return C.eglTerminate(disp) == C.EGL_TRUE +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + return C.GoString(C.eglQueryString(disp, name)) +} + +func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { + return C.eglGetDisplay(disp) +} + +func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { + eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) + return eglSurf +} diff --git a/pkg/gel/gio/internal/egl/egl_windows.go b/pkg/gel/gio/internal/egl/egl_windows.go new file mode 100644 index 0000000..49eac11 --- /dev/null +++ b/pkg/gel/gio/internal/egl/egl_windows.go @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package egl + +import ( + "fmt" + "runtime" + "sync" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "github.com/p9c/p9/pkg/gel/gio/internal/gl" +) + +type ( + _EGLint int32 + _EGLDisplay uintptr + _EGLConfig uintptr + _EGLContext uintptr + _EGLSurface uintptr + NativeDisplayType uintptr + NativeWindowType uintptr +) + +var ( + libEGL = syscall.NewLazyDLL("libEGL.dll") + _eglChooseConfig = libEGL.NewProc("eglChooseConfig") + _eglCreateContext = libEGL.NewProc("eglCreateContext") + _eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface") + _eglDestroyContext = libEGL.NewProc("eglDestroyContext") + _eglDestroySurface = libEGL.NewProc("eglDestroySurface") + _eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib") + _eglGetDisplay = libEGL.NewProc("eglGetDisplay") + _eglGetError = libEGL.NewProc("eglGetError") + _eglInitialize = libEGL.NewProc("eglInitialize") + _eglMakeCurrent = libEGL.NewProc("eglMakeCurrent") + _eglReleaseThread = libEGL.NewProc("eglReleaseThread") + _eglSwapInterval = libEGL.NewProc("eglSwapInterval") + _eglSwapBuffers = libEGL.NewProc("eglSwapBuffers") + _eglTerminate = libEGL.NewProc("eglTerminate") + _eglQueryString = libEGL.NewProc("eglQueryString") +) + +var loadOnce sync.Once + +func loadEGL() error { + var err error + loadOnce.Do(func() { + err = loadDLLs() + }) + return err +} + +func loadDLLs() error { + if err := loadDLL(libEGL, "libEGL.dll"); err != nil { + return err + } + if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil { + return err + } + // d3dcompiler_47.dll is needed internally for shader compilation to function. + return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), "d3dcompiler_47.dll") +} + +func loadDLL(dll *syscall.LazyDLL, name string) error { + err := dll.Load() + if err != nil { + return fmt.Errorf("egl: failed to load %s: %v", name, err) + } + return nil +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg _EGLConfig + var ncfg _EGLint + a := &attribs[0] + r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg))) + issue34474KeepAlive(a) + return cfg, r != 0 +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { + a := &attribs[0] + c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg), uintptr(shareCtx), uintptr(unsafe.Pointer(a))) + issue34474KeepAlive(a) + return _EGLContext(c) +} + +func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { + a := &attribs[0] + s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg), uintptr(win), uintptr(unsafe.Pointer(a))) + issue34474KeepAlive(a) + return _EGLSurface(s) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx)) + return r != 0 +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { + var val uintptr + r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg), uintptr(attr), uintptr(unsafe.Pointer(&val))) + return _EGLint(val), r != 0 +} + +func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { + d, _, _ := _eglGetDisplay.Call(uintptr(disp)) + return _EGLDisplay(d) +} + +func eglGetError() _EGLint { + e, _, _ := _eglGetError.Call() + return _EGLint(e) +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min uintptr + r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)), uintptr(unsafe.Pointer(&min))) + return _EGLint(maj), _EGLint(min), r != 0 +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { + r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read), uintptr(ctx)) + return r != 0 +} + +func eglReleaseThread() bool { + r, _, _ := _eglReleaseThread.Call() + return r != 0 +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval)) + return r != 0 +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglTerminate(disp _EGLDisplay) bool { + r, _, _ := _eglTerminate.Call(uintptr(disp)) + return r != 0 +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name)) + return syscall.BytePtrToString((*byte)(unsafe.Pointer(r))) +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/pkg/gel/gio/internal/f32color/rgba.go b/pkg/gel/gio/internal/f32color/rgba.go new file mode 100644 index 0000000..eecf018 --- /dev/null +++ b/pkg/gel/gio/internal/f32color/rgba.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32color + +import ( + "image/color" + "math" +) + +// RGBA is a 32 bit floating point linear premultiplied color space. +type RGBA struct { + R, G, B, A float32 +} + +// Array returns rgba values in a [4]float32 array. +func (rgba RGBA) Array() [4]float32 { + return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A} +} + +// Float32 returns r, g, b, a values. +func (col RGBA) Float32() (r, g, b, a float32) { + return col.R, col.G, col.B, col.A +} + +// SRGBA converts from linear to sRGB color space. +func (col RGBA) SRGB() color.NRGBA { + if col.A == 0 { + return color.NRGBA{} + } + return color.NRGBA{ + R: uint8(linearTosRGB(col.R/col.A)*255 + .5), + G: uint8(linearTosRGB(col.G/col.A)*255 + .5), + B: uint8(linearTosRGB(col.B/col.A)*255 + .5), + A: uint8(col.A*255 + .5), + } +} + +// Luminance calculates the relative luminance of a linear RGBA color. +// Normalized to 0 for black and 1 for white. +// +// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details +func (col RGBA) Luminance() float32 { + return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B +} + +// Opaque returns the color without alpha component. +func (col RGBA) Opaque() RGBA { + col.A = 1.0 + return col +} + +// LinearFromSRGB converts from col in the sRGB colorspace to RGBA. +func LinearFromSRGB(col color.NRGBA) RGBA { + af := float32(col.A) / 0xFF + return RGBA{ + R: sRGBToLinear(float32(col.R)/0xff) * af, + G: sRGBToLinear(float32(col.G)/0xff) * af, + B: sRGBToLinear(float32(col.B)/0xff) * af, + A: af, + } +} + +// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color. +// +// Each component in the result is `sRGBToLinear(c * alpha)`, where `c` +// is the linear color. +func NRGBAToRGBA(col color.NRGBA) color.RGBA { + if col.A == 0xFF { + return color.RGBA(col) + } + c := LinearFromSRGB(col) + return color.RGBA{ + R: uint8(linearTosRGB(c.R)*255 + .5), + G: uint8(linearTosRGB(c.G)*255 + .5), + B: uint8(linearTosRGB(c.B)*255 + .5), + A: col.A, + } +} + +// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color. +// +// Each component in the result is `c * alpha`, where `c` is the linear color. +func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA { + if col.A == 0xFF { + return color.RGBA(col) + } + c := LinearFromSRGB(col) + return color.RGBA{ + R: uint8(c.R*255 + .5), + G: uint8(c.G*255 + .5), + B: uint8(c.B*255 + .5), + A: col.A, + } +} + +// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color. +func RGBAToNRGBA(col color.RGBA) color.NRGBA { + if col.A == 0xFF { + return color.NRGBA(col) + } + + linear := RGBA{ + R: sRGBToLinear(float32(col.R) / 0xff), + G: sRGBToLinear(float32(col.G) / 0xff), + B: sRGBToLinear(float32(col.B) / 0xff), + A: float32(col.A) / 0xff, + } + + return linear.SRGB() +} + +// linearTosRGB transforms color value from linear to sRGB. +func linearTosRGB(c float32) float32 { + // Formula from EXT_sRGB. + switch { + case c <= 0: + return 0 + case 0 < c && c < 0.0031308: + return 12.92 * c + case 0.0031308 <= c && c < 1: + return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055 + } + + return 1 +} + +// sRGBToLinear transforms color value from sRGB to linear. +func sRGBToLinear(c float32) float32 { + // Formula from EXT_sRGB. + if c <= 0.04045 { + return c / 12.92 + } else { + return float32(math.Pow(float64((c+0.055)/1.055), 2.4)) + } +} + +// MulAlpha applies the alpha to the color. +func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA { + c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF) + return c +} + +// Disabled blends color towards the luminance and multiplies alpha. +// Blending towards luminance will desaturate the color. +// Multiplying alpha blends the color together more with the background. +func Disabled(c color.NRGBA) (d color.NRGBA) { + const r = 80 // blend ratio + lum := approxLuminance(c) + return color.NRGBA{ + R: byte((int(c.R)*r + int(lum)*(256-r)) / 256), + G: byte((int(c.G)*r + int(lum)*(256-r)) / 256), + B: byte((int(c.B)*r + int(lum)*(256-r)) / 256), + A: byte(int(c.A) * (128 + 32) / 256), + } +} + +// Hovered blends color towards a brighter color. +func Hovered(c color.NRGBA) (d color.NRGBA) { + const r = 0x20 // lighten ratio + return color.NRGBA{ + R: byte(255 - int(255-c.R)*(255-r)/256), + G: byte(255 - int(255-c.G)*(255-r)/256), + B: byte(255 - int(255-c.B)*(255-r)/256), + A: c.A, + } +} + +// approxLuminance is a fast approximate version of RGBA.Luminance. +func approxLuminance(c color.NRGBA) byte { + const ( + r = 13933 // 0.2126 * 256 * 256 + g = 46871 // 0.7152 * 256 * 256 + b = 4732 // 0.0722 * 256 * 256 + t = r + g + b + ) + return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t) +} diff --git a/pkg/gel/gio/internal/f32color/rgba_test.go b/pkg/gel/gio/internal/f32color/rgba_test.go new file mode 100644 index 0000000..ea0f871 --- /dev/null +++ b/pkg/gel/gio/internal/f32color/rgba_test.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32color + +import ( + "image/color" + "testing" +) + +func TestNRGBAToLinearRGBA_Boundary(t *testing.T) { + for col := 0; col <= 0xFF; col++ { + for alpha := 0; alpha <= 0xFF; alpha++ { + in := color.NRGBA{R: uint8(col), A: uint8(alpha)} + premul := NRGBAToLinearRGBA(in) + if premul.A != uint8(alpha) { + t.Errorf("%v: got %v expected %v", in, premul.A, alpha) + } + if premul.R > premul.A { + t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A) + } + } + } +} + +func TestLinearToRGBARoundtrip(t *testing.T) { + for col := 0; col <= 0xFF; col++ { + for alpha := 0; alpha <= 0xFF; alpha++ { + want := color.NRGBA{R: uint8(col), A: uint8(alpha)} + if alpha == 0 { + want.R = 0 + } + got := LinearFromSRGB(want).SRGB() + if want != got { + t.Errorf("got %v expected %v", got, want) + } + } + } +} diff --git a/pkg/gel/gio/internal/fling/animation.go b/pkg/gel/gio/internal/fling/animation.go new file mode 100644 index 0000000..1534c92 --- /dev/null +++ b/pkg/gel/gio/internal/fling/animation.go @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "runtime" + "time" + + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +type Animation struct { + // Current offset in pixels. + x float32 + // Initial time. + t0 time.Time + // Initial velocity in pixels pr second. + v0 float32 +} + +var ( + // Pixels/second. + minFlingVelocity = unit.Dp(50) + maxFlingVelocity = unit.Dp(8000) +) + +const ( + thresholdVelocity = 1 +) + +// Start a fling given a starting velocity. Returns whether a +// fling was started. +func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool { + min := float32(c.Px(minFlingVelocity)) + v := velocity + if -min <= v && v <= min { + return false + } + max := float32(c.Px(maxFlingVelocity)) + if v > max { + v = max + } else if v < -max { + v = -max + } + f.init(now, v) + return true +} + +func (f *Animation) init(now time.Time, v0 float32) { + f.t0 = now + f.v0 = v0 + f.x = 0 +} + +func (f *Animation) Active() bool { + return f.v0 != 0 +} + +// Tick computes and returns a fling distance since +// the last time Tick was called. +func (f *Animation) Tick(now time.Time) int { + if !f.Active() { + return 0 + } + var k float32 + if runtime.GOOS == "darwin" { + k = -2 // iOS + } else { + k = -4.2 // Android and default + } + t := now.Sub(f.t0) + // The acceleration x''(t) of a point mass with a drag + // force, f, proportional with velocity, x'(t), is + // governed by the equation + // + // x''(t) = kx'(t) + // + // Given the starting position x(0) = 0, the starting + // velocity x'(0) = v0, the position is then + // given by + // + // x(t) = v0*e^(k*t)/k - v0/k + // + ekt := float32(math.Exp(float64(k) * t.Seconds())) + x := f.v0*ekt/k - f.v0/k + dist := x - f.x + idist := int(dist) + f.x += float32(idist) + // Solving for the velocity x'(t) gives us + // + // x'(t) = v0*e^(k*t) + v := f.v0 * ekt + if -thresholdVelocity < v && v < thresholdVelocity { + f.v0 = 0 + } + return idist +} diff --git a/pkg/gel/gio/internal/fling/extrapolation.go b/pkg/gel/gio/internal/fling/extrapolation.go new file mode 100644 index 0000000..655ef84 --- /dev/null +++ b/pkg/gel/gio/internal/fling/extrapolation.go @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import ( + "math" + "strconv" + "strings" + "time" +) + +// Extrapolation computes a 1-dimensional velocity estimate +// for a set of timestamped points using the least squares +// fit of a 2nd order polynomial. The same method is used +// by Android. +type Extrapolation struct { + // Index into points. + idx int + // Circular buffer of samples. + samples []sample + lastValue float32 + // Pre-allocated cache for samples. + cache [historySize]sample + + // Filtered values and times + values [historySize]float32 + times [historySize]float32 +} + +type sample struct { + t time.Duration + v float32 +} + +type matrix struct { + rows, cols int + data []float32 +} + +type Estimate struct { + Velocity float32 + Distance float32 +} + +type coefficients [degree + 1]float32 + +const ( + degree = 2 + historySize = 20 + maxAge = 100 * time.Millisecond + maxSampleGap = 40 * time.Millisecond +) + +// SampleDelta adds a relative sample to the estimation. +func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) { + val := delta + e.lastValue + e.Sample(t, val) +} + +// Sample adds an absolute sample to the estimation. +func (e *Extrapolation) Sample(t time.Duration, val float32) { + e.lastValue = val + if e.samples == nil { + e.samples = e.cache[:0] + } + s := sample{ + t: t, + v: val, + } + if e.idx == len(e.samples) && e.idx < cap(e.samples) { + e.samples = append(e.samples, s) + } else { + e.samples[e.idx] = s + } + e.idx++ + if e.idx == cap(e.samples) { + e.idx = 0 + } +} + +// Velocity returns an estimate of the implied velocity and +// distance for the points sampled, or zero if the estimation method +// failed. +func (e *Extrapolation) Estimate() Estimate { + if len(e.samples) == 0 { + return Estimate{} + } + values := e.values[:0] + times := e.times[:0] + first := e.get(0) + t := first.t + // Walk backwards collecting samples. + for i := 0; i < len(e.samples); i++ { + p := e.get(-i) + age := first.t - p.t + if age >= maxAge || t-p.t >= maxSampleGap { + // If the samples are too old or + // too much time passed between samples + // assume they're not part of the fling. + break + } + t = p.t + values = append(values, first.v-p.v) + times = append(times, float32((-age).Seconds())) + } + coef, ok := polyFit(times, values) + if !ok { + return Estimate{} + } + dist := values[len(values)-1] - values[0] + return Estimate{ + Velocity: coef[1], + Distance: dist, + } +} + +func (e *Extrapolation) get(i int) sample { + idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples) + return e.samples[idx] +} + +// fit computes the least squares polynomial fit for +// the set of points in X, Y. If the fitting fails +// because of contradicting or insufficient data, +// fit returns false. +func polyFit(X, Y []float32) (coefficients, bool) { + if len(X) != len(Y) { + panic("X and Y lengths differ") + } + if len(X) <= degree { + // Not enough points to fit a curve. + return coefficients{}, false + } + + // Use a method similar to Android's VelocityTracker.cpp: + // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp + // where all weights are 1. + + // First, expand the X vector to the matrix A in column-major order. + A := newMatrix(degree+1, len(X)) + for i, x := range X { + A.set(0, i, 1) + for j := 1; j < A.rows; j++ { + A.set(j, i, A.get(j-1, i)*x) + } + } + + Q, Rt, ok := decomposeQR(A) + if !ok { + return coefficients{}, false + } + // Solve R*B = Qt*Y for B, which is then the polynomial coefficients. + // Since R is upper triangular, we can proceed from bottom right to + // upper left. + // https://en.wikipedia.org/wiki/Non-linear_least_squares + var B coefficients + for i := Q.rows - 1; i >= 0; i-- { + B[i] = dot(Q.col(i), Y) + for j := Q.rows - 1; j > i; j-- { + B[i] -= Rt.get(i, j) * B[j] + } + B[i] /= Rt.get(i, i) + } + return B, true +} + +// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if +// possible. R is guaranteed to be upper triangular and only the square +// part of Rt is returned. +func decomposeQR(A *matrix) (*matrix, *matrix, bool) { + // Gram-Schmidt QR decompose A where Q*R = A. + // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process + Q := newMatrix(A.rows, A.cols) // Column-major. + Rt := newMatrix(A.rows, A.rows) // R transposed, row-major. + for i := 0; i < Q.rows; i++ { + // Copy A column. + for j := 0; j < Q.cols; j++ { + Q.set(i, j, A.get(i, j)) + } + // Subtract projections. Note that int the projection + // + // proju a = / u + // + // the normalized column e replaces u, where = 1: + // + // proje a = / e = e + for j := 0; j < i; j++ { + d := dot(Q.col(j), Q.col(i)) + for k := 0; k < Q.cols; k++ { + Q.set(i, k, Q.get(i, k)-d*Q.get(j, k)) + } + } + // Normalize Q columns. + n := norm(Q.col(i)) + if n < 0.000001 { + // Degenerate data, no solution. + return nil, nil, false + } + invNorm := 1 / n + for j := 0; j < Q.cols; j++ { + Q.set(i, j, Q.get(i, j)*invNorm) + } + // Update Rt. + for j := i; j < Rt.cols; j++ { + Rt.set(i, j, dot(Q.col(i), A.col(j))) + } + } + return Q, Rt, true +} + +func norm(V []float32) float32 { + var n float32 + for _, v := range V { + n += v * v + } + return float32(math.Sqrt(float64(n))) +} + +func dot(V1, V2 []float32) float32 { + var d float32 + for i, v1 := range V1 { + d += v1 * V2[i] + } + return d +} + +func newMatrix(rows, cols int) *matrix { + return &matrix{ + rows: rows, + cols: cols, + data: make([]float32, rows*cols), + } +} + +func (m *matrix) set(row, col int, v float32) { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + m.data[row*m.cols+col] = v +} + +func (m *matrix) get(row, col int) float32 { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + return m.data[row*m.cols+col] +} + +func (m *matrix) col(c int) []float32 { + return m.data[c*m.cols : (c+1)*m.cols] +} + +func (m *matrix) approxEqual(m2 *matrix) bool { + if m.rows != m2.rows || m.cols != m2.cols { + return false + } + const epsilon = 0.00001 + for row := 0; row < m.rows; row++ { + for col := 0; col < m.cols; col++ { + d := m2.get(row, col) - m.get(row, col) + if d < -epsilon || d > epsilon { + return false + } + } + } + return true +} + +func (m *matrix) transpose() *matrix { + t := &matrix{ + rows: m.cols, + cols: m.rows, + data: make([]float32, len(m.data)), + } + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + t.set(j, i, m.get(i, j)) + } + } + return t +} + +func (m *matrix) mul(m2 *matrix) *matrix { + if m.rows != m2.cols { + panic("mismatched matrices") + } + mm := &matrix{ + rows: m.rows, + cols: m2.cols, + data: make([]float32, m.rows*m2.cols), + } + for i := 0; i < mm.rows; i++ { + for j := 0; j < mm.cols; j++ { + var v float32 + for k := 0; k < m.rows; k++ { + v += m.get(k, j) * m2.get(i, k) + } + mm.set(i, j, v) + } + } + return mm +} + +func (m *matrix) String() string { + var b strings.Builder + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + v := m.get(i, j) + b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32)) + b.WriteString(", ") + } + b.WriteString("\n") + } + return b.String() +} + +func (c coefficients) approxEqual(c2 coefficients) bool { + const epsilon = 0.00001 + for i, v := range c { + d := v - c2[i] + if d < -epsilon || d > epsilon { + return false + } + } + return true +} diff --git a/pkg/gel/gio/internal/fling/extrapolation_test.go b/pkg/gel/gio/internal/fling/extrapolation_test.go new file mode 100644 index 0000000..3f9d982 --- /dev/null +++ b/pkg/gel/gio/internal/fling/extrapolation_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package fling + +import "testing" + +func TestDecomposeQR(t *testing.T) { + A := &matrix{ + rows: 3, cols: 3, + data: []float32{ + 12, 6, -4, + -51, 167, 24, + 4, -68, -41, + }, + } + Q, Rt, ok := decomposeQR(A) + if !ok { + t.Fatal("decomposeQR failed") + } + R := Rt.transpose() + QR := Q.mul(R) + if !A.approxEqual(QR) { + t.Log("A\n", A) + t.Log("Q\n", Q) + t.Log("R\n", R) + t.Log("QR\n", QR) + t.Fatal("Q*R not approximately equal to A") + } +} + +func TestFit(t *testing.T) { + X := []float32{-1, 0, 1} + Y := []float32{2, 0, 2} + + got, ok := polyFit(X, Y) + if !ok { + t.Fatal("polyFit failed") + } + want := coefficients{0, 0, 2} + if !got.approxEqual(want) { + t.Fatalf("polyFit: got %v want %v", got, want) + } +} diff --git a/pkg/gel/gio/internal/gl/gl.go b/pkg/gel/gio/internal/gl/gl.go new file mode 100644 index 0000000..9696c71 --- /dev/null +++ b/pkg/gel/gio/internal/gl/gl.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +type ( + Attrib uint + Enum uint +) + +const ( + ALL_BARRIER_BITS = 0xffffffff + ARRAY_BUFFER = 0x8892 + BLEND = 0xbe2 + CLAMP_TO_EDGE = 0x812f + COLOR_ATTACHMENT0 = 0x8ce0 + COLOR_BUFFER_BIT = 0x4000 + COMPILE_STATUS = 0x8b81 + COMPUTE_SHADER = 0x91B9 + DEPTH_BUFFER_BIT = 0x100 + DEPTH_ATTACHMENT = 0x8d00 + DEPTH_COMPONENT16 = 0x81a5 + DEPTH_COMPONENT24 = 0x81A6 + DEPTH_COMPONENT32F = 0x8CAC + DEPTH_TEST = 0xb71 + DRAW_FRAMEBUFFER = 0x8CA9 + DST_COLOR = 0x306 + DYNAMIC_DRAW = 0x88E8 + DYNAMIC_READ = 0x88E9 + ELEMENT_ARRAY_BUFFER = 0x8893 + EXTENSIONS = 0x1f03 + FALSE = 0 + FLOAT = 0x1406 + FRAGMENT_SHADER = 0x8b30 + FRAMEBUFFER = 0x8d40 + FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210 + FRAMEBUFFER_BINDING = 0x8ca6 + FRAMEBUFFER_COMPLETE = 0x8cd5 + HALF_FLOAT = 0x140b + HALF_FLOAT_OES = 0x8d61 + INFO_LOG_LENGTH = 0x8B84 + INVALID_INDEX = ^uint(0) + GREATER = 0x204 + GEQUAL = 0x206 + LINEAR = 0x2601 + LINK_STATUS = 0x8b82 + LUMINANCE = 0x1909 + MAP_READ_BIT = 0x0001 + MAX_TEXTURE_SIZE = 0xd33 + NEAREST = 0x2600 + NO_ERROR = 0x0 + NUM_EXTENSIONS = 0x821D + ONE = 0x1 + ONE_MINUS_SRC_ALPHA = 0x303 + PROGRAM_BINARY_LENGTH = 0x8741 + QUERY_RESULT = 0x8866 + QUERY_RESULT_AVAILABLE = 0x8867 + R16F = 0x822d + R8 = 0x8229 + READ_FRAMEBUFFER = 0x8ca8 + READ_ONLY = 0x88B8 + READ_WRITE = 0x88BA + RED = 0x1903 + RENDERER = 0x1F01 + RENDERBUFFER = 0x8d41 + RENDERBUFFER_BINDING = 0x8ca7 + RENDERBUFFER_HEIGHT = 0x8d43 + RENDERBUFFER_WIDTH = 0x8d42 + RGB = 0x1907 + RGBA = 0x1908 + RGBA8 = 0x8058 + SHADER_STORAGE_BUFFER = 0x90D2 + SHORT = 0x1402 + SRGB = 0x8c40 + SRGB_ALPHA_EXT = 0x8c42 + SRGB8 = 0x8c41 + SRGB8_ALPHA8 = 0x8c43 + STATIC_DRAW = 0x88e4 + STENCIL_BUFFER_BIT = 0x00000400 + TEXTURE_2D = 0xde1 + TEXTURE_MAG_FILTER = 0x2800 + TEXTURE_MIN_FILTER = 0x2801 + TEXTURE_WRAP_S = 0x2802 + TEXTURE_WRAP_T = 0x2803 + TEXTURE0 = 0x84c0 + TEXTURE1 = 0x84c1 + TRIANGLE_STRIP = 0x5 + TRIANGLES = 0x4 + TRUE = 1 + UNIFORM_BUFFER = 0x8A11 + UNPACK_ALIGNMENT = 0xcf5 + UNSIGNED_BYTE = 0x1401 + UNSIGNED_SHORT = 0x1403 + VERSION = 0x1f02 + VERTEX_SHADER = 0x8b31 + WRITE_ONLY = 0x88B9 + ZERO = 0x0 + + // EXT_disjoint_timer_query + TIME_ELAPSED_EXT = 0x88BF + GPU_DISJOINT_EXT = 0x8FBB +) + +var _ interface { + ActiveTexture(texture Enum) + AttachShader(p Program, s Shader) + BeginQuery(target Enum, query Query) + BindAttribLocation(p Program, a Attrib, name string) + BindBuffer(target Enum, b Buffer) + BindBufferBase(target Enum, index int, buffer Buffer) + BindFramebuffer(target Enum, fb Framebuffer) + BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) + BindRenderbuffer(target Enum, fb Renderbuffer) + BindTexture(target Enum, t Texture) + BlendEquation(mode Enum) + BlendFunc(sfactor, dfactor Enum) + BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) + BufferData(target Enum, size int, usage Enum) + BufferSubData(target Enum, offset int, src []byte) + CheckFramebufferStatus(target Enum) Enum + Clear(mask Enum) + ClearColor(red, green, blue, alpha float32) + ClearDepthf(d float32) + CompileShader(s Shader) + CreateBuffer() Buffer + CreateFramebuffer() Framebuffer + CreateProgram() Program + CreateQuery() Query + CreateRenderbuffer() Renderbuffer + CreateShader(ty Enum) Shader + CreateTexture() Texture + DeleteBuffer(v Buffer) + DeleteFramebuffer(v Framebuffer) + DeleteProgram(p Program) + DeleteQuery(query Query) + DeleteRenderbuffer(r Renderbuffer) + DeleteShader(s Shader) + DeleteTexture(v Texture) + DepthFunc(f Enum) + DepthMask(mask bool) + DisableVertexAttribArray(a Attrib) + Disable(cap Enum) + DispatchCompute(x, y, z int) + DrawArrays(mode Enum, first, count int) + DrawElements(mode Enum, count int, ty Enum, offset int) + Enable(cap Enum) + EnableVertexAttribArray(a Attrib) + EndQuery(target Enum) + FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) + FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) + GetBinding(pname Enum) Object + GetError() Enum + GetInteger(pname Enum) int + GetProgrami(p Program, pname Enum) int + GetProgramInfoLog(p Program) string + GetQueryObjectuiv(query Query, pname Enum) uint + GetShaderi(s Shader, pname Enum) int + GetShaderInfoLog(s Shader) string + GetString(pname Enum) string + GetUniformBlockIndex(p Program, name string) uint + GetUniformLocation(p Program, name string) Uniform + InvalidateFramebuffer(target, attachment Enum) + LinkProgram(p Program) + MapBufferRange(target Enum, offset, length int, access Enum) []byte + MemoryBarrier(barriers Enum) + ReadPixels(x, y, width, height int, format, ty Enum, data []byte) + RenderbufferStorage(target, internalformat Enum, width, height int) + ShaderSource(s Shader, src string) + TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) + TexParameteri(target, pname Enum, param int) + TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) + TexSubImage2D(target Enum, level, xoff, yoff int, width, height int, format, ty Enum, data []byte) + UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) + Uniform1f(dst Uniform, v float32) + Uniform1i(dst Uniform, v int) + Uniform2f(dst Uniform, v0, v1 float32) + Uniform3f(dst Uniform, v0, v1, v2 float32) + Uniform4f(dst Uniform, v0, v1, v2, v3 float32) + UseProgram(p Program) + UnmapBuffer(target Enum) bool + VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) + Viewport(x, y, width, height int) +} = (*Functions)(nil) diff --git a/pkg/gel/gio/internal/gl/gl_js.go b/pkg/gel/gio/internal/gl/gl_js.go new file mode 100644 index 0000000..13890a7 --- /dev/null +++ b/pkg/gel/gio/internal/gl/gl_js.go @@ -0,0 +1,381 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "strings" + "syscall/js" +) + +type Functions struct { + Ctx js.Value + EXT_disjoint_timer_query js.Value + EXT_disjoint_timer_query_webgl2 js.Value + + // Cached reference to the Uint8Array JS type. + uint8Array js.Value + + // Cached JS arrays. + arrayBuf js.Value + int32Buf js.Value +} + +type Context js.Value + +func NewFunctions(ctx Context) (*Functions, error) { + f := &Functions{ + Ctx: js.Value(ctx), + uint8Array: js.Global().Get("Uint8Array"), + } + if err := f.Init(); err != nil { + return nil, err + } + return f, nil +} + +func (f *Functions) Init() error { + webgl2Class := js.Global().Get("WebGL2RenderingContext") + iswebgl2 := !webgl2Class.IsUndefined() && f.Ctx.InstanceOf(webgl2Class) + if !iswebgl2 { + f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query") + if f.getExtension("OES_texture_half_float").IsNull() && f.getExtension("OES_texture_float").IsNull() { + return errors.New("gl: no support for neither OES_texture_half_float nor OES_texture_float") + } + if f.getExtension("EXT_sRGB").IsNull() { + return errors.New("gl: EXT_sRGB not supported") + } + } else { + // WebGL2 extensions. + f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2") + if f.getExtension("EXT_color_buffer_half_float").IsNull() && f.getExtension("EXT_color_buffer_float").IsNull() { + return errors.New("gl: no support for neither EXT_color_buffer_half_float nor EXT_color_buffer_float") + } + } + return nil +} + +func (f *Functions) getExtension(name string) js.Value { + return f.Ctx.Call("getExtension", name) +} + +func (f *Functions) ActiveTexture(t Enum) { + f.Ctx.Call("activeTexture", int(t)) +} +func (f *Functions) AttachShader(p Program, s Shader) { + f.Ctx.Call("attachShader", js.Value(p), js.Value(s)) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("beginQuery", int(target), js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query)) + } +} +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + f.Ctx.Call("bindAttribLocation", js.Value(p), int(a), name) +} +func (f *Functions) BindBuffer(target Enum, b Buffer) { + f.Ctx.Call("bindBuffer", int(target), js.Value(b)) +} +func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { + f.Ctx.Call("bindBufferBase", int(target), index, js.Value(b)) +} +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + f.Ctx.Call("bindFramebuffer", int(target), js.Value(fb)) +} +func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + f.Ctx.Call("bindRenderbuffer", int(target), js.Value(rb)) +} +func (f *Functions) BindTexture(target Enum, t Texture) { + f.Ctx.Call("bindTexture", int(target), js.Value(t)) +} +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + panic("not implemented") +} +func (f *Functions) BlendEquation(mode Enum) { + f.Ctx.Call("blendEquation", int(mode)) +} +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + f.Ctx.Call("blendFunc", int(sfactor), int(dfactor)) +} +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + panic("not implemented") +} +func (f *Functions) BufferData(target Enum, size int, usage Enum) { + f.Ctx.Call("bufferData", int(target), size, int(usage)) +} +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + f.Ctx.Call("bufferSubData", int(target), offset, f.byteArrayOf(src)) +} +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(f.Ctx.Call("checkFramebufferStatus", int(target)).Int()) +} +func (f *Functions) Clear(mask Enum) { + f.Ctx.Call("clear", int(mask)) +} +func (f *Functions) ClearColor(red, green, blue, alpha float32) { + f.Ctx.Call("clearColor", red, green, blue, alpha) +} +func (f *Functions) ClearDepthf(d float32) { + f.Ctx.Call("clearDepth", d) +} +func (f *Functions) CompileShader(s Shader) { + f.Ctx.Call("compileShader", js.Value(s)) +} +func (f *Functions) CreateBuffer() Buffer { + return Buffer(f.Ctx.Call("createBuffer")) +} +func (f *Functions) CreateFramebuffer() Framebuffer { + return Framebuffer(f.Ctx.Call("createFramebuffer")) +} +func (f *Functions) CreateProgram() Program { + return Program(f.Ctx.Call("createProgram")) +} +func (f *Functions) CreateQuery() Query { + return Query(f.Ctx.Call("createQuery")) +} +func (f *Functions) CreateRenderbuffer() Renderbuffer { + return Renderbuffer(f.Ctx.Call("createRenderbuffer")) +} +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader(f.Ctx.Call("createShader", int(ty))) +} +func (f *Functions) CreateTexture() Texture { + return Texture(f.Ctx.Call("createTexture")) +} +func (f *Functions) DeleteBuffer(v Buffer) { + f.Ctx.Call("deleteBuffer", js.Value(v)) +} +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.Ctx.Call("deleteFramebuffer", js.Value(v)) +} +func (f *Functions) DeleteProgram(p Program) { + f.Ctx.Call("deleteProgram", js.Value(p)) +} +func (f *Functions) DeleteQuery(query Query) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("deleteQuery", js.Value(query)) + } else { + f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query)) + } +} +func (f *Functions) DeleteShader(s Shader) { + f.Ctx.Call("deleteShader", js.Value(s)) +} +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.Ctx.Call("deleteRenderbuffer", js.Value(v)) +} +func (f *Functions) DeleteTexture(v Texture) { + f.Ctx.Call("deleteTexture", js.Value(v)) +} +func (f *Functions) DepthFunc(fn Enum) { + f.Ctx.Call("depthFunc", int(fn)) +} +func (f *Functions) DepthMask(mask bool) { + f.Ctx.Call("depthMask", mask) +} +func (f *Functions) DisableVertexAttribArray(a Attrib) { + f.Ctx.Call("disableVertexAttribArray", int(a)) +} +func (f *Functions) Disable(cap Enum) { + f.Ctx.Call("disable", int(cap)) +} +func (f *Functions) DrawArrays(mode Enum, first, count int) { + f.Ctx.Call("drawArrays", int(mode), first, count) +} +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + f.Ctx.Call("drawElements", int(mode), count, int(ty), offset) +} +func (f *Functions) DispatchCompute(x, y, z int) { + panic("not implemented") +} +func (f *Functions) Enable(cap Enum) { + f.Ctx.Call("enable", int(cap)) +} +func (f *Functions) EnableVertexAttribArray(a Attrib) { + f.Ctx.Call("enableVertexAttribArray", int(a)) +} +func (f *Functions) EndQuery(target Enum) { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + f.Ctx.Call("endQuery", int(target)) + } else { + f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target)) + } +} +func (f *Functions) Finish() { + f.Ctx.Call("finish") +} +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + f.Ctx.Call("framebufferRenderbuffer", int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer)) +} +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + f.Ctx.Call("framebufferTexture2D", int(target), int(attachment), int(texTarget), js.Value(t), level) +} +func (f *Functions) GetError() Enum { + // Avoid slow getError calls. See gio#179. + return 0 +} +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + return paramVal(f.Ctx.Call("getRenderbufferParameteri", int(pname))) +} +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + return paramVal(f.Ctx.Call("getFramebufferAttachmentParameter", int(target), int(attachment), int(pname))) +} +func (f *Functions) GetBinding(pname Enum) Object { + return Object(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetInteger(pname Enum) int { + return paramVal(f.Ctx.Call("getParameter", int(pname))) +} +func (f *Functions) GetProgrami(p Program, pname Enum) int { + return paramVal(f.Ctx.Call("getProgramParameter", js.Value(p), int(pname))) +} +func (f *Functions) GetProgramInfoLog(p Program) string { + return f.Ctx.Call("getProgramInfoLog", js.Value(p)).String() +} +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + if !f.EXT_disjoint_timer_query_webgl2.IsNull() { + return uint(paramVal(f.Ctx.Call("getQueryParameter", js.Value(query), int(pname)))) + } else { + return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname)))) + } +} +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + return paramVal(f.Ctx.Call("getShaderParameter", js.Value(s), int(pname))) +} +func (f *Functions) GetShaderInfoLog(s Shader) string { + return f.Ctx.Call("getShaderInfoLog", js.Value(s)).String() +} +func (f *Functions) GetString(pname Enum) string { + switch pname { + case EXTENSIONS: + extsjs := f.Ctx.Call("getSupportedExtensions") + var exts []string + for i := 0; i < extsjs.Length(); i++ { + exts = append(exts, "GL_"+extsjs.Index(i).String()) + } + return strings.Join(exts, " ") + default: + return f.Ctx.Call("getParameter", int(pname)).String() + } +} +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + return uint(paramVal(f.Ctx.Call("getUniformBlockIndex", js.Value(p), name))) +} +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + return Uniform(f.Ctx.Call("getUniformLocation", js.Value(p), name)) +} +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + fn := f.Ctx.Get("invalidateFramebuffer") + if !fn.IsUndefined() { + if f.int32Buf.IsUndefined() { + f.int32Buf = js.Global().Get("Int32Array").New(1) + } + f.int32Buf.SetIndex(0, int32(attachment)) + f.Ctx.Call("invalidateFramebuffer", int(target), f.int32Buf) + } +} +func (f *Functions) LinkProgram(p Program) { + f.Ctx.Call("linkProgram", js.Value(p)) +} +func (f *Functions) PixelStorei(pname Enum, param int32) { + f.Ctx.Call("pixelStorei", int(pname), param) +} +func (f *Functions) MemoryBarrier(barriers Enum) { + panic("not implemented") +} +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + panic("not implemented") +} +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + f.Ctx.Call("renderbufferStorage", int(target), int(internalformat), width, height) +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + ba := f.byteArrayOf(data) + f.Ctx.Call("readPixels", x, y, width, height, int(format), int(ty), ba) + js.CopyBytesToGo(data, ba) +} +func (f *Functions) Scissor(x, y, width, height int32) { + f.Ctx.Call("scissor", x, y, width, height) +} +func (f *Functions) ShaderSource(s Shader, src string) { + f.Ctx.Call("shaderSource", js.Value(s), src) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) { + f.Ctx.Call("texImage2D", int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), nil) +} +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + f.Ctx.Call("texStorage2D", int(target), levels, int(internalFormat), width, height) +} +func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + f.Ctx.Call("texSubImage2D", int(target), level, x, y, width, height, int(format), int(ty), f.byteArrayOf(data)) +} +func (f *Functions) TexParameteri(target, pname Enum, param int) { + f.Ctx.Call("texParameteri", int(target), int(pname), int(param)) +} +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + f.Ctx.Call("uniformBlockBinding", js.Value(p), int(uniformBlockIndex), int(uniformBlockBinding)) +} +func (f *Functions) Uniform1f(dst Uniform, v float32) { + f.Ctx.Call("uniform1f", js.Value(dst), v) +} +func (f *Functions) Uniform1i(dst Uniform, v int) { + f.Ctx.Call("uniform1i", js.Value(dst), v) +} +func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + f.Ctx.Call("uniform2f", js.Value(dst), v0, v1) +} +func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + f.Ctx.Call("uniform3f", js.Value(dst), v0, v1, v2) +} +func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + f.Ctx.Call("uniform4f", js.Value(dst), v0, v1, v2, v3) +} +func (f *Functions) UseProgram(p Program) { + f.Ctx.Call("useProgram", js.Value(p)) +} +func (f *Functions) UnmapBuffer(target Enum) bool { + panic("not implemented") +} +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + f.Ctx.Call("vertexAttribPointer", int(dst), size, int(ty), normalized, stride, offset) +} +func (f *Functions) Viewport(x, y, width, height int) { + f.Ctx.Call("viewport", x, y, width, height) +} + +func (f *Functions) byteArrayOf(data []byte) js.Value { + if len(data) == 0 { + return js.Null() + } + f.resizeByteBuffer(len(data)) + ba := f.uint8Array.New(f.arrayBuf, int(0), int(len(data))) + js.CopyBytesToJS(ba, data) + return ba +} + +func (f *Functions) resizeByteBuffer(n int) { + if n == 0 { + return + } + if !f.arrayBuf.IsUndefined() && f.arrayBuf.Length() >= n { + return + } + f.arrayBuf = js.Global().Get("ArrayBuffer").New(n) +} + +func paramVal(v js.Value) int { + switch v.Type() { + case js.TypeBoolean: + if b := v.Bool(); b { + return 1 + } else { + return 0 + } + case js.TypeNumber: + return v.Int() + default: + panic("unknown parameter type") + } +} diff --git a/pkg/gel/gio/internal/gl/gl_unix.go b/pkg/gel/gio/internal/gl/gl_unix.go new file mode 100644 index 0000000..a1d017a --- /dev/null +++ b/pkg/gel/gio/internal/gl/gl_unix.go @@ -0,0 +1,635 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin linux freebsd openbsd + +package gl + +import ( + "runtime" + "strings" + "unsafe" +) + +/* +#cgo CFLAGS: -Werror +#cgo linux,!android pkg-config: glesv2 +#cgo linux freebsd LDFLAGS: -ldl +#cgo freebsd openbsd android LDFLAGS: -lGLESv2 +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib +#cgo openbsd CFLAGS: -I/usr/X11R6/include +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib +#cgo darwin,!ios CFLAGS: -DGL_SILENCE_DEPRECATION +#cgo darwin,!ios LDFLAGS: -framework OpenGL +#cgo darwin,ios CFLAGS: -DGLES_SILENCE_DEPRECATION +#cgo darwin,ios LDFLAGS: -framework OpenGLES + +#include +#define __USE_GNU +#include + +#ifdef __APPLE__ + #include "TargetConditionals.h" + #if TARGET_OS_IPHONE + #include + #else + #include + #endif +#else +#include +#include +#endif + +static void (*_glBindBufferBase)(GLenum target, GLuint index, GLuint buffer); +static GLuint (*_glGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); +static void (*_glUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); +static void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments); + +static void (*_glBeginQuery)(GLenum target, GLuint id); +static void (*_glDeleteQueries)(GLsizei n, const GLuint *ids); +static void (*_glEndQuery)(GLenum target); +static void (*_glGenQueries)(GLsizei n, GLuint *ids); +static void (*_glGetProgramBinary)(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary); +static void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params); +static const GLubyte* (*_glGetStringi)(GLenum name, GLuint index); +static void (*_glMemoryBarrier)(GLbitfield barriers); +static void (*_glDispatchCompute)(GLuint x, GLuint y, GLuint z); +static void* (*_glMapBufferRange)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access); +static GLboolean (*_glUnmapBuffer)(GLenum target); +static void (*_glBindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); +static void (*_glTexStorage2D)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); +static void (*_glBlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); + +// The pointer-free version of glVertexAttribPointer, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) { + glVertexAttribPointer(index, size, type, normalized, stride, (const GLvoid *)offset); +} + +// The pointer-free version of glDrawElements, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glDrawElements(GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) { + glDrawElements(mode, count, type, (const GLvoid *)offset); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBindBufferBase(GLenum target, GLuint index, GLuint buffer) { + _glBindBufferBase(target, index, buffer); +} + +__attribute__ ((visibility ("hidden"))) void gio_glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) { + _glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding); +} + +__attribute__ ((visibility ("hidden"))) GLuint gio_glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName) { + return _glGetUniformBlockIndex(program, uniformBlockName); +} + +__attribute__ ((visibility ("hidden"))) void gio_glInvalidateFramebuffer(GLenum target, GLenum attachment) { + // Framebuffer invalidation is just a hint and can safely be ignored. + if (_glInvalidateFramebuffer != NULL) { + _glInvalidateFramebuffer(target, 1, &attachment); + } +} + +__attribute__ ((visibility ("hidden"))) void gio_glBeginQuery(GLenum target, GLenum attachment) { + _glBeginQuery(target, attachment); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDeleteQueries(GLsizei n, const GLuint *ids) { + _glDeleteQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glEndQuery(GLenum target) { + _glEndQuery(target); +} + +__attribute__ ((visibility ("hidden"))) const GLubyte* gio_glGetStringi(GLenum name, GLuint index) { + if (_glGetStringi == NULL) { + return NULL; + } + return _glGetStringi(name, index); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGenQueries(GLsizei n, GLuint *ids) { + _glGenQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetProgramBinary(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary) { + _glGetProgramBinary(program, bufsize, length, binaryFormat, binary); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint *params) { + _glGetQueryObjectuiv(id, pname, params); +} + +__attribute__ ((visibility ("hidden"))) void gio_glMemoryBarrier(GLbitfield barriers) { + _glMemoryBarrier(barriers); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDispatchCompute(GLuint x, GLuint y, GLuint z) { + _glDispatchCompute(x, y, z); +} + +__attribute__ ((visibility ("hidden"))) void *gio_glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) { + return _glMapBufferRange(target, offset, length, access); +} + +__attribute__ ((visibility ("hidden"))) GLboolean gio_glUnmapBuffer(GLenum target) { + return _glUnmapBuffer(target); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) { + _glBindImageTexture(unit, texture, level, layered, layer, access, format); +} + +__attribute__ ((visibility ("hidden"))) void gio_glTexStorage2D(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height) { + _glTexStorage2D(target, levels, internalFormat, width, height); +} + +__attribute__ ((visibility ("hidden"))) void gio_glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { + _glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); +} + +__attribute__((constructor)) static void gio_loadGLFunctions() { + // Load libGLESv3 if available. + dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL); + + _glBindBufferBase = dlsym(RTLD_DEFAULT, "glBindBufferBase"); + _glGetUniformBlockIndex = dlsym(RTLD_DEFAULT, "glGetUniformBlockIndex"); + _glUniformBlockBinding = dlsym(RTLD_DEFAULT, "glUniformBlockBinding"); + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glInvalidateFramebuffer"); + _glGetStringi = dlsym(RTLD_DEFAULT, "glGetStringi"); + // Fall back to EXT_invalidate_framebuffer if available. + if (_glInvalidateFramebuffer == NULL) { + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glDiscardFramebufferEXT"); + } + + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQuery"); + if (_glBeginQuery == NULL) + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQueryEXT"); + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueries"); + if (_glDeleteQueries == NULL) + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueriesEXT"); + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQuery"); + if (_glEndQuery == NULL) + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQueryEXT"); + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueries"); + if (_glGenQueries == NULL) + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueriesEXT"); + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuiv"); + if (_glGetQueryObjectuiv == NULL) + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuivEXT"); + + _glMemoryBarrier = dlsym(RTLD_DEFAULT, "glMemoryBarrier"); + _glDispatchCompute = dlsym(RTLD_DEFAULT, "glDispatchCompute"); + _glMapBufferRange = dlsym(RTLD_DEFAULT, "glMapBufferRange"); + _glUnmapBuffer = dlsym(RTLD_DEFAULT, "glUnmapBuffer"); + _glBindImageTexture = dlsym(RTLD_DEFAULT, "glBindImageTexture"); + _glTexStorage2D = dlsym(RTLD_DEFAULT, "glTexStorage2D"); + _glBlitFramebuffer = dlsym(RTLD_DEFAULT, "glBlitFramebuffer"); + _glGetProgramBinary = dlsym(RTLD_DEFAULT, "glGetProgramBinary"); +} +*/ +import "C" + +type Context interface{} + +type Functions struct { + // Query caches. + uints [100]C.GLuint + ints [100]C.GLint +} + +func NewFunctions(ctx Context) (*Functions, error) { + if ctx != nil { + panic("non-nil context") + } + return new(Functions), nil +} + +func (f *Functions) ActiveTexture(texture Enum) { + C.glActiveTexture(C.GLenum(texture)) +} + +func (f *Functions) AttachShader(p Program, s Shader) { + C.glAttachShader(C.GLuint(p.V), C.GLuint(s.V)) +} + +func (f *Functions) BeginQuery(target Enum, query Query) { + C.gio_glBeginQuery(C.GLenum(target), C.GLenum(query.V)) +} + +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + C.glBindAttribLocation(C.GLuint(p.V), C.GLuint(a), cname) +} + +func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { + C.gio_glBindBufferBase(C.GLenum(target), C.GLuint(index), C.GLuint(b.V)) +} + +func (f *Functions) BindBuffer(target Enum, b Buffer) { + C.glBindBuffer(C.GLenum(target), C.GLuint(b.V)) +} + +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + C.glBindFramebuffer(C.GLenum(target), C.GLuint(fb.V)) +} + +func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) { + C.glBindRenderbuffer(C.GLenum(target), C.GLuint(fb.V)) +} + +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + l := C.GLboolean(C.GL_FALSE) + if layered { + l = C.GL_TRUE + } + C.gio_glBindImageTexture(C.GLuint(unit), C.GLuint(t.V), C.GLint(level), l, C.GLint(layer), C.GLenum(access), C.GLenum(format)) +} + +func (f *Functions) BindTexture(target Enum, t Texture) { + C.glBindTexture(C.GLenum(target), C.GLuint(t.V)) +} + +func (f *Functions) BlendEquation(mode Enum) { + C.glBlendEquation(C.GLenum(mode)) +} + +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + C.glBlendFunc(C.GLenum(sfactor), C.GLenum(dfactor)) +} + +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + C.gio_glBlitFramebuffer( + C.GLint(sx0), C.GLint(sy0), C.GLint(sx1), C.GLint(sy1), + C.GLint(dx0), C.GLint(dy0), C.GLint(dx1), C.GLint(dy1), + C.GLenum(mask), C.GLenum(filter), + ) +} + +func (f *Functions) BufferData(target Enum, size int, usage Enum) { + C.glBufferData(C.GLenum(target), C.GLsizeiptr(size), nil, C.GLenum(usage)) +} + +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + var p unsafe.Pointer + if len(src) > 0 { + p = unsafe.Pointer(&src[0]) + } + C.glBufferSubData(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(len(src)), p) +} + +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(C.glCheckFramebufferStatus(C.GLenum(target))) +} + +func (f *Functions) Clear(mask Enum) { + C.glClear(C.GLbitfield(mask)) +} + +func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) { + C.glClearColor(C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha)) +} + +func (f *Functions) ClearDepthf(d float32) { + C.glClearDepthf(C.GLfloat(d)) +} + +func (f *Functions) CompileShader(s Shader) { + C.glCompileShader(C.GLuint(s.V)) +} + +func (f *Functions) CreateBuffer() Buffer { + C.glGenBuffers(1, &f.uints[0]) + return Buffer{uint(f.uints[0])} +} + +func (f *Functions) CreateFramebuffer() Framebuffer { + C.glGenFramebuffers(1, &f.uints[0]) + return Framebuffer{uint(f.uints[0])} +} + +func (f *Functions) CreateProgram() Program { + return Program{uint(C.glCreateProgram())} +} + +func (f *Functions) CreateQuery() Query { + C.gio_glGenQueries(1, &f.uints[0]) + return Query{uint(f.uints[0])} +} + +func (f *Functions) CreateRenderbuffer() Renderbuffer { + C.glGenRenderbuffers(1, &f.uints[0]) + return Renderbuffer{uint(f.uints[0])} +} + +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader{uint(C.glCreateShader(C.GLenum(ty)))} +} + +func (f *Functions) CreateTexture() Texture { + C.glGenTextures(1, &f.uints[0]) + return Texture{uint(f.uints[0])} +} + +func (f *Functions) DeleteBuffer(v Buffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteBuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteFramebuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteProgram(p Program) { + C.glDeleteProgram(C.GLuint(p.V)) +} + +func (f *Functions) DeleteQuery(query Query) { + f.uints[0] = C.GLuint(query.V) + C.gio_glDeleteQueries(1, &f.uints[0]) +} + +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteRenderbuffers(1, &f.uints[0]) +} + +func (f *Functions) DeleteShader(s Shader) { + C.glDeleteShader(C.GLuint(s.V)) +} + +func (f *Functions) DeleteTexture(v Texture) { + f.uints[0] = C.GLuint(v.V) + C.glDeleteTextures(1, &f.uints[0]) +} + +func (f *Functions) DepthFunc(v Enum) { + C.glDepthFunc(C.GLenum(v)) +} + +func (f *Functions) DepthMask(mask bool) { + m := C.GLboolean(C.GL_FALSE) + if mask { + m = C.GLboolean(C.GL_TRUE) + } + C.glDepthMask(m) +} + +func (f *Functions) DisableVertexAttribArray(a Attrib) { + C.glDisableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Disable(cap Enum) { + C.glDisable(C.GLenum(cap)) +} + +func (f *Functions) DrawArrays(mode Enum, first int, count int) { + C.glDrawArrays(C.GLenum(mode), C.GLint(first), C.GLsizei(count)) +} + +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + C.gio_glDrawElements(C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset)) +} + +func (f *Functions) DispatchCompute(x, y, z int) { + C.gio_glDispatchCompute(C.GLuint(x), C.GLuint(y), C.GLuint(z)) +} + +func (f *Functions) Enable(cap Enum) { + C.glEnable(C.GLenum(cap)) +} + +func (f *Functions) EndQuery(target Enum) { + C.gio_glEndQuery(C.GLenum(target)) +} + +func (f *Functions) EnableVertexAttribArray(a Attrib) { + C.glEnableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Finish() { + C.glFinish() +} + +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + C.glFramebufferRenderbuffer(C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer.V)) +} + +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + C.glFramebufferTexture2D(C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t.V), C.GLint(level)) +} + +func (c *Functions) GetBinding(pname Enum) Object { + return Object{uint(c.GetInteger(pname))} +} + +func (f *Functions) GetError() Enum { + return Enum(C.glGetError()) +} + +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + C.glGetRenderbufferParameteriv(C.GLenum(target), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + C.glGetFramebufferAttachmentParameteriv(C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetInteger(pname Enum) int { + C.glGetIntegerv(C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetProgrami(p Program, pname Enum) int { + C.glGetProgramiv(C.GLuint(p.V), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetProgramBinary(p Program) []byte { + sz := f.GetProgrami(p, PROGRAM_BINARY_LENGTH) + if sz == 0 { + return nil + } + buf := make([]byte, sz) + var format C.GLenum + C.gio_glGetProgramBinary(C.GLuint(p.V), C.GLsizei(sz), nil, &format, unsafe.Pointer(&buf[0])) + return buf +} + +func (f *Functions) GetProgramInfoLog(p Program) string { + n := f.GetProgrami(p, INFO_LOG_LENGTH) + buf := make([]byte, n) + C.glGetProgramInfoLog(C.GLuint(p.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf) +} + +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + C.gio_glGetQueryObjectuiv(C.GLuint(query.V), C.GLenum(pname), &f.uints[0]) + return uint(f.uints[0]) +} + +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + C.glGetShaderiv(C.GLuint(s.V), C.GLenum(pname), &f.ints[0]) + return int(f.ints[0]) +} + +func (f *Functions) GetShaderInfoLog(s Shader) string { + n := f.GetShaderi(s, INFO_LOG_LENGTH) + buf := make([]byte, n) + C.glGetShaderInfoLog(C.GLuint(s.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf) +} + +func (f *Functions) GetStringi(pname Enum, index int) string { + str := C.gio_glGetStringi(C.GLenum(pname), C.GLuint(index)) + if str == nil { + return "" + } + return C.GoString((*C.char)(unsafe.Pointer(str))) +} + +func (f *Functions) GetString(pname Enum) string { + switch { + case runtime.GOOS == "darwin" && pname == EXTENSIONS: + // macOS OpenGL 3 core profile doesn't support glGetString(GL_EXTENSIONS). + // Use glGetStringi(GL_EXTENSIONS, ). + var exts []string + nexts := f.GetInteger(NUM_EXTENSIONS) + for i := 0; i < nexts; i++ { + ext := f.GetStringi(EXTENSIONS, i) + exts = append(exts, ext) + } + return strings.Join(exts, " ") + default: + str := C.glGetString(C.GLenum(pname)) + return C.GoString((*C.char)(unsafe.Pointer(str))) + } +} + +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return uint(C.gio_glGetUniformBlockIndex(C.GLuint(p.V), cname)) +} + +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return Uniform{int(C.glGetUniformLocation(C.GLuint(p.V), cname))} +} + +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + C.gio_glInvalidateFramebuffer(C.GLenum(target), C.GLenum(attachment)) +} + +func (f *Functions) LinkProgram(p Program) { + C.glLinkProgram(C.GLuint(p.V)) +} + +func (f *Functions) PixelStorei(pname Enum, param int32) { + C.glPixelStorei(C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) MemoryBarrier(barriers Enum) { + C.gio_glMemoryBarrier(C.GLbitfield(barriers)) +} + +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + p := C.gio_glMapBufferRange(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(length), C.GLbitfield(access)) + if p == nil { + return nil + } + return (*[1 << 30]byte)(p)[:length:length] +} + +func (f *Functions) Scissor(x, y, width, height int32) { + C.glScissor(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glReadPixels(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + C.glRenderbufferStorage(C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ShaderSource(s Shader, src string) { + csrc := C.CString(src) + defer C.free(unsafe.Pointer(csrc)) + strlen := C.GLint(len(src)) + C.glShaderSource(C.GLuint(s.V), 1, &csrc, &strlen) +} + +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { + C.glTexImage2D(C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), nil) +} + +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + C.gio_glTexStorage2D(C.GLenum(target), C.GLsizei(levels), C.GLenum(internalFormat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glTexSubImage2D(C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) TexParameteri(target, pname Enum, param int) { + C.glTexParameteri(C.GLenum(target), C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + C.gio_glUniformBlockBinding(C.GLuint(p.V), C.GLuint(uniformBlockIndex), C.GLuint(uniformBlockBinding)) +} + +func (f *Functions) Uniform1f(dst Uniform, v float32) { + C.glUniform1f(C.GLint(dst.V), C.GLfloat(v)) +} + +func (f *Functions) Uniform1i(dst Uniform, v int) { + C.glUniform1i(C.GLint(dst.V), C.GLint(v)) +} + +func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) { + C.glUniform2f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1)) +} + +func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) { + C.glUniform3f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2)) +} + +func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) { + C.glUniform4f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3)) +} + +func (f *Functions) UseProgram(p Program) { + C.glUseProgram(C.GLuint(p.V)) +} + +func (f *Functions) UnmapBuffer(target Enum) bool { + r := C.gio_glUnmapBuffer(C.GLenum(target)) + return r == C.GL_TRUE +} + +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) { + var n C.GLboolean = C.GL_FALSE + if normalized { + n = C.GL_TRUE + } + C.gio_glVertexAttribPointer(C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset)) +} + +func (f *Functions) Viewport(x int, y int, width int, height int) { + C.glViewport(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} diff --git a/pkg/gel/gio/internal/gl/gl_windows.go b/pkg/gel/gio/internal/gl/gl_windows.go new file mode 100644 index 0000000..099c82b --- /dev/null +++ b/pkg/gel/gio/internal/gl/gl_windows.go @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "math" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll") + _glActiveTexture = LibGLESv2.NewProc("glActiveTexture") + _glAttachShader = LibGLESv2.NewProc("glAttachShader") + _glBeginQuery = LibGLESv2.NewProc("glBeginQuery") + _glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation") + _glBindBuffer = LibGLESv2.NewProc("glBindBuffer") + _glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase") + _glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer") + _glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer") + _glBindTexture = LibGLESv2.NewProc("glBindTexture") + _glBlendEquation = LibGLESv2.NewProc("glBlendEquation") + _glBlendFunc = LibGLESv2.NewProc("glBlendFunc") + _glBufferData = LibGLESv2.NewProc("glBufferData") + _glBufferSubData = LibGLESv2.NewProc("glBufferSubData") + _glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus") + _glClear = LibGLESv2.NewProc("glClear") + _glClearColor = LibGLESv2.NewProc("glClearColor") + _glClearDepthf = LibGLESv2.NewProc("glClearDepthf") + _glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries") + _glCompileShader = LibGLESv2.NewProc("glCompileShader") + _glGenBuffers = LibGLESv2.NewProc("glGenBuffers") + _glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers") + _glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex") + _glCreateProgram = LibGLESv2.NewProc("glCreateProgram") + _glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers") + _glCreateShader = LibGLESv2.NewProc("glCreateShader") + _glGenTextures = LibGLESv2.NewProc("glGenTextures") + _glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers") + _glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers") + _glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram") + _glDeleteShader = LibGLESv2.NewProc("glDeleteShader") + _glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers") + _glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures") + _glDepthFunc = LibGLESv2.NewProc("glDepthFunc") + _glDepthMask = LibGLESv2.NewProc("glDepthMask") + _glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray") + _glDisable = LibGLESv2.NewProc("glDisable") + _glDrawArrays = LibGLESv2.NewProc("glDrawArrays") + _glDrawElements = LibGLESv2.NewProc("glDrawElements") + _glEnable = LibGLESv2.NewProc("glEnable") + _glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray") + _glEndQuery = LibGLESv2.NewProc("glEndQuery") + _glFinish = LibGLESv2.NewProc("glFinish") + _glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer") + _glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D") + _glGenQueries = LibGLESv2.NewProc("glGenQueries") + _glGetError = LibGLESv2.NewProc("glGetError") + _glGetRenderbufferParameteri = LibGLESv2.NewProc("glGetRenderbufferParameteri") + _glGetFramebufferAttachmentParameteri = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteri") + _glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv") + _glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv") + _glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog") + _glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv") + _glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv") + _glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog") + _glGetString = LibGLESv2.NewProc("glGetString") + _glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation") + _glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer") + _glLinkProgram = LibGLESv2.NewProc("glLinkProgram") + _glPixelStorei = LibGLESv2.NewProc("glPixelStorei") + _glReadPixels = LibGLESv2.NewProc("glReadPixels") + _glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage") + _glScissor = LibGLESv2.NewProc("glScissor") + _glShaderSource = LibGLESv2.NewProc("glShaderSource") + _glTexImage2D = LibGLESv2.NewProc("glTexImage2D") + _glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D") + _glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D") + _glTexParameteri = LibGLESv2.NewProc("glTexParameteri") + _glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding") + _glUniform1f = LibGLESv2.NewProc("glUniform1f") + _glUniform1i = LibGLESv2.NewProc("glUniform1i") + _glUniform2f = LibGLESv2.NewProc("glUniform2f") + _glUniform3f = LibGLESv2.NewProc("glUniform3f") + _glUniform4f = LibGLESv2.NewProc("glUniform4f") + _glUseProgram = LibGLESv2.NewProc("glUseProgram") + _glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer") + _glViewport = LibGLESv2.NewProc("glViewport") +) + +type Functions struct { + // Query caches. + int32s [100]int32 +} + +type Context interface{} + +func NewFunctions(ctx Context) (*Functions, error) { + if ctx != nil { + panic("non-nil context") + } + return new(Functions), nil +} + +func (c *Functions) ActiveTexture(t Enum) { + syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0) +} +func (c *Functions) AttachShader(p Program, s Shader) { + syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p.V), uintptr(s.V), 0) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query.V), 0) +} +func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := cString(name) + c0 := &cname[0] + syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p.V), uintptr(a), uintptr(unsafe.Pointer(c0))) + issue34474KeepAlive(c) +} +func (c *Functions) BindBuffer(target Enum, b Buffer) { + syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b.V), 0) +} +func (c *Functions) BindBufferBase(target Enum, index int, b Buffer) { + syscall.Syscall(_glBindBufferBase.Addr(), 3, uintptr(target), uintptr(index), uintptr(b.V)) +} +func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb.V), 0) +} +func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb.V), 0) +} +func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { + panic("not implemented") +} +func (c *Functions) BindTexture(target Enum, t Texture) { + syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t.V), 0) +} +func (c *Functions) BlendEquation(mode Enum) { + syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0) +} +func (c *Functions) BlendFunc(sfactor, dfactor Enum) { + syscall.Syscall(_glBlendFunc.Addr(), 2, uintptr(sfactor), uintptr(dfactor), 0) +} +func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { + panic("not implemented") +} +func (c *Functions) BufferData(target Enum, size int, usage Enum) { + syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(size), 0, uintptr(usage), 0, 0) +} +func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { + if n := len(src); n > 0 { + s0 := &src[0] + syscall.Syscall6(_glBufferSubData.Addr(), 4, uintptr(target), uintptr(offset), uintptr(n), uintptr(unsafe.Pointer(s0)), 0, 0) + issue34474KeepAlive(s0) + } +} +func (c *Functions) CheckFramebufferStatus(target Enum) Enum { + s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0) + return Enum(s) +} +func (c *Functions) Clear(mask Enum) { + syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0) +} +func (c *Functions) ClearColor(red, green, blue, alpha float32) { + syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0) +} +func (c *Functions) ClearDepthf(d float32) { + syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0) +} +func (c *Functions) CompileShader(s Shader) { + syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s.V), 0, 0) +} +func (c *Functions) CreateBuffer() Buffer { + var buf uintptr + syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0) + return Buffer{uint(buf)} +} +func (c *Functions) CreateFramebuffer() Framebuffer { + var fb uintptr + syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0) + return Framebuffer{uint(fb)} +} +func (c *Functions) CreateProgram() Program { + p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0) + return Program{uint(p)} +} +func (f *Functions) CreateQuery() Query { + var q uintptr + syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0) + return Query{uint(q)} +} +func (c *Functions) CreateRenderbuffer() Renderbuffer { + var rb uintptr + syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0) + return Renderbuffer{uint(rb)} +} +func (c *Functions) CreateShader(ty Enum) Shader { + s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0) + return Shader{uint(s)} +} +func (c *Functions) CreateTexture() Texture { + var t uintptr + syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0) + return Texture{uint(t)} +} +func (c *Functions) DeleteBuffer(v Buffer) { + syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DeleteFramebuffer(v Framebuffer) { + syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DeleteProgram(p Program) { + syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (f *Functions) DeleteQuery(query Query) { + syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query.V)), 0) +} +func (c *Functions) DeleteShader(s Shader) { + syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s.V), 0, 0) +} +func (c *Functions) DeleteRenderbuffer(v Renderbuffer) { + syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DeleteTexture(v Texture) { + syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) +} +func (c *Functions) DepthFunc(f Enum) { + syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0) +} +func (c *Functions) DepthMask(mask bool) { + var m uintptr + if mask { + m = 1 + } + syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0) +} +func (c *Functions) DisableVertexAttribArray(a Attrib) { + syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (c *Functions) Disable(cap Enum) { + syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) DrawArrays(mode Enum, first, count int) { + syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count)) +} +func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0) +} +func (f *Functions) DispatchCompute(x, y, z int) { + panic("not implemented") +} +func (c *Functions) Enable(cap Enum) { + syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) EnableVertexAttribArray(a Attrib) { + syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (f *Functions) EndQuery(target Enum) { + syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0) +} +func (c *Functions) Finish() { + syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0) +} +func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer.V), 0, 0) +} +func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t.V), uintptr(level), 0) +} +func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { + cname := cString(name) + c0 := &cname[0] + u, _, _ := syscall.Syscall(_glGetUniformBlockIndex.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) + issue34474KeepAlive(c0) + return uint(u) +} +func (c *Functions) GetBinding(pname Enum) Object { + return Object{uint(c.GetInteger(pname))} +} +func (c *Functions) GetError() Enum { + e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0) + return Enum(e) +} +func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetRenderbufferParameteri.Addr(), 2, uintptr(target), uintptr(pname), 0) + return int(p) +} +func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname)) + return int(p) +} +func (c *Functions) GetInteger(pname Enum) int { + syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0) + return int(c.int32s[0]) +} +func (c *Functions) GetProgrami(p Program, pname Enum) int { + syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return int(c.int32s[0]) +} +func (c *Functions) GetProgramInfoLog(p Program) string { + n := c.GetProgrami(p, INFO_LOG_LENGTH) + buf := make([]byte, n) + syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf) +} +func (c *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return uint(c.int32s[0]) +} +func (c *Functions) GetShaderi(s Shader, pname Enum) int { + syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) + return int(c.int32s[0]) +} +func (c *Functions) GetShaderInfoLog(s Shader) string { + n := c.GetShaderi(s, INFO_LOG_LENGTH) + buf := make([]byte, n) + syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf) +} +func (c *Functions) GetString(pname Enum) string { + s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0) + return windows.BytePtrToString((*byte)(unsafe.Pointer(s))) +} +func (c *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := cString(name) + c0 := &cname[0] + u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) + issue34474KeepAlive(c0) + return Uniform{int(u)} +} +func (c *Functions) InvalidateFramebuffer(target, attachment Enum) { + addr := _glInvalidateFramebuffer.Addr() + if addr == 0 { + // InvalidateFramebuffer is just a hint. Skip it if not supported. + return + } + syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment))) +} +func (c *Functions) LinkProgram(p Program) { + syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (c *Functions) PixelStorei(pname Enum, param int32) { + syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0) +} +func (f *Functions) MemoryBarrier(barriers Enum) { + panic("not implemented") +} +func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { + panic("not implemented") +} +func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { + d0 := &data[0] + syscall.Syscall9(_glReadPixels.Addr(), 7, uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)), 0, 0) + issue34474KeepAlive(d0) +} +func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) Scissor(x, y, width, height int32) { + syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) ShaderSource(s Shader, src string) { + var n uintptr = uintptr(len(src)) + psrc := &src + syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s.V), 1, uintptr(unsafe.Pointer(psrc)), uintptr(unsafe.Pointer(&n)), 0, 0) + issue34474KeepAlive(psrc) +} +func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { + syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0) +} +func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { + syscall.Syscall6(_glTexStorage2D.Addr(), 5, uintptr(target), uintptr(levels), uintptr(internalFormat), uintptr(width), uintptr(height), 0) +} +func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + d0 := &data[0] + syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0))) + issue34474KeepAlive(d0) +} +func (c *Functions) TexParameteri(target, pname Enum, param int) { + syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param)) +} +func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { + syscall.Syscall(_glUniformBlockBinding.Addr(), 3, uintptr(p.V), uintptr(uniformBlockIndex), uintptr(uniformBlockBinding)) +} +func (c *Functions) Uniform1f(dst Uniform, v float32) { + syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst.V), uintptr(math.Float32bits(v)), 0) +} +func (c *Functions) Uniform1i(dst Uniform, v int) { + syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst.V), uintptr(v), 0) +} +func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1))) +} +func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0) +} +func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0) +} +func (c *Functions) UseProgram(p Program) { + syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p.V), 0, 0) +} +func (f *Functions) UnmapBuffer(target Enum) bool { + panic("not implemented") +} +func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + var norm uintptr + if normalized { + norm = 1 + } + syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset)) +} +func (c *Functions) Viewport(x, y, width, height int) { + syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} + +func cString(s string) []byte { + b := make([]byte, len(s)+1) + copy(b, s) + return b +} + +// issue34474KeepAlive calls runtime.KeepAlive as a +// workaround for golang.org/issue/34474. +func issue34474KeepAlive(v interface{}) { + runtime.KeepAlive(v) +} diff --git a/pkg/gel/gio/internal/gl/types.go b/pkg/gel/gio/internal/gl/types.go new file mode 100644 index 0000000..45db3be --- /dev/null +++ b/pkg/gel/gio/internal/gl/types.go @@ -0,0 +1,27 @@ +// +build !js + +package gl + +type ( + Buffer struct{ V uint } + Framebuffer struct{ V uint } + Program struct{ V uint } + Renderbuffer struct{ V uint } + Shader struct{ V uint } + Texture struct{ V uint } + Query struct{ V uint } + Uniform struct{ V int } + Object struct{ V uint } +) + +func (u Uniform) Valid() bool { + return u.V != -1 +} + +func (p Program) Valid() bool { + return p.V != 0 +} + +func (s Shader) Valid() bool { + return s.V != 0 +} diff --git a/pkg/gel/gio/internal/gl/types_js.go b/pkg/gel/gio/internal/gl/types_js.go new file mode 100644 index 0000000..584c2af --- /dev/null +++ b/pkg/gel/gio/internal/gl/types_js.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import "syscall/js" + +type ( + Buffer js.Value + Framebuffer js.Value + Program js.Value + Renderbuffer js.Value + Shader js.Value + Texture js.Value + Query js.Value + Uniform js.Value + Object js.Value +) + +func (p Program) Valid() bool { + return !js.Value(p).IsUndefined() && !js.Value(p).IsNull() +} + +func (s Shader) Valid() bool { + return !js.Value(s).IsUndefined() && !js.Value(s).IsNull() +} + +func (u Uniform) Valid() bool { + return !js.Value(u).IsUndefined() && !js.Value(u).IsNull() +} diff --git a/pkg/gel/gio/internal/gl/util.go b/pkg/gel/gio/internal/gl/util.go new file mode 100644 index 0000000..3d5b44b --- /dev/null +++ b/pkg/gel/gio/internal/gl/util.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "fmt" + "strings" +) + +func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) { + vs, err := createShader(ctx, VERTEX_SHADER, vsSrc) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(vs) + fs, err := createShader(ctx, FRAGMENT_SHADER, fsSrc) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(fs) + prog := ctx.CreateProgram() + if !prog.Valid() { + return Program{}, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, vs) + ctx.AttachShader(prog, fs) + for i, a := range attribs { + ctx.BindAttribLocation(prog, Attrib(i), a) + } + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func CreateComputeProgram(ctx *Functions, src string) (Program, error) { + cs, err := createShader(ctx, COMPUTE_SHADER, src) + if err != nil { + return Program{}, err + } + defer ctx.DeleteShader(cs) + prog := ctx.CreateProgram() + if !prog.Valid() { + return Program{}, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, cs) + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func createShader(ctx *Functions, typ Enum, src string) (Shader, error) { + sh := ctx.CreateShader(typ) + if !sh.Valid() { + return Shader{}, errors.New("glCreateShader failed") + } + ctx.ShaderSource(sh, src) + ctx.CompileShader(sh) + if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 { + log := ctx.GetShaderInfoLog(sh) + ctx.DeleteShader(sh) + return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log)) + } + return sh, nil +} + +func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) { + var ver [2]int + if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil { + return ver, true, nil + } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil { + // WebGL major version v corresponds to OpenGL ES version v + 1 + ver[0]++ + return ver, true, nil + } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil { + return ver, false, nil + } + return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer) +} diff --git a/pkg/gel/gio/internal/opconst/ops.go b/pkg/gel/gio/internal/opconst/ops.go new file mode 100644 index 0000000..db9dd8d --- /dev/null +++ b/pkg/gel/gio/internal/opconst/ops.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package opconst + +type OpType byte + +// Start at a high number for easier debugging. +const firstOpIndex = 200 + +const ( + TypeMacro OpType = iota + firstOpIndex + TypeCall + TypeDefer + TypeTransform + TypeInvalidate + TypeImage + TypePaint + TypeColor + TypeLinearGradient + TypeArea + TypePointerInput + TypePass + TypeClipboardRead + TypeClipboardWrite + TypeKeyInput + TypeKeyFocus + TypeKeySoftKeyboard + TypeSave + TypeLoad + TypeAux + TypeClip + TypeProfile + TypeCursor + TypePath + TypeStroke +) + +const ( + TypeMacroLen = 1 + 4 + 4 + TypeCallLen = 1 + 4 + 4 + TypeDeferLen = 1 + TypeTransformLen = 1 + 4*6 + TypeRedrawLen = 1 + 8 + TypeImageLen = 1 + TypePaintLen = 1 + TypeColorLen = 1 + 4 + TypeLinearGradientLen = 1 + 8*2 + 4*2 + TypeAreaLen = 1 + 1 + 4*4 + TypePointerInputLen = 1 + 1 + 1 + 2*4 + 2*4 + TypePassLen = 1 + 1 + TypeClipboardReadLen = 1 + TypeClipboardWriteLen = 1 + TypeKeyInputLen = 1 + TypeKeyFocusLen = 1 + TypeKeySoftKeyboardLen = 1 + 1 + TypeSaveLen = 1 + 4 + TypeLoadLen = 1 + 1 + 4 + TypeAuxLen = 1 + TypeClipLen = 1 + 4*4 + 1 + TypeProfileLen = 1 + TypeCursorLen = 1 + 1 + TypePathLen = 1 + TypeStrokeLen = 1 + 4 +) + +// StateMask is a bitmask of state types a load operation +// should restore. +type StateMask uint8 + +const ( + TransformState StateMask = 1 << iota + + AllState = ^StateMask(0) +) + +// InitialStateID is the ID for saving and loading +// the initial operation state. +const InitialStateID = 0 + +func (t OpType) Size() int { + return [...]int{ + TypeMacroLen, + TypeCallLen, + TypeDeferLen, + TypeTransformLen, + TypeRedrawLen, + TypeImageLen, + TypePaintLen, + TypeColorLen, + TypeLinearGradientLen, + TypeAreaLen, + TypePointerInputLen, + TypePassLen, + TypeClipboardReadLen, + TypeClipboardWriteLen, + TypeKeyInputLen, + TypeKeyFocusLen, + TypeKeySoftKeyboardLen, + TypeSaveLen, + TypeLoadLen, + TypeAuxLen, + TypeClipLen, + TypeProfileLen, + TypeCursorLen, + TypePathLen, + TypeStrokeLen, + }[t-firstOpIndex] +} + +func (t OpType) NumRefs() int { + switch t { + case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor: + return 1 + case TypeImage: + return 2 + default: + return 0 + } +} diff --git a/pkg/gel/gio/internal/ops/ops.go b/pkg/gel/gio/internal/ops/ops.go new file mode 100644 index 0000000..b1c7b6a --- /dev/null +++ b/pkg/gel/gio/internal/ops/ops.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ops + +import ( + "encoding/binary" + "math" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/scene" +) + +func DecodeCommand(d []byte) scene.Command { + var cmd scene.Command + copy(byteslice.Uint32(cmd[:]), d) + return cmd +} + +func EncodeCommand(out []byte, cmd scene.Command) { + copy(out, byteslice.Uint32(cmd[:])) +} + +func DecodeTransform(data []byte) (t f32.Affine2D) { + if opconst.OpType(data[0]) != opconst.TypeTransform { + panic("invalid op") + } + data = data[1:] + data = data[:4*6] + + bo := binary.LittleEndian + a := math.Float32frombits(bo.Uint32(data)) + b := math.Float32frombits(bo.Uint32(data[4*1:])) + c := math.Float32frombits(bo.Uint32(data[4*2:])) + d := math.Float32frombits(bo.Uint32(data[4*3:])) + e := math.Float32frombits(bo.Uint32(data[4*4:])) + f := math.Float32frombits(bo.Uint32(data[4*5:])) + return f32.NewAffine2D(a, b, c, d, e, f) +} + +// DecodeSave decodes the state id of a save op. +func DecodeSave(data []byte) int { + if opconst.OpType(data[0]) != opconst.TypeSave { + panic("invalid op") + } + bo := binary.LittleEndian + return int(bo.Uint32(data[1:])) +} + +// DecodeLoad decodes the state id and mask of a load op. +func DecodeLoad(data []byte) (int, opconst.StateMask) { + if opconst.OpType(data[0]) != opconst.TypeLoad { + panic("invalid op") + } + bo := binary.LittleEndian + return int(bo.Uint32(data[2:])), opconst.StateMask(data[1]) +} diff --git a/pkg/gel/gio/internal/ops/reader.go b/pkg/gel/gio/internal/ops/reader.go new file mode 100644 index 0000000..63e62be --- /dev/null +++ b/pkg/gel/gio/internal/ops/reader.go @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ops + +import ( + "encoding/binary" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Reader parses an ops list. +type Reader struct { + pc PC + stack []macro + ops *op.Ops + deferOps op.Ops + deferDone bool +} + +// EncodedOp represents an encoded op returned by +// Reader. +type EncodedOp struct { + Key Key + Data []byte + Refs []interface{} +} + +// Key is a unique key for a given op. +type Key struct { + ops *op.Ops + pc int + version int + sx, hx, sy, hy float32 +} + +// Shadow of op.MacroOp. +type macroOp struct { + ops *op.Ops + pc PC +} + +// PC is an instruction counter for an operation list. +type PC struct { + data int + refs int +} + +type macro struct { + ops *op.Ops + retPC PC + endPC PC +} + +type opMacroDef struct { + endpc PC +} + +// Reset start reading from the beginning of ops. +func (r *Reader) Reset(ops *op.Ops) { + r.ResetAt(ops, PC{}) +} + +// ResetAt is like Reset, except it starts reading from pc. +func (r *Reader) ResetAt(ops *op.Ops, pc PC) { + r.stack = r.stack[:0] + r.deferOps.Reset() + r.deferDone = false + r.pc = pc + r.ops = ops +} + +// NewPC returns a PC representing the current instruction counter of +// ops. +func NewPC(ops *op.Ops) PC { + return PC{ + data: len(ops.Data()), + refs: len(ops.Refs()), + } +} + +func (k Key) SetTransform(t f32.Affine2D) Key { + sx, hx, _, hy, sy, _ := t.Elems() + k.sx = sx + k.hx = hx + k.hy = hy + k.sy = sy + return k +} + +func (r *Reader) Decode() (EncodedOp, bool) { + if r.ops == nil { + return EncodedOp{}, false + } + deferring := false + for { + if len(r.stack) > 0 { + b := r.stack[len(r.stack)-1] + if r.pc == b.endPC { + r.ops = b.ops + r.pc = b.retPC + r.stack = r.stack[:len(r.stack)-1] + continue + } + } + data := r.ops.Data() + data = data[r.pc.data:] + refs := r.ops.Refs() + if len(data) == 0 { + if r.deferDone { + return EncodedOp{}, false + } + r.deferDone = true + // Execute deferred macros. + r.ops = &r.deferOps + r.pc = PC{} + continue + } + key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.Version()} + t := opconst.OpType(data[0]) + n := t.Size() + nrefs := t.NumRefs() + data = data[:n] + refs = refs[r.pc.refs:] + refs = refs[:nrefs] + switch t { + case opconst.TypeDefer: + deferring = true + r.pc.data += n + r.pc.refs += nrefs + continue + case opconst.TypeAux: + // An Aux operations is always wrapped in a macro, and + // its length is the remaining space. + block := r.stack[len(r.stack)-1] + n += block.endPC.data - r.pc.data - opconst.TypeAuxLen + data = data[:n] + case opconst.TypeCall: + if deferring { + deferring = false + // Copy macro for deferred execution. + if t.NumRefs() != 1 { + panic("internal error: unexpected number of macro refs") + } + deferData := r.deferOps.Write1(t.Size(), refs[0]) + copy(deferData, data) + continue + } + var op macroOp + op.decode(data, refs) + macroData := op.ops.Data()[op.pc.data:] + if opconst.OpType(macroData[0]) != opconst.TypeMacro { + panic("invalid macro reference") + } + var opDef opMacroDef + opDef.decode(macroData[:opconst.TypeMacro.Size()]) + retPC := r.pc + retPC.data += n + retPC.refs += nrefs + r.stack = append(r.stack, macro{ + ops: r.ops, + retPC: retPC, + endPC: opDef.endpc, + }) + r.ops = op.ops + r.pc = op.pc + r.pc.data += opconst.TypeMacro.Size() + r.pc.refs += opconst.TypeMacro.NumRefs() + continue + case opconst.TypeMacro: + var op opMacroDef + op.decode(data) + r.pc = op.endpc + continue + } + r.pc.data += n + r.pc.refs += nrefs + return EncodedOp{Key: key, Data: data, Refs: refs}, true + } +} + +func (op *opMacroDef) decode(data []byte) { + if opconst.OpType(data[0]) != opconst.TypeMacro { + panic("invalid op") + } + bo := binary.LittleEndian + data = data[:9] + dataIdx := int(int32(bo.Uint32(data[1:]))) + refsIdx := int(int32(bo.Uint32(data[5:]))) + *op = opMacroDef{ + endpc: PC{ + data: dataIdx, + refs: refsIdx, + }, + } +} + +func (m *macroOp) decode(data []byte, refs []interface{}) { + if opconst.OpType(data[0]) != opconst.TypeCall { + panic("invalid op") + } + data = data[:9] + bo := binary.LittleEndian + dataIdx := int(int32(bo.Uint32(data[1:]))) + refsIdx := int(int32(bo.Uint32(data[5:]))) + *m = macroOp{ + ops: refs[0].(*op.Ops), + pc: PC{ + data: dataIdx, + refs: refsIdx, + }, + } +} diff --git a/pkg/gel/gio/internal/scene/scene.go b/pkg/gel/gio/internal/scene/scene.go new file mode 100644 index 0000000..b3ade1b --- /dev/null +++ b/pkg/gel/gio/internal/scene/scene.go @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package scene encodes and decodes graphics commands in the format used by the +// compute renderer. +package scene + +import ( + "fmt" + "image/color" + "math" + "unsafe" + + "github.com/p9c/p9/pkg/gel/gio/f32" +) + +type Op uint32 + +type Command [sceneElemSize / 4]uint32 + +// GPU commands from scene.h +const ( + OpNop Op = iota + OpLine + OpQuad + OpCubic + OpFillColor + OpLineWidth + OpTransform + OpBeginClip + OpEndClip + OpFillImage + OpSetFillMode +) + +// FillModes, from setup.h. +type FillMode uint32 + +const ( + FillModeNonzero = 0 + FillModeStroke = 1 +) + +const CommandSize = int(unsafe.Sizeof(Command{})) + +const sceneElemSize = 36 + +func (c Command) Op() Op { + return Op(c[0]) +} + +func (c Command) String() string { + switch Op(c[0]) { + case OpNop: + return "nop" + case OpLine: + from, to := DecodeLine(c) + return fmt.Sprintf("line(%v, %v)", from, to) + case OpQuad: + from, ctrl, to := DecodeQuad(c) + return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to) + case OpCubic: + from, ctrl0, ctrl1, to := DecodeCubic(c) + return fmt.Sprintf("cubic(%v, %v, %v, %v)", from, ctrl0, ctrl1, to) + case OpFillColor: + return "fillcolor" + case OpLineWidth: + return "linewidth" + case OpTransform: + t := f32.NewAffine2D( + math.Float32frombits(c[1]), + math.Float32frombits(c[3]), + math.Float32frombits(c[5]), + math.Float32frombits(c[2]), + math.Float32frombits(c[4]), + math.Float32frombits(c[6]), + ) + return fmt.Sprintf("transform (%v)", t) + case OpBeginClip: + bounds := f32.Rectangle{ + Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), + Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), + } + return fmt.Sprintf("beginclip (%v)", bounds) + case OpEndClip: + bounds := f32.Rectangle{ + Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), + Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), + } + return fmt.Sprintf("endclip (%v)", bounds) + case OpFillImage: + return "fillimage" + case OpSetFillMode: + return "setfillmode" + default: + panic("unreachable") + } +} + +func Line(start, end f32.Point) Command { + return Command{ + 0: uint32(OpLine), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(end.X), + 4: math.Float32bits(end.Y), + } +} + +func Cubic(start, ctrl0, ctrl1, end f32.Point) Command { + return Command{ + 0: uint32(OpCubic), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(ctrl0.X), + 4: math.Float32bits(ctrl0.Y), + 5: math.Float32bits(ctrl1.X), + 6: math.Float32bits(ctrl1.Y), + 7: math.Float32bits(end.X), + 8: math.Float32bits(end.Y), + } +} + +func Quad(start, ctrl, end f32.Point) Command { + return Command{ + 0: uint32(OpQuad), + 1: math.Float32bits(start.X), + 2: math.Float32bits(start.Y), + 3: math.Float32bits(ctrl.X), + 4: math.Float32bits(ctrl.Y), + 5: math.Float32bits(end.X), + 6: math.Float32bits(end.Y), + } +} + +func Transform(m f32.Affine2D) Command { + sx, hx, ox, hy, sy, oy := m.Elems() + return Command{ + 0: uint32(OpTransform), + 1: math.Float32bits(sx), + 2: math.Float32bits(hy), + 3: math.Float32bits(hx), + 4: math.Float32bits(sy), + 5: math.Float32bits(ox), + 6: math.Float32bits(oy), + } +} + +func SetLineWidth(width float32) Command { + return Command{ + 0: uint32(OpLineWidth), + 1: math.Float32bits(width), + } +} + +func BeginClip(bbox f32.Rectangle) Command { + return Command{ + 0: uint32(OpBeginClip), + 1: math.Float32bits(bbox.Min.X), + 2: math.Float32bits(bbox.Min.Y), + 3: math.Float32bits(bbox.Max.X), + 4: math.Float32bits(bbox.Max.Y), + } +} + +func EndClip(bbox f32.Rectangle) Command { + return Command{ + 0: uint32(OpEndClip), + 1: math.Float32bits(bbox.Min.X), + 2: math.Float32bits(bbox.Min.Y), + 3: math.Float32bits(bbox.Max.X), + 4: math.Float32bits(bbox.Max.Y), + } +} + +func FillColor(col color.RGBA) Command { + return Command{ + 0: uint32(OpFillColor), + 1: uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A), + } +} + +func FillImage(index int) Command { + return Command{ + 0: uint32(OpFillImage), + 1: uint32(index), + } +} + +func SetFillMode(mode FillMode) Command { + return Command{ + 0: uint32(OpSetFillMode), + 1: uint32(mode), + } +} + +func DecodeLine(cmd Command) (from, to f32.Point) { + if cmd[0] != uint32(OpLine) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + return +} + +func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) { + if cmd[0] != uint32(OpQuad) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + ctrl = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) + return +} + +func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) { + if cmd[0] != uint32(OpCubic) { + panic("invalid command") + } + from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) + ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) + ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) + to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8])) + return +} diff --git a/pkg/gel/gio/internal/srgb/srgb.go b/pkg/gel/gio/internal/srgb/srgb.go new file mode 100644 index 0000000..53d1bbe --- /dev/null +++ b/pkg/gel/gio/internal/srgb/srgb.go @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package srgb + +import ( + "fmt" + "runtime" + "strings" + + "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" + "github.com/p9c/p9/pkg/gel/gio/internal/gl" +) + +// FBO implements an intermediate sRGB FBO +// for gamma-correct rendering on platforms without +// sRGB enabled native framebuffers. +type FBO struct { + c *gl.Functions + width, height int + frameBuffer gl.Framebuffer + depthBuffer gl.Renderbuffer + colorTex gl.Texture + blitted bool + quad gl.Buffer + prog gl.Program + gl3 bool +} + +func New(ctx gl.Context) (*FBO, error) { + f, err := gl.NewFunctions(ctx) + if err != nil { + return nil, err + } + var gl3 bool + glVer := f.GetString(gl.VERSION) + ver, _, err := gl.ParseGLVersion(glVer) + if err != nil { + return nil, err + } + if ver[0] >= 3 { + gl3 = true + } else { + exts := f.GetString(gl.EXTENSIONS) + if !strings.Contains(exts, "EXT_sRGB") { + return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB") + } + } + s := &FBO{ + c: f, + gl3: gl3, + frameBuffer: f.CreateFramebuffer(), + colorTex: f.CreateTexture(), + depthBuffer: f.CreateRenderbuffer(), + } + f.BindTexture(gl.TEXTURE_2D, s.colorTex) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + return s, nil +} + +func (s *FBO) Blit() { + if !s.blitted { + prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, []string{"pos", "uv"}) + if err != nil { + panic(err) + } + s.prog = prog + s.c.UseProgram(prog) + s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) + s.quad = s.c.CreateBuffer() + s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) + coords := byteslice.Slice([]float32{ + -1, +1, 0, 1, + +1, +1, 1, 1, + -1, -1, 0, 0, + +1, -1, 1, 0, + }) + s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW) + s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords) + s.blitted = true + } + s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{}) + s.c.UseProgram(s.prog) + s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) + s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad) + s.c.VertexAttribPointer(0 /* pos */, 2, gl.FLOAT, false, 4*4, 0) + s.c.VertexAttribPointer(1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2) + s.c.EnableVertexAttribArray(0) + s.c.EnableVertexAttribArray(1) + s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) + s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{}) + s.c.DisableVertexAttribArray(0) + s.c.DisableVertexAttribArray(1) + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) + s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) + s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) + // The Android emulator requires framebuffer 0 bound at eglSwapBuffer time. + // Bind the sRGB framebuffer again in afterPresent. + s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{}) +} + +func (s *FBO) AfterPresent() { + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) +} + +func (s *FBO) Refresh(w, h int) error { + s.width, s.height = w, h + if w == 0 || h == 0 { + return nil + } + s.c.BindTexture(gl.TEXTURE_2D, s.colorTex) + if s.gl3 { + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA, gl.UNSIGNED_BYTE) + } else /* EXT_sRGB */ { + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h, gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE) + } + currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING)) + s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer) + s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) + s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) + s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer) + s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.colorTex, 0) + s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, s.depthBuffer) + if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError()) + } + + if runtime.GOOS == "js" { + // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8 + // texture result in twice gamma corrected colors. Using a plain RGBA + // texture seems to work. + s.c.ClearColor(.5, .5, .5, 1.0) + s.c.Clear(gl.COLOR_BUFFER_BIT) + var pixel [4]byte + s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) + if pixel[0] == 128 { // Correct sRGB color value is ~188 + s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA, gl.UNSIGNED_BYTE) + if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError()) + } + } + } + + return nil +} + +func (s *FBO) Release() { + s.c.DeleteFramebuffer(s.frameBuffer) + s.c.DeleteTexture(s.colorTex) + s.c.DeleteRenderbuffer(s.depthBuffer) + if s.blitted { + s.c.DeleteBuffer(s.quad) + s.c.DeleteProgram(s.prog) + } + s.c = nil +} + +const ( + blitVSrc = ` +#version 100 + +precision highp float; + +attribute vec2 pos; +attribute vec2 uv; + +varying vec2 vUV; + +void main() { + gl_Position = vec4(pos, 0, 1); + vUV = uv; +} +` + blitFSrc = ` +#version 100 + +precision mediump float; + +uniform sampler2D tex; +varying vec2 vUV; + +vec3 gamma(vec3 rgb) { + vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); + vec3 lin = rgb * vec3(12.92); + bvec3 cut = lessThan(rgb, vec3(0.0031308)); + return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); +} + +void main() { + vec4 col = texture2D(tex, vUV); + vec3 rgb = col.rgb; + rgb = gamma(rgb); + gl_FragColor = vec4(rgb, col.a); +} +` +) diff --git a/pkg/gel/gio/internal/stroke/dash.go b/pkg/gel/gio/internal/stroke/dash.go new file mode 100644 index 0000000..fa68189 --- /dev/null +++ b/pkg/gel/gio/internal/stroke/dash.go @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// The algorithms to compute dashes have been extracted, adapted from +// (and used as a reference implementation): +// - github.com/tdewolff/canvas (Licensed under MIT) + +package stroke + +import ( + "math" + "sort" + + "github.com/p9c/p9/pkg/gel/gio/f32" +) + +type DashOp struct { + Phase float32 + Dashes []float32 +} + +func IsSolidLine(sty DashOp) bool { + return sty.Phase == 0 && len(sty.Dashes) == 0 +} + +func (qs StrokeQuads) dash(sty DashOp) StrokeQuads { + sty = dashCanonical(sty) + + switch { + case len(sty.Dashes) == 0: + return qs + case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0: + return StrokeQuads{} + } + + if len(sty.Dashes)%2 == 1 { + // If the dash pattern is of uneven length, dash and space lengths + // alternate. The following duplicates the pattern so that uneven + // indices are always spaces. + sty.Dashes = append(sty.Dashes, sty.Dashes...) + } + + var ( + i0, pos0 = dashStart(sty) + out StrokeQuads + + contour uint32 = 1 + ) + + for _, ps := range qs.split() { + var ( + i = i0 + pos = pos0 + t []float64 + length = ps.len() + ) + for pos+sty.Dashes[i] < length { + pos += sty.Dashes[i] + if 0.0 < pos { + t = append(t, float64(pos)) + } + i++ + if i == len(sty.Dashes) { + i = 0 + } + } + + j0 := 0 + endsInDash := i%2 == 0 + if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash { + j0 = 1 + } + + var ( + qd StrokeQuads + pd = ps.splitAt(&contour, t...) + ) + for j := j0; j < len(pd)-1; j += 2 { + qd = qd.append(pd[j]) + } + if endsInDash { + if ps.closed() { + qd = pd[len(pd)-1].append(qd) + } else { + qd = qd.append(pd[len(pd)-1]) + } + } + out = out.append(qd) + contour++ + } + return out +} + +func dashCanonical(sty DashOp) DashOp { + var ( + o = sty + ds = o.Dashes + ) + + if len(sty.Dashes) == 0 { + return sty + } + + // Remove zeros except first and last. + for i := 1; i < len(ds)-1; i++ { + if f32Eq(ds[i], 0.0) { + ds[i-1] += ds[i+1] + ds = append(ds[:i], ds[i+2:]...) + i-- + } + } + + // Remove first zero, collapse with second and last. + if f32Eq(ds[0], 0.0) { + if len(ds) < 3 { + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, + } + } + o.Phase -= ds[1] + ds[len(ds)-1] += ds[1] + ds = ds[2:] + } + + // Remove last zero, collapse with fist and second to last. + if f32Eq(ds[len(ds)-1], 0.0) { + if len(ds) < 3 { + return DashOp{} + } + o.Phase += ds[len(ds)-2] + ds[0] += ds[len(ds)-2] + ds = ds[:len(ds)-2] + } + + // If there are zeros or negatives, don't draw dashes. + for i := 0; i < len(ds); i++ { + if ds[i] < 0.0 || f32Eq(ds[i], 0.0) { + return DashOp{ + Phase: 0.0, + Dashes: []float32{0.0}, + } + } + } + + // Remove repeated patterns. +loop: + for len(ds)%2 == 0 { + mid := len(ds) / 2 + for i := 0; i < mid; i++ { + if !f32Eq(ds[i], ds[mid+i]) { + break loop + } + } + ds = ds[:mid] + } + return o +} + +func dashStart(sty DashOp) (int, float32) { + i0 := 0 // i0 is the index into dashes. + for sty.Dashes[i0] <= sty.Phase { + sty.Phase -= sty.Dashes[i0] + i0++ + if i0 == len(sty.Dashes) { + i0 = 0 + } + } + // pos0 may be negative if the offset lands halfway into dash. + pos0 := -sty.Phase + if sty.Phase < 0.0 { + var sum float32 + for _, d := range sty.Dashes { + sum += d + } + pos0 = -(sum + sty.Phase) // handle negative offsets + } + return i0, pos0 +} + +func (qs StrokeQuads) len() float32 { + var sum float32 + for i := range qs { + q := qs[i].Quad + sum += quadBezierLen(q.From, q.Ctrl, q.To) + } + return sum +} + +// splitAt splits the path into separate paths at the specified intervals +// along the path. +// splitAt updates the provided contour counter as it splits the segments. +func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads { + if len(ts) == 0 { + qs.setContour(*contour) + return []StrokeQuads{qs} + } + + sort.Float64s(ts) + if ts[0] == 0 { + ts = ts[1:] + } + + var ( + j int // index into ts + t float64 // current position along curve + ) + + var oo []StrokeQuads + var oi StrokeQuads + push := func() { + oo = append(oo, oi) + oi = nil + } + + for _, ps := range qs.split() { + for _, q := range ps { + if j == len(ts) { + oi = append(oi, q) + continue + } + speed := func(t float64) float64 { + return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, q.Quad.To, float32(t)))) + } + invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1) + + var ( + t0 float64 + r0 = q.Quad.From + r1 = q.Quad.Ctrl + r2 = q.Quad.To + + // from keeps track of the start of the 'running' segment. + from = r0 + ) + for j < len(ts) && t < ts[j] && ts[j] <= t+dt { + tj := invL(ts[j] - t) + tsub := (tj - t0) / (1.0 - t0) + t0 = tj + + var q1 f32.Point + _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub)) + + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ + From: from, + Ctrl: q1, + To: r0, + }, + }) + push() + (*contour)++ + + from = r0 + j++ + } + if !f64Eq(t0, 1) { + if len(oi) > 0 { + r0 = oi.pen() + } + oi = append(oi, StrokeQuad{ + Contour: *contour, + Quad: QuadSegment{ + From: r0, + Ctrl: r1, + To: r2, + }, + }) + } + t += dt + } + } + if len(oi) > 0 { + push() + (*contour)++ + } + + return oo +} + +func f32Eq(a, b float32) bool { + const epsilon = 1e-10 + return math.Abs(float64(a-b)) < epsilon +} + +func f64Eq(a, b float64) bool { + const epsilon = 1e-10 + return math.Abs(a-b) < epsilon +} + +func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, fp func(float64) float64, tmin, tmax float64) (func(float64) float64, float64) { + // The TODOs below are copied verbatim from tdewolff/canvas: + // + // TODO: find better way to determine N. For Arc 10 seems fine, for some + // Quads 10 is too low, for Cube depending on inflection points is + // maybe not the best indicator + // + // TODO: track efficiency, how many times is fp called? + // Does a look-up table make more sense? + fLength := func(t float64) float64 { + return math.Abs(gaussLegendre(fp, tmin, t)) + } + totalLength := fLength(tmax) + t := func(L float64) float64 { + return bisectionMethod(fLength, L, tmin, tmax) + } + return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, tmax), totalLength +} + +func polynomialChebyshevApprox(N int, f func(float64) float64, xmin, xmax, ymin, ymax float64) func(float64) float64 { + var ( + invN = 1.0 / float64(N) + fs = make([]float64, N) + ) + for k := 0; k < N; k++ { + u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN) + fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1)) + } + + c := make([]float64, N) + for j := 0; j < N; j++ { + var a float64 + for k := 0; k < N; k++ { + a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N)) + } + c[j] = 2 * invN * a + } + + if ymax < ymin { + ymin, ymax = ymax, ymin + } + return func(x float64) float64 { + x = math.Min(xmax, math.Max(xmin, x)) + u := (x-xmin)/(xmax-xmin)*2 - 1 + var a float64 + for j := 0; j < N; j++ { + a += c[j] * math.Cos(float64(j)*math.Acos(u)) + } + y := -0.5*c[0] + a + if !math.IsNaN(ymin) && !math.IsNaN(ymax) { + y = math.Min(ymax, math.Max(ymin, y)) + } + return y + } +} + +// bisectionMethod finds the value x for which f(x) = y in the interval x +// in [xmin, xmax] using the bisection method. +func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 { + const ( + maxIter = 100 + tolerance = 0.001 // 0.1% + ) + + var ( + n = 0 + x float64 + tolX = math.Abs(xmax-xmin) * tolerance + tolY = math.Abs(f(xmax)-f(xmin)) * tolerance + ) + for { + x = 0.5 * (xmin + xmax) + if n >= maxIter { + return x + } + + dy := f(x) - y + switch { + case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX: + return x + case dy > 0: + xmax = x + default: + xmin = x + } + n++ + } +} + +type gaussLegendreFunc func(func(float64) float64, float64, float64) float64 + +// Gauss-Legendre quadrature integration from a to b with n=7 +func gaussLegendre7(f func(float64) float64, a, b float64) float64 { + c := 0.5 * (b - a) + d := 0.5 * (a + b) + Qd1 := f(-0.949108*c + d) + Qd2 := f(-0.741531*c + d) + Qd3 := f(-0.405845*c + d) + Qd4 := f(d) + Qd5 := f(0.405845*c + d) + Qd6 := f(0.741531*c + d) + Qd7 := f(0.949108*c + d) + return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4) +} diff --git a/pkg/gel/gio/internal/stroke/stroke.go b/pkg/gel/gio/internal/stroke/stroke.go new file mode 100644 index 0000000..affe656 --- /dev/null +++ b/pkg/gel/gio/internal/stroke/stroke.go @@ -0,0 +1,890 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Most of the algorithms to compute strokes and their offsets have been +// extracted, adapted from (and used as a reference implementation): +// - github.com/tdewolff/canvas (Licensed under MIT) +// +// These algorithms have been implemented from: +// Fast, precise flattening of cubic Bézier path and offset curves +// Thomas F. Hain, et al. +// +// An electronic version is available at: +// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf +// +// Possible improvements (in term of speed and/or accuracy) on these +// algorithms are: +// +// - Polar Stroking: New Theory and Methods for Stroking Paths, +// M. Kilgard +// https://arxiv.org/pdf/2007.00308.pdf +// +// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html +// R. Levien + +// Package stroke implements conversion of strokes to filled outlines. It is used as a +// fallback for stroke configurations not natively supported by the renderer. +package stroke + +import ( + "encoding/binary" + "math" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/internal/scene" +) + +// The following are copies of types from op/clip to avoid a circular import of +// that package. +// TODO: when the old renderer is gone, this package can be merged with +// op/clip, eliminating the duplicate types. +type StrokeStyle struct { + Width float32 + Miter float32 + Cap StrokeCap + Join StrokeJoin +} + +type StrokeCap uint8 + +const ( + RoundCap StrokeCap = iota + FlatCap + SquareCap +) + +type StrokeJoin uint8 + +const ( + RoundJoin StrokeJoin = iota + BevelJoin +) + +// strokeTolerance is used to reconcile rounding errors arising +// when splitting quads into smaller and smaller segments to approximate +// them into straight lines, and when joining back segments. +// +// The magic value of 0.01 was found by striking a compromise between +// aesthetic looking (curves did look like curves, even after linearization) +// and speed. +const strokeTolerance = 0.01 + +type QuadSegment struct { + From, Ctrl, To f32.Point +} + +type StrokeQuad struct { + Contour uint32 + Quad QuadSegment +} + +type strokeState struct { + p0, p1 f32.Point // p0 is the start point, p1 the end point. + n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point. + r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point. + ctl f32.Point // ctl is the control point of the quadratic Bézier segment. +} + +type StrokeQuads []StrokeQuad + +func (qs *StrokeQuads) setContour(n uint32) { + for i := range *qs { + (*qs)[i].Contour = n + } +} + +func (qs *StrokeQuads) pen() f32.Point { + return (*qs)[len(*qs)-1].Quad.To +} + +func (qs *StrokeQuads) closed() bool { + beg := (*qs)[0].Quad.From + end := (*qs)[len(*qs)-1].Quad.To + return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y) +} + +func (qs *StrokeQuads) lineTo(pt f32.Point) { + end := qs.pen() + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: end, + Ctrl: end.Add(pt).Mul(0.5), + To: pt, + }, + }) +} + +func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) { + const segments = 16 + pen := qs.pen() + m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments) + for i := 0; i < segments; i++ { + p0 := qs.pen() + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, Ctrl: ctl, To: p2, + }, + }) + } +} + +// split splits a slice of quads into slices of quads grouped +// by contours (ie: splitted at move-to boundaries). +func (qs StrokeQuads) split() []StrokeQuads { + if len(qs) == 0 { + return nil + } + + var ( + c uint32 + o []StrokeQuads + i = len(o) + ) + for _, q := range qs { + if q.Contour != c { + c = q.Contour + i = len(o) + o = append(o, StrokeQuads{}) + } + o[i] = append(o[i], q) + } + + return o +} + +func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads { + if !IsSolidLine(dashes) { + qs = qs.dash(dashes) + } + + var ( + o StrokeQuads + hw = 0.5 * stroke.Width + ) + + for _, ps := range qs.split() { + rhs, lhs := ps.offset(hw, stroke) + switch lhs { + case nil: + o = o.append(rhs) + default: + // Closed path. + // Inner path should go opposite direction to cancel outer path. + switch { + case ps.ccw(): + lhs = lhs.reverse() + o = o.append(rhs) + o = o.append(lhs) + default: + rhs = rhs.reverse() + o = o.append(lhs) + o = o.append(rhs) + } + } + } + + return o +} + +// offset returns the right-hand and left-hand sides of the path, offset by +// the half-width hw. +// The stroke handles how segments are joined and ends are capped. +func (qs StrokeQuads) offset(hw float32, stroke StrokeStyle) (rhs, lhs StrokeQuads) { + var ( + states []strokeState + beg = qs[0].Quad.From + end = qs[len(qs)-1].Quad.To + closed = beg == end + ) + for i := range qs { + q := qs[i].Quad + + var ( + n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw) + n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw) + r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0) + r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1) + ) + states = append(states, strokeState{ + p0: q.From, + p1: q.To, + n0: n0, + n1: n1, + r0: r0, + r1: r1, + ctl: q.Ctrl, + }) + } + + for i, state := range states { + rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance)) + lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance)) + + // join the current and next segments + if hasNext := i+1 < len(states); hasNext || closed { + var next strokeState + switch { + case hasNext: + next = states[i+1] + case closed: + next = states[0] + } + if state.n1 != next.n0 { + strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0) + } + } + } + + if closed { + rhs.close() + lhs.close() + return rhs, lhs + } + + qbeg := &states[0] + qend := &states[len(states)-1] + + // Default to counter-clockwise direction. + lhs = lhs.reverse() + strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1) + + rhs = rhs.append(lhs) + strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1)) + + rhs.close() + + return rhs, nil +} + +func (qs *StrokeQuads) close() { + p0 := (*qs)[len(*qs)-1].Quad.To + p1 := (*qs)[0].Quad.From + + if p1 == p0 { + return + } + + *qs = append(*qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }) +} + +// ccw returns whether the path is counter-clockwise. +func (qs StrokeQuads) ccw() bool { + // Use the Shoelace formula: + // https://en.wikipedia.org/wiki/Shoelace_formula + var area float32 + for _, ps := range qs.split() { + for i := 1; i < len(ps); i++ { + pi := ps[i].Quad.To + pj := ps[i-1].Quad.To + area += (pi.X - pj.X) * (pi.Y + pj.Y) + } + } + return area <= 0.0 +} + +func (qs StrokeQuads) reverse() StrokeQuads { + if len(qs) == 0 { + return nil + } + + ps := make(StrokeQuads, 0, len(qs)) + for i := range qs { + q := qs[len(qs)-1-i] + q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To + ps = append(ps, q) + } + + return ps +} + +func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads { + switch { + case len(ps) == 0: + return qs + case len(qs) == 0: + return ps + } + + // Consolidate quads and smooth out rounding errors. + // We need to also check for the strokeTolerance to correctly handle + // join/cap points or on-purpose disjoint quads. + p0 := qs[len(qs)-1].Quad.To + p1 := ps[0].Quad.From + if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance { + qs = append(qs, StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }) + } + return append(qs, ps...) +} + +func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment { + q.From = t.Transform(q.From) + q.Ctrl = t.Transform(q.Ctrl) + q.To = t.Transform(q.To) + return q +} + +// strokePathNorm returns the normal vector at t. +func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { + switch t { + case 0: + n := p1.Sub(p0) + if n.X == 0 && n.Y == 0 { + return f32.Point{} + } + n = rot90CW(n) + return normPt(n, d) + case 1: + n := p2.Sub(p1) + if n.X == 0 && n.Y == 0 { + return f32.Point{} + } + n = rot90CW(n) + return normPt(n, d) + } + panic("impossible") +} + +func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } +func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) } + +// cosPt returns the cosine of the opening angle between p and q. +func cosPt(p, q f32.Point) float32 { + np := math.Hypot(float64(p.X), float64(p.Y)) + nq := math.Hypot(float64(q.X), float64(q.Y)) + return dotPt(p, q) / float32(np*nq) +} + +func normPt(p f32.Point, l float32) f32.Point { + d := math.Hypot(float64(p.X), float64(p.Y)) + l64 := float64(l) + if math.Abs(d-l64) < 1e-10 { + return f32.Point{} + } + n := float32(l64 / d) + return f32.Point{X: p.X * n, Y: p.Y * n} +} + +func lenPt(p f32.Point) float32 { + return float32(math.Hypot(float64(p.X), float64(p.Y))) +} + +func dotPt(p, q f32.Point) float32 { + return p.X*q.X + p.Y*q.Y +} + +func perpDot(p, q f32.Point) float32 { + return p.X*q.Y - p.Y*q.X +} + +// strokePathCurv returns the curvature at t, along the quadratic Bézier +// curve defined by the triplet (beg, ctl, end). +func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 { + var ( + d1p = quadBezierD1(beg, ctl, end, t) + d2p = quadBezierD2(beg, ctl, end, t) + + // Negative when bending right, ie: the curve is CW at this point. + a = float64(perpDot(d1p, d2p)) + ) + + // We check early that the segment isn't too line-like and + // save a costly call to math.Pow that will be discarded by dividing + // with a too small 'a'. + if math.Abs(a) < 1e-10 { + return float32(math.NaN()) + } + return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a) +} + +// quadBezierSample returns the point on the Bézier curve at t. +// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2 +func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point { + t1 := 1 - t + c0 := t1 * t1 + c1 := 2 * t1 * t + c2 := t * t + + o := p0.Mul(c0) + o = o.Add(p1.Mul(c1)) + o = o.Add(p2.Mul(c2)) + return o +} + +// quadBezierD1 returns the first derivative of the Bézier curve with respect to t. +// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1) +func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point { + p10 := p1.Sub(p0).Mul(2 * (1 - t)) + p21 := p2.Sub(p1).Mul(2 * t) + + return p10.Add(p21) +} + +// quadBezierD2 returns the second derivative of the Bézier curve with respect to t: +// B''(t) = 2(P2 - 2P1 + P0) +func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { + p := p2.Sub(p1.Mul(2)).Add(p0) + return p.Mul(2) +} + +// quadBezierLen returns the length of the Bézier curve. +// See: +// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/ +func quadBezierLen(p0, p1, p2 f32.Point) float32 { + a := p0.Sub(p1.Mul(2)).Add(p2) + b := p1.Mul(2).Sub(p0.Mul(2)) + A := float64(4 * dotPt(a, a)) + B := float64(4 * dotPt(a, b)) + C := float64(dotPt(b, b)) + if f64Eq(A, 0.0) { + // p1 is in the middle between p0 and p2, + // so it is a straight line from p0 to p2. + return lenPt(p2.Sub(p0)) + } + + Sabc := 2 * math.Sqrt(A+B+C) + A2 := math.Sqrt(A) + A32 := 2 * A * A2 + C2 := 2 * math.Sqrt(C) + BA := B / A2 + return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32)) +} + +func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { + // Gio strokes are only quadratic Bézier curves, w/o any inflection point. + // So we just have to flatten them. + var qs StrokeQuads + return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) +} + +// flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments, +// themselves also encoded as Bézier (degenerate, flat) quadratic curves. +func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads { + var ( + t float32 + flat64 = float64(flatness) + ) + for t < 1 { + s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X)) + den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y)) + if s2*den == 0.0 { + break + } + + s2 /= den + t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2))) + if t >= 1.0 { + break + } + var q0, q1, q2 f32.Point + q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t) + qs.addLine(q0, q1, q2, 0, d) + } + qs.addLine(p0, p1, p2, 1, d) + return qs +} + +func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) { + + switch i := len(*qs); i { + case 0: + p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) + default: + // Address possible rounding errors and use previous point. + p0 = (*qs)[i-1].Quad.To + } + + p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d)) + + *qs = append(*qs, + StrokeQuad{ + Quad: QuadSegment{ + From: p0, + Ctrl: p0.Add(p1).Mul(0.5), + To: p1, + }, + }, + ) +} + +// quadInterp returns the interpolated point at t. +func quadInterp(p, q f32.Point, t float32) f32.Point { + return f32.Pt( + (1-t)*p.X+t*q.X, + (1-t)*p.Y+t*q.Y, + ) +} + +// quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve, +// split before (resp. after) the provided parametric t value. +func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) { + + var ( + b0 = p0 + b1 = quadInterp(p0, p1, t) + b2 = quadBezierSample(p0, p1, p2, t) + + a0 = b2 + a1 = quadInterp(p1, p2, t) + a2 = p2 + ) + + return b0, b1, b2, a0, a1, a2 +} + +// strokePathJoin joins the two paths rhs and lhs, according to the provided +// stroke operation. +func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + if stroke.Miter > 0 { + strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + switch stroke.Join { + case BevelJoin: + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + case RoundJoin: + strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + default: + panic("impossible") + } +} + +func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + + rhs.lineTo(rp) + lhs.lineTo(lp) +} + +func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + cw := dotPt(rot90CW(n0), n1) >= 0.0 + switch { + case cw: + // Path bends to the right, ie. CW (or 180 degree turn). + c := pivot.Sub(lhs.pen()) + angle := -math.Acos(float64(cosPt(n0, n1))) + lhs.arc(c, c, float32(angle)) + lhs.lineTo(lp) // Add a line to accommodate for rounding errors. + rhs.lineTo(rp) + default: + // Path bends to the left, ie. CCW. + angle := math.Acos(float64(cosPt(n0, n1))) + c := pivot.Sub(rhs.pen()) + rhs.arc(c, c, float32(angle)) + rhs.lineTo(rp) // Add a line to accommodate for rounding errors. + lhs.lineTo(lp) + } +} + +func strokePathMiterJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { + if n0 == n1.Mul(-1) { + strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + + // This is to handle nearly linear joints that would be clipped otherwise. + limit := math.Max(float64(stroke.Miter), 1.001) + + cw := dotPt(rot90CW(n0), n1) >= 0.0 + if cw { + // hw is used to calculate |R|. + // When running CW, n0 and n1 point the other way, + // so the sign of r0 and r1 is negated. + hw = -hw + } + hw64 := float64(hw) + + cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1)))) + d := hw64 / cos + if math.Abs(limit*hw64) < math.Abs(d) { + stroke.Miter = 0 // Set miter to zero to disable the miter joint. + strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1) + return + } + mid := pivot.Add(normPt(n0.Add(n1), float32(d))) + + rp := pivot.Add(n1) + lp := pivot.Sub(n1) + switch { + case cw: + // Path bends to the right, ie. CW. + lhs.lineTo(mid) + default: + // Path bends to the left, ie. CCW. + rhs.lineTo(mid) + } + rhs.lineTo(rp) + lhs.lineTo(lp) +} + +// strokePathCap caps the provided path qs, according to the provided stroke operation. +func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + switch stroke.Cap { + case FlatCap: + strokePathFlatCap(qs, hw, pivot, n0) + case SquareCap: + strokePathSquareCap(qs, hw, pivot, n0) + case RoundCap: + strokePathRoundCap(qs, hw, pivot, n0) + default: + panic("impossible") + } +} + +// strokePathFlatCap caps the start or end of a path with a flat cap. +func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + end := pivot.Sub(n0) + qs.lineTo(end) +} + +// strokePathSquareCap caps the start or end of a path with a square cap. +func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + var ( + e = pivot.Add(rot90CCW(n0)) + corner1 = e.Add(n0) + corner2 = e.Sub(n0) + end = pivot.Sub(n0) + ) + + qs.lineTo(corner1) + qs.lineTo(corner2) + qs.lineTo(end) +} + +// strokePathRoundCap caps the start or end of a path with a round cap. +func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { + c := pivot.Sub(qs.pen()) + qs.arc(c, c, math.Pi) +} + +// ArcTransform computes a transformation that can be used for generating quadratic bézier +// curve approximations for an arc. +// +// The math is extracted from the following paper: +// "Drawing an elliptical arc using polylines, quadratic or +// cubic Bezier curves", L. Maisonobe +// An electronic version may be found at: +// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf +func ArcTransform(p, f1, f2 f32.Point, angle float32, segments int) f32.Affine2D { + c := f32.Point{ + X: 0.5 * (f1.X + f2.X), + Y: 0.5 * (f1.Y + f2.Y), + } + + // semi-major axis: 2a = |PF1| + |PF2| + a := 0.5 * (dist(f1, p) + dist(f2, p)) + + // semi-minor axis: c^2 = a^2+b^2 (c: focal distance) + f := dist(f1, c) + b := math.Sqrt(a*a - f*f) + + var rx, ry, alpha, start float64 + switch { + case a > b: + rx = a + ry = b + default: + rx = b + ry = a + } + + var x float64 + switch { + case f1 == c || f2 == c: + // degenerate case of a circle. + alpha = 0 + default: + switch { + case f1.X > c.X: + x = float64(f1.X - c.X) + alpha = math.Acos(x / f) + case f1.X < c.X: + x = float64(f2.X - c.X) + alpha = math.Acos(x / f) + case f1.X == c.X: + // special case of a "vertical" ellipse. + alpha = math.Pi / 2 + if f1.Y < c.Y { + alpha = -alpha + } + } + } + + start = math.Acos(float64(p.X-c.X) / dist(c, p)) + if c.Y > p.Y { + start = -start + } + start -= alpha + + var ( + θ = angle / float32(segments) + ref f32.Affine2D // transform from absolute frame to ellipse-based one + rot f32.Affine2D // rotation matrix for each segment + inv f32.Affine2D // transform from ellipse-based frame to absolute one + ) + ref = ref.Offset(f32.Point{}.Sub(c)) + ref = ref.Rotate(f32.Point{}, float32(-alpha)) + ref = ref.Scale(f32.Point{}, f32.Point{ + X: float32(1 / rx), + Y: float32(1 / ry), + }) + inv = ref.Invert() + rot = rot.Rotate(f32.Point{}, float32(0.5*θ)) + + // Instead of invoking math.Sincos for every segment, compute a rotation + // matrix once and apply for each segment. + // Before applying the rotation matrix rot, transform the coordinates + // to a frame centered to the ellipse (and warped into a unit circle), then rotate. + // Finally, transform back into the original frame. + return inv.Mul(rot).Mul(ref) +} + +func dist(p1, p2 f32.Point) float64 { + var ( + x1 = float64(p1.X) + y1 = float64(p1.Y) + x2 = float64(p2.X) + y2 = float64(p2.Y) + dx = x2 - x1 + dy = y2 - y1 + ) + return math.Hypot(dx, dy) +} + +func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads { + quads := decodeToStrokeQuads(scene) + return quads.stroke(style, dashes) +} + +// decodeToStrokeQuads decodes scene commands to quads ready to stroke. +func decodeToStrokeQuads(pathData []byte) StrokeQuads { + quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) + for len(pathData) >= scene.CommandSize+4 { + contour := binary.LittleEndian.Uint32(pathData) + cmd := ops.DecodeCommand(pathData[4:]) + switch cmd.Op() { + case scene.OpLine: + var q QuadSegment + q.From, q.To = scene.DecodeLine(cmd) + q.Ctrl = q.From.Add(q.To).Mul(.5) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpQuad: + var q QuadSegment + q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + case scene.OpCubic: + for _, q := range SplitCubic(scene.DecodeCubic(cmd)) { + quad := StrokeQuad{ + Contour: contour, + Quad: q, + } + quads = append(quads, quad) + } + default: + panic("unsupported scene command") + } + pathData = pathData[scene.CommandSize+4:] + } + return quads +} + +func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment { + quads := make([]QuadSegment, 0, 10) + // Set the maximum distance proportionally to the longest side + // of the bounding rectangle. + hull := f32.Rectangle{ + Min: from, + Max: ctrl0, + }.Canon().Add(ctrl1).Add(to) + l := hull.Dx() + if h := hull.Dy(); h > l { + l = h + } + approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to) + return quads +} + +// approxCubeTo approximates a cubic Bézier by a series of quadratic +// curves. +func approxCubeTo(quads *[]QuadSegment, splits int, maxDist float32, from, ctrl0, ctrl1, to f32.Point) int { + // The idea is from + // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + // where a quadratic approximates a cubic by eliminating its t³ term + // from its polynomial expression anchored at the starting point: + // + // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) + // + // The control point for the new quadratic Q1 that shares starting point, pen, with P is + // + // C1 = (3ctrl0 - pen)/2 + // + // The reverse cubic anchored at the end point has the polynomial + // + // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) + // + // The corresponding quadratic Q2 that shares the end point, to, with P has control + // point + // + // C2 = (3ctrl1 - to)/2 + // + // The combined quadratic Bézier, Q, shares both start and end points with its cubic + // and use the midpoint between the two curves Q1 and Q2 as control point: + // + // C = (3ctrl0 - pen + 3ctrl1 - to)/4 + c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) + const maxSplits = 32 + if splits >= maxSplits { + *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) + return splits + } + // The maximum distance between the cubic P and its approximation Q given t + // can be shown to be + // + // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| + // + // To save a square root, compare d² with the squared tolerance. + v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from) + d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) + if d2 <= maxDist*maxDist { + *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) + return splits + } + // De Casteljau split the curve and approximate the halves. + t := float32(0.5) + c0 := from.Add(ctrl0.Sub(from).Mul(t)) + c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) + c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) + c01 := c0.Add(c1.Sub(c0).Mul(t)) + c12 := c1.Add(c2.Sub(c1).Mul(t)) + c0112 := c01.Add(c12.Sub(c01).Mul(t)) + splits++ + splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112) + splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to) + return splits +} diff --git a/pkg/gel/gio/io/clipboard/clipboard.go b/pkg/gel/gio/io/clipboard/clipboard.go new file mode 100644 index 0000000..af6c9ee --- /dev/null +++ b/pkg/gel/gio/io/clipboard/clipboard.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clipboard + +import ( + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Event is generated when the clipboard content is requested. +type Event struct { + Text string +} + +// ReadOp requests the text of the clipboard, delivered to +// the current handler through an Event. +type ReadOp struct { + Tag event.Tag +} + +// WriteOp copies Text to the clipboard. +type WriteOp struct { + Text string +} + +func (h ReadOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardReadLen, h.Tag) + data[0] = byte(opconst.TypeClipboardRead) +} + +func (h WriteOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeClipboardWriteLen, &h.Text) + data[0] = byte(opconst.TypeClipboardWrite) +} + +func (Event) ImplementsEvent() {} diff --git a/pkg/gel/gio/io/event/event.go b/pkg/gel/gio/io/event/event.go new file mode 100644 index 0000000..b40a1e1 --- /dev/null +++ b/pkg/gel/gio/io/event/event.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package event contains the types for event handling. + +The Queue interface is the protocol for receiving external events. + +For example: + + var queue event.Queue = ... + + for _, e := range queue.Events(h) { + switch e.(type) { + ... + } + } + +In general, handlers must be declared before events become +available. Other packages such as pointer and key provide +the means for declaring handlers for specific event types. + +The following example declares a handler ready for key input: + + import github.com/p9c/p9/pkg/gel/gio/io/key + + ops := new(op.Ops) + var h *Handler = ... + key.InputOp{Tag: h}.Add(ops) + +*/ +package event + +// Queue maps an event handler key to the events +// available to the handler. +type Queue interface { + // Events returns the available events for an + // event handler tag. + Events(t Tag) []Event +} + +// Tag is the stable identifier for an event handler. +// For a handler h, the tag is typically &h. +type Tag interface{} + +// Event is the marker interface for events. +type Event interface { + ImplementsEvent() +} diff --git a/pkg/gel/gio/io/key/key.go b/pkg/gel/gio/io/key/key.go new file mode 100644 index 0000000..0486656 --- /dev/null +++ b/pkg/gel/gio/io/key/key.go @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package key implements key and text events and operations. + +The InputOp operations is used for declaring key input handlers. Use +an implementation of the Queue interface from package ui to receive +events. +*/ +package key + +import ( + "fmt" + "strings" + + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// InputOp declares a handler ready for key events. +// Key events are in general only delivered to the +// focused key handler. +type InputOp struct { + Tag event.Tag +} + +// SoftKeyboardOp shows or hide the on-screen keyboard, if available. +// It replaces any previous SoftKeyboardOp. +type SoftKeyboardOp struct { + Show bool +} + +// FocusOp sets or clears the keyboard focus. It replaces any previous +// FocusOp in the same frame. +type FocusOp struct { + // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag + // has no InputOp in the same frame. + Tag event.Tag +} + +// A FocusEvent is generated when a handler gains or loses +// focus. +type FocusEvent struct { + Focus bool +} + +// An Event is generated when a key is pressed. For text input +// use EditEvent. +type Event struct { + // Name of the key. For letters, the upper case form is used, via + // unicode.ToUpper. The shift modifier is taken into account, all other + // modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1" + // combinations both give the Name "!" with the US keyboard layout. + Name string + // Modifiers is the set of active modifiers when the key was pressed. + Modifiers Modifiers + // State is the state of the key when the event was fired. + State State +} + +// An EditEvent is generated when text is input. +type EditEvent struct { + Text string +} + +// State is the state of a key during an event. +type State uint8 + +const ( + // Press is the state of a pressed key. + Press State = iota + // Release is the state of a key that has been released. + // + // Note: release events are only implemented on the following platforms: + // macOS, Linux, Windows, WebAssembly. + Release +) + +// Modifiers +type Modifiers uint32 + +const ( + // ModCtrl is the ctrl modifier key. + ModCtrl Modifiers = 1 << iota + // ModCommand is the command modifier key + // found on Apple keyboards. + ModCommand + // ModShift is the shift modifier key. + ModShift + // ModAlt is the alt modifier key, or the option + // key on Apple keyboards. + ModAlt + // ModSuper is the "logo" modifier key, often + // represented by a Windows logo. + ModSuper +) + +const ( + // Names for special keys. + NameLeftArrow = "←" + NameRightArrow = "→" + NameUpArrow = "↑" + NameDownArrow = "↓" + NameReturn = "⏎" + NameEnter = "⌤" + NameEscape = "⎋" + NameHome = "⇱" + NameEnd = "⇲" + NameDeleteBackward = "⌫" + NameDeleteForward = "⌦" + NamePageUp = "⇞" + NamePageDown = "⇟" + NameTab = "⇥" + NameSpace = "Space" +) + +// Contain reports whether m contains all modifiers +// in m2. +func (m Modifiers) Contain(m2 Modifiers) bool { + return m&m2 == m2 +} + +func (h InputOp) Add(o *op.Ops) { + if h.Tag == nil { + panic("Tag must be non-nil") + } + data := o.Write1(opconst.TypeKeyInputLen, h.Tag) + data[0] = byte(opconst.TypeKeyInput) +} + +func (h SoftKeyboardOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeKeySoftKeyboardLen) + data[0] = byte(opconst.TypeKeySoftKeyboard) + if h.Show { + data[1] = 1 + } +} + +func (h FocusOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeKeyFocusLen, h.Tag) + data[0] = byte(opconst.TypeKeyFocus) +} + +func (EditEvent) ImplementsEvent() {} +func (Event) ImplementsEvent() {} +func (FocusEvent) ImplementsEvent() {} + +func (e Event) String() string { + return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State) +} + +func (m Modifiers) String() string { + var strs []string + if m.Contain(ModCtrl) { + strs = append(strs, "ModCtrl") + } + if m.Contain(ModCommand) { + strs = append(strs, "ModCommand") + } + if m.Contain(ModShift) { + strs = append(strs, "ModShift") + } + if m.Contain(ModAlt) { + strs = append(strs, "ModAlt") + } + if m.Contain(ModSuper) { + strs = append(strs, "ModSuper") + } + return strings.Join(strs, "|") +} + +func (s State) String() string { + switch s { + case Press: + return "Press" + case Release: + return "Release" + default: + panic("invalid State") + } +} diff --git a/pkg/gel/gio/io/key/mod.go b/pkg/gel/gio/io/key/mod.go new file mode 100644 index 0000000..c5db56c --- /dev/null +++ b/pkg/gel/gio/io/key/mod.go @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !darwin + +package key + +// ModShortcut is the platform's shortcut modifier, usually the Ctrl +// key. On Apple platforms it is the Cmd key. +const ModShortcut = ModCtrl diff --git a/pkg/gel/gio/io/key/mod_darwin.go b/pkg/gel/gio/io/key/mod_darwin.go new file mode 100644 index 0000000..c0f1437 --- /dev/null +++ b/pkg/gel/gio/io/key/mod_darwin.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package key + +// ModShortcut is the platform's shortcut modifier, usually the Ctrl +// key. On Apple platforms it is the Cmd key. +const ModShortcut = ModCommand diff --git a/pkg/gel/gio/io/pointer/doc.go b/pkg/gel/gio/io/pointer/doc.go new file mode 100644 index 0000000..7243b94 --- /dev/null +++ b/pkg/gel/gio/io/pointer/doc.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package pointer implements pointer events and operations. +A pointer is either a mouse controlled cursor or a touch +object such as a finger. + +The InputOp operation is used to declare a handler ready for pointer +events. Use an event.Queue to receive events. + +Types + +Only events that match a specified list of types are delivered to a handler. + +For example, to receive Press, Drag, and Release events (but not Move, Enter, +Leave, or Scroll): + + var ops op.Ops + var h *Handler = ... + + pointer.InputOp{ + Tag: h, + Types: pointer.Press | pointer.Drag | pointer.Release, + }.Add(ops) + +Cancel events are always delivered. + +Areas + +The area operations are used for specifying the area where +subsequent InputOp are active. + +For example, to set up a rectangular hit area: + + r := image.Rectangle{...} + pointer.Rect(r).Add(ops) + pointer.InputOp{Tag: h}.Add(ops) + +Note that areas compound: the effective area of multiple area +operations is the intersection of the areas. + +Matching events + +StackOp operations and input handlers form an implicit tree. +Each stack operation is a node, and each input handler is associated +with the most recent node. + +For example: + + ops := new(op.Ops) + var stack op.StackOp + var h1, h2 *Handler + + state := op.Save(ops) + pointer.InputOp{Tag: h1}.Add(Ops) + state.Load() + + state = op.Save(ops) + pointer.InputOp{Tag: h2}.Add(ops) + state.Load() + +implies a tree of two inner nodes, each with one pointer handler. + +When determining which handlers match an Event, only handlers whose +areas contain the event position are considered. The matching +proceeds as follows. + +First, the foremost matching handler is included. If the handler +has pass-through enabled, this step is repeated. + +Then, all matching handlers from the current node and all parent +nodes are included. + +In the example above, all events will go to h2 only even though both +handlers have the same area (the entire screen). + +Pass-through + +The PassOp operations controls the pass-through setting. A handler's +pass-through setting is recorded along with the InputOp. + +Pass-through handlers are useful for overlay widgets such as a hidden +side drawer. When the user touches the side, both the (transparent) +drawer handle and the interface below should receive pointer events. + +Disambiguation + +When more than one handler matches a pointer event, the event queue +follows a set of rules for distributing the event. + +As long as the pointer has not received a Press event, all +matching handlers receive all events. + +When a pointer is pressed, the set of matching handlers is +recorded. The set is not updated according to the pointer position +and hit areas. Rather, handlers stay in the matching set until they +no longer appear in a InputOp or when another handler in the set +grabs the pointer. + +A handler can exclude all other handler from its matching sets +by setting the Grab flag in its InputOp. The Grab flag is sticky +and stays in effect until the handler no longer appears in any +matching sets. + +The losing handlers are notified by a Cancel event. + +For multiple grabbing handlers, the foremost handler wins. + +Priorities + +Handlers know their position in a matching set of a pointer through +event priorities. The Shared priority is for matching sets with +multiple handlers; the Grabbed priority indicate exclusive access. + +Priorities are useful for deferred gesture matching. + +Consider a scrollable list of clickable elements. When the user touches an +element, it is unknown whether the gesture is a click on the element +or a drag (scroll) of the list. While the click handler might light up +the element in anticipation of a click, the scrolling handler does not +scroll on finger movements with lower than Grabbed priority. + +Should the user release the finger, the click handler registers a click. + +However, if the finger moves beyond a threshold, the scrolling handler +determines that the gesture is a drag and sets its Grab flag. The +click handler receives a Cancel (removing the highlight) and further +movements for the scroll handler has priority Grabbed, scrolling the +list. +*/ +package pointer diff --git a/pkg/gel/gio/io/pointer/pointer.go b/pkg/gel/gio/io/pointer/pointer.go new file mode 100644 index 0000000..91c6c58 --- /dev/null +++ b/pkg/gel/gio/io/pointer/pointer.go @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package pointer + +import ( + "encoding/binary" + "fmt" + "image" + "strings" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Event is a pointer event. +type Event struct { + Type Type + Source Source + // PointerID is the id for the pointer and can be used + // to track a particular pointer from Press to + // Release or Cancel. + PointerID ID + // Priority is the priority of the receiving handler + // for this event. + Priority Priority + // Time is when the event was received. The + // timestamp is relative to an undefined base. + Time time.Duration + // Buttons are the set of pressed mouse buttons for this event. + Buttons Buttons + // Position is the position of the event, relative to + // the current transformation, as set by op.TransformOp. + Position f32.Point + // Scroll is the scroll amount, if any. + Scroll f32.Point + // Modifiers is the set of active modifiers when + // the mouse button was pressed. + Modifiers key.Modifiers +} + +// AreaOp updates the hit area to the intersection of the current +// hit area and the area. The area is transformed before applying +// it. +type AreaOp struct { + kind areaKind + rect image.Rectangle +} + +// CursorNameOp sets the cursor for the current area. +type CursorNameOp struct { + Name CursorName +} + +// InputOp declares an input handler ready for pointer +// events. +type InputOp struct { + Tag event.Tag + // Grab, if set, request that the handler get + // Grabbed priority. + Grab bool + // Types is a bitwise-or of event types to receive. + Types Type + // ScrollBounds describe the maximum scrollable distances in both + // axes. Specifically, any Event e delivered to Tag will satisfy + // + // ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis) + // ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis) + ScrollBounds image.Rectangle +} + +// PassOp sets the pass-through mode. +type PassOp struct { + Pass bool +} + +type ID uint16 + +// Type of an Event. +type Type uint8 + +// Priority of an Event. +type Priority uint8 + +// Source of an Event. +type Source uint8 + +// Buttons is a set of mouse buttons +type Buttons uint8 + +// CursorName is the name of a cursor. +type CursorName string + +// Must match app/internal/input.areaKind +type areaKind uint8 + +const ( + // CursorDefault is the default cursor. + CursorDefault CursorName = "" + // CursorText is the cursor for text. + CursorText CursorName = "text" + // CursorPointer is the cursor for a link. + CursorPointer CursorName = "pointer" + // CursorCrossHair is the cursor for precise location. + CursorCrossHair CursorName = "crosshair" + // CursorColResize is the cursor for vertical resize. + CursorColResize CursorName = "col-resize" + // CursorRowResize is the cursor for horizontal resize. + CursorRowResize CursorName = "row-resize" + // CursorGrab is the cursor for moving object in any direction. + CursorGrab CursorName = "grab" + // CursorNone hides the cursor. To show it again, use any other cursor. + CursorNone CursorName = "none" +) + +const ( + // A Cancel event is generated when the current gesture is + // interrupted by other handlers or the system. + Cancel Type = (1 << iota) >> 1 + // Press of a pointer. + Press + // Release of a pointer. + Release + // Move of a pointer. + Move + // Drag of a pointer. + Drag + // Pointer enters an area watching for pointer input + Enter + // Pointer leaves an area watching for pointer input + Leave + // Scroll of a pointer. + Scroll +) + +const ( + // Mouse generated event. + Mouse Source = iota + // Touch generated event. + Touch +) + +const ( + // Shared priority is for handlers that + // are part of a matching set larger than 1. + Shared Priority = iota + // Foremost priority is like Shared, but the + // handler is the foremost of the matching set. + Foremost + // Grabbed is used for matching sets of size 1. + Grabbed +) + +const ( + // ButtonPrimary is the primary button, usually the left button for a + // right-handed user. + ButtonPrimary Buttons = 1 << iota + // ButtonSecondary is the secondary button, usually the right button for a + // right-handed user. + ButtonSecondary + // ButtonTertiary is the tertiary button, usually the middle button. + ButtonTertiary +) + +const ( + areaRect areaKind = iota + areaEllipse +) + +// Rect constructs a rectangular hit area. +func Rect(size image.Rectangle) AreaOp { + return AreaOp{ + kind: areaRect, + rect: size, + } +} + +// Ellipse constructs an ellipsoid hit area. +func Ellipse(size image.Rectangle) AreaOp { + return AreaOp{ + kind: areaEllipse, + rect: size, + } +} + +func (op AreaOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeAreaLen) + data[0] = byte(opconst.TypeArea) + data[1] = byte(op.kind) + bo := binary.LittleEndian + bo.PutUint32(data[2:], uint32(op.rect.Min.X)) + bo.PutUint32(data[6:], uint32(op.rect.Min.Y)) + bo.PutUint32(data[10:], uint32(op.rect.Max.X)) + bo.PutUint32(data[14:], uint32(op.rect.Max.Y)) +} + +func (op CursorNameOp) Add(o *op.Ops) { + data := o.Write1(opconst.TypeCursorLen, op.Name) + data[0] = byte(opconst.TypeCursor) +} + +// Add panics if the scroll range does not contain zero. +func (op InputOp) Add(o *op.Ops) { + if op.Tag == nil { + panic("Tag must be non-nil") + } + if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 { + panic(fmt.Errorf("invalid scroll range value %v", b)) + } + data := o.Write1(opconst.TypePointerInputLen, op.Tag) + data[0] = byte(opconst.TypePointerInput) + if op.Grab { + data[1] = 1 + } + data[2] = byte(op.Types) + bo := binary.LittleEndian + bo.PutUint32(data[3:], uint32(op.ScrollBounds.Min.X)) + bo.PutUint32(data[7:], uint32(op.ScrollBounds.Min.Y)) + bo.PutUint32(data[11:], uint32(op.ScrollBounds.Max.X)) + bo.PutUint32(data[15:], uint32(op.ScrollBounds.Max.Y)) +} + +func (op PassOp) Add(o *op.Ops) { + data := o.Write(opconst.TypePassLen) + data[0] = byte(opconst.TypePass) + if op.Pass { + data[1] = 1 + } +} + +func (t Type) String() string { + switch t { + case Press: + return "Press" + case Release: + return "Release" + case Cancel: + return "Cancel" + case Move: + return "Move" + case Drag: + return "Drag" + case Enter: + return "Enter" + case Leave: + return "Leave" + case Scroll: + return "Scroll" + default: + panic("unknown Type") + } +} + +func (p Priority) String() string { + switch p { + case Shared: + return "Shared" + case Foremost: + return "Foremost" + case Grabbed: + return "Grabbed" + default: + panic("unknown priority") + } +} + +func (s Source) String() string { + switch s { + case Mouse: + return "Mouse" + case Touch: + return "Touch" + default: + panic("unknown source") + } +} + +// Contain reports whether the set b contains +// all of the buttons. +func (b Buttons) Contain(buttons Buttons) bool { + return b&buttons == buttons +} + +func (b Buttons) String() string { + var strs []string + if b.Contain(ButtonPrimary) { + strs = append(strs, "ButtonPrimary") + } + if b.Contain(ButtonSecondary) { + strs = append(strs, "ButtonSecondary") + } + if b.Contain(ButtonTertiary) { + strs = append(strs, "ButtonTertiary") + } + return strings.Join(strs, "|") +} + +func (c CursorName) String() string { + if c == CursorDefault { + return "default" + } + return string(c) +} + +func (Event) ImplementsEvent() {} diff --git a/pkg/gel/gio/io/profile/profile.go b/pkg/gel/gio/io/profile/profile.go new file mode 100644 index 0000000..acaca85 --- /dev/null +++ b/pkg/gel/gio/io/profile/profile.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package profiles provides access to rendering +// profiles. +package profile + +import ( + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Op registers a handler for receiving +// Events. +type Op struct { + Tag event.Tag +} + +// Event contains profile data from a single +// rendered frame. +type Event struct { + // Timings. Very likely to change. + Timings string +} + +func (p Op) Add(o *op.Ops) { + data := o.Write1(opconst.TypeProfileLen, p.Tag) + data[0] = byte(opconst.TypeProfile) +} + +func (p Event) ImplementsEvent() {} diff --git a/pkg/gel/gio/io/router/clipboard.go b/pkg/gel/gio/io/router/clipboard.go new file mode 100644 index 0000000..798d0b5 --- /dev/null +++ b/pkg/gel/gio/io/router/clipboard.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/io/event" +) + +type clipboardQueue struct { + receivers map[event.Tag]struct{} + // request avoid read clipboard every frame while waiting. + requested bool + text *string + reader ops.Reader +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *clipboardQueue) WriteClipboard() (string, bool) { + if q.text == nil { + return "", false + } + text := *q.text + q.text = nil + return text, true +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *clipboardQueue) ReadClipboard() bool { + if len(q.receivers) <= 0 || q.requested { + return false + } + q.requested = true + return true +} + +func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) { + for r := range q.receivers { + events.Add(r, e) + delete(q.receivers, r) + } +} + +func (q *clipboardQueue) ProcessWriteClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardWrite { + panic("invalid op") + } + q.text = refs[0].(*string) +} + +func (q *clipboardQueue) ProcessReadClipboard(d []byte, refs []interface{}) { + if opconst.OpType(d[0]) != opconst.TypeClipboardRead { + panic("invalid op") + } + if q.receivers == nil { + q.receivers = make(map[event.Tag]struct{}) + } + tag := refs[0].(event.Tag) + if _, ok := q.receivers[tag]; !ok { + q.receivers[tag] = struct{}{} + q.requested = false + } +} diff --git a/pkg/gel/gio/io/router/clipboard_test.go b/pkg/gel/gio/io/router/clipboard_test.go new file mode 100644 index 0000000..839f8ce --- /dev/null +++ b/pkg/gel/gio/io/router/clipboard_test.go @@ -0,0 +1,154 @@ +package router + +import ( + "testing" + + "github.com/p9c/p9/pkg/gel/gio/io/clipboard" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestClipboardDuplicateEvent(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + + // Both must receive the event once + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + clipboard.ReadOp{Tag: &handler[1]}.Add(ops) + + router.Frame(ops) + event := clipboard.Event{Text: "Test"} + router.Queue(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + assertClipboardEvent(t, router.Events(&handler[1]), true) + ops.Reset() + + // No ReadOp + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() + + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + // No ClipboardEvent sent + assertClipboardReadOp(t, router, 1) + assertClipboardEvent(t, router.Events(&handler[0]), false) + assertClipboardEvent(t, router.Events(&handler[1]), false) + ops.Reset() +} + +func TestQueueProcessReadClipboard(t *testing.T) { + ops, router, handler := new(op.Ops), new(Router), make([]int, 2) + ops.Reset() + + // Request read + clipboard.ReadOp{Tag: &handler[0]}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 1) + ops.Reset() + + for i := 0; i < 3; i++ { + // No ReadOp + // One receiver must still wait for response + + router.Frame(ops) + assertClipboardReadOpDuplicated(t, router, 1) + ops.Reset() + } + + router.Frame(ops) + // Send the clipboard event + event := clipboard.Event{Text: "Text 2"} + router.Queue(event) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), true) + ops.Reset() + + // No ReadOp + // There's no receiver waiting + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardEvent(t, router.Events(&handler[0]), false) + ops.Reset() +} + +func TestQueueProcessWriteClipboard(t *testing.T) { + ops, router := new(op.Ops), new(Router) + ops.Reset() + + clipboard.WriteOp{Text: "Write 1"}.Add(ops) + + router.Frame(ops) + assertClipboardWriteOp(t, router, "Write 1") + ops.Reset() + + // No WriteOp + + router.Frame(ops) + assertClipboardWriteOp(t, router, "") + ops.Reset() + + clipboard.WriteOp{Text: "Write 2"}.Add(ops) + + router.Frame(ops) + assertClipboardReadOp(t, router, 0) + assertClipboardWriteOp(t, router, "Write 2") + ops.Reset() +} + +func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) { + t.Helper() + var evtClipboard int + for _, e := range events { + switch e.(type) { + case clipboard.Event: + evtClipboard++ + } + } + if evtClipboard <= 0 && expected { + t.Error("expected to receive some event") + } + if evtClipboard > 0 && !expected { + t.Error("unexpected event received") + } +} + +func assertClipboardReadOp(t *testing.T, router *Router, expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("unexpected number of receivers") + } + if router.cqueue.ReadClipboard() != (expected > 0) { + t.Error("missing requests") + } +} + +func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) { + t.Helper() + if len(router.cqueue.receivers) != expected { + t.Error("receivers removed") + } + if router.cqueue.ReadClipboard() != false { + t.Error("duplicated requests") + } +} + +func assertClipboardWriteOp(t *testing.T, router *Router, expected string) { + t.Helper() + if (router.cqueue.text != nil) != (expected != "") { + t.Error("text not defined") + } + text, ok := router.cqueue.WriteClipboard() + if ok != (expected != "") { + t.Error("duplicated requests") + } + if text != expected { + t.Errorf("got text %s, expected %s", text, expected) + } +} diff --git a/pkg/gel/gio/io/router/key.go b/pkg/gel/gio/io/router/key.go new file mode 100644 index 0000000..04189e5 --- /dev/null +++ b/pkg/gel/gio/io/router/key.go @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +type TextInputState uint8 + +type keyQueue struct { + focus event.Tag + handlers map[event.Tag]*keyHandler + reader ops.Reader + state TextInputState +} + +type keyHandler struct { + // visible will be true if the InputOp is present + // in the current frame. + visible bool + new bool +} + +const ( + TextInputKeep TextInputState = iota + TextInputClose + TextInputOpen +) + +// InputState returns the last text input state as +// determined in Frame. +func (q *keyQueue) InputState() TextInputState { + return q.state +} + +func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) { + if q.handlers == nil { + q.handlers = make(map[event.Tag]*keyHandler) + } + for _, h := range q.handlers { + h.visible, h.new = false, false + } + q.reader.Reset(root) + + focus, changed, state := q.resolveFocus(events) + for k, h := range q.handlers { + if !h.visible { + delete(q.handlers, k) + if q.focus == k { + // Remove the focus from the handler that is no longer visible. + q.focus = nil + state = TextInputClose + } + } else if h.new && k != focus { + // Reset the handler on (each) first appearance, but don't trigger redraw. + events.AddNoRedraw(k, key.FocusEvent{Focus: false}) + } + } + if changed && focus != nil { + if _, exists := q.handlers[focus]; !exists { + focus = nil + } + } + if changed && focus != q.focus { + if q.focus != nil { + events.Add(q.focus, key.FocusEvent{Focus: false}) + } + q.focus = focus + if q.focus != nil { + events.Add(q.focus, key.FocusEvent{Focus: true}) + } else { + state = TextInputClose + } + } + q.state = state +} + +func (q *keyQueue) Push(e event.Event, events *handlerEvents) { + if q.focus != nil { + events.Add(q.focus, e) + } +} + +func (q *keyQueue) resolveFocus(events *handlerEvents) (focus event.Tag, changed bool, state TextInputState) { + for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeKeyFocus: + op := decodeFocusOp(encOp.Data, encOp.Refs) + changed = true + focus = op.Tag + case opconst.TypeKeySoftKeyboard: + op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs) + if op.Show { + state = TextInputOpen + } else { + state = TextInputClose + } + case opconst.TypeKeyInput: + op := decodeKeyInputOp(encOp.Data, encOp.Refs) + h, ok := q.handlers[op.Tag] + if !ok { + h = &keyHandler{new: true} + q.handlers[op.Tag] = h + } + h.visible = true + } + } + return +} + +func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp { + if opconst.OpType(d[0]) != opconst.TypeKeyInput { + panic("invalid op") + } + return key.InputOp{ + Tag: refs[0].(event.Tag), + } +} + +func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp { + if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard { + panic("invalid op") + } + return key.SoftKeyboardOp{ + Show: d[1] != 0, + } +} + +func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp { + if opconst.OpType(d[0]) != opconst.TypeKeyFocus { + panic("invalid op") + } + return key.FocusOp{ + Tag: refs[0], + } +} diff --git a/pkg/gel/gio/io/router/key_test.go b/pkg/gel/gio/io/router/key_test.go new file mode 100644 index 0000000..bf88294 --- /dev/null +++ b/pkg/gel/gio/io/router/key_test.go @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "reflect" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestKeyWakeup(t *testing.T) { + handler := new(int) + var ops op.Ops + key.InputOp{Tag: handler}.Add(&ops) + + var r Router + // Test that merely adding a handler doesn't trigger redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); wake { + t.Errorf("adding key.InputOp triggered a redraw") + } + // However, adding a handler queues a Focus(false) event. + if evts := r.Events(handler); len(evts) != 1 { + t.Errorf("no Focus event for newly registered key.InputOp") + } + // Verify that r.Events does trigger a redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); !wake { + t.Errorf("key.FocusEvent event didn't trigger a redraw") + } +} + +func TestKeyMultiples(t *testing.T) { + handlers := make([]int, 3) + ops := new(op.Ops) + r := new(Router) + + key.SoftKeyboardOp{Show: true}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: &handlers[2]}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + + // The last one must be focused: + key.InputOp{Tag: &handlers[2]}.Add(ops) + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertKeyEvent(t, r.Events(&handlers[2]), true) + assertFocus(t, r, &handlers[2]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyStacked(t *testing.T) { + handlers := make([]int, 4) + ops := new(op.Ops) + r := new(Router) + + s := op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: nil}.Add(ops) + s.Load() + s = op.Save(ops) + key.SoftKeyboardOp{Show: false}.Add(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Tag: &handlers[1]}.Add(ops) + s.Load() + s = op.Save(ops) + key.InputOp{Tag: &handlers[2]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + s = op.Save(ops) + key.InputOp{Tag: &handlers[3]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEvent(t, r.Events(&handlers[1]), true) + assertKeyEvent(t, r.Events(&handlers[2]), false) + assertKeyEvent(t, r.Events(&handlers[3]), false) + assertFocus(t, r, &handlers[1]) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeySoftKeyboardNoFocus(t *testing.T) { + ops := new(op.Ops) + r := new(Router) + + // It's possible to open the keyboard + // without any active focus: + key.SoftKeyboardOp{Show: true}.Add(ops) + + r.Frame(ops) + + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyRemoveFocus(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // New InputOp with Focus and Keyboard: + s := op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // New InputOp without any focus: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + // Add some key events: + event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}) + r.Queue(event) + + assertKeyEvent(t, r.Events(&handlers[0]), true, event) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // Will get the focus removed: + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + // Unchanged: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + // Remove focus by focusing on a tag that don't exist. + s = op.Save(ops) + key.FocusOp{Tag: new(int)}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + + ops.Reset() + + // Set focus to InputOp which already + // exists in the previous frame: + s = op.Save(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // Remove focus. + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + key.FocusOp{Tag: nil}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputOpen) +} + +func TestKeyFocusedInvisible(t *testing.T) { + handlers := make([]int, 2) + ops := new(op.Ops) + r := new(Router) + + // Set new InputOp with focus: + s := op.Save(ops) + key.FocusOp{Tag: &handlers[0]}.Add(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + key.SoftKeyboardOp{Show: true}.Add(ops) + s.Load() + + // Set new InputOp without focus: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), true) + assertKeyEvent(t, r.Events(&handlers[1]), false) + assertFocus(t, r, &handlers[0]) + assertKeyboard(t, r, TextInputOpen) + + ops.Reset() + + // + // Removed first (focused) element! + // + + // Unchanged: + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEventUnexpected(t, r.Events(&handlers[0])) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputClose) + + ops.Reset() + + // Respawn the first element: + // It must receive one `Event{Focus: false}`. + s = op.Save(ops) + key.InputOp{Tag: &handlers[0]}.Add(ops) + s.Load() + + // Unchanged + s = op.Save(ops) + key.InputOp{Tag: &handlers[1]}.Add(ops) + s.Load() + + r.Frame(ops) + + assertKeyEvent(t, r.Events(&handlers[0]), false) + assertKeyEventUnexpected(t, r.Events(&handlers[1])) + assertFocus(t, r, nil) + assertKeyboard(t, r, TextInputKeep) + +} + +func assertKeyEvent(t *testing.T, events []event.Event, expected bool, expectedInputs ...event.Event) { + t.Helper() + var evtFocus int + var evtKeyPress int + for _, e := range events { + switch ev := e.(type) { + case key.FocusEvent: + if ev.Focus != expected { + t.Errorf("focus is expected to be %v, got %v", expected, ev.Focus) + } + evtFocus++ + case key.Event, key.EditEvent: + if len(expectedInputs) <= evtKeyPress { + t.Errorf("unexpected key events") + } + if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) { + t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev) + } + evtKeyPress++ + } + } + if evtFocus <= 0 { + t.Errorf("expected focus event") + } + if evtFocus > 1 { + t.Errorf("expected single focus event") + } + if evtKeyPress != len(expectedInputs) { + t.Errorf("expected key events") + } +} + +func assertKeyEventUnexpected(t *testing.T, events []event.Event) { + t.Helper() + var evtFocus int + for _, e := range events { + switch e.(type) { + case key.FocusEvent: + evtFocus++ + } + } + if evtFocus > 1 { + t.Errorf("unexpected focus event") + } +} + +func assertFocus(t *testing.T, router *Router, expected event.Tag) { + t.Helper() + if router.kqueue.focus != expected { + t.Errorf("expected %v to be focused, got %v", expected, router.kqueue.focus) + } +} + +func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { + t.Helper() + if router.kqueue.state != expected { + t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state) + } +} diff --git a/pkg/gel/gio/io/router/pointer.go b/pkg/gel/gio/io/router/pointer.go new file mode 100644 index 0000000..7ca2167 --- /dev/null +++ b/pkg/gel/gio/io/router/pointer.go @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "encoding/binary" + "image" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +type pointerQueue struct { + hitTree []hitNode + areas []areaNode + cursors []cursorNode + cursor pointer.CursorName + handlers map[event.Tag]*pointerHandler + pointers []pointerInfo + reader ops.Reader + + // states holds the storage for save/restore ops. + states []collectState + scratch []event.Tag +} + +type hitNode struct { + next int + area int + // Pass tracks the most recent PassOp mode. + pass bool + + // For handler nodes. + tag event.Tag +} + +type cursorNode struct { + name pointer.CursorName + area int +} + +type pointerInfo struct { + id pointer.ID + pressed bool + handlers []event.Tag + // last tracks the last pointer event received, + // used while processing frame events. + last pointer.Event + + // entered tracks the tags that contain the pointer. + entered []event.Tag +} + +type pointerHandler struct { + area int + active bool + wantsGrab bool + types pointer.Type + // min and max horizontal/vertical scroll + scrollRange image.Rectangle +} + +type areaOp struct { + kind areaKind + rect f32.Rectangle +} + +type areaNode struct { + trans f32.Affine2D + next int + area areaOp +} + +type areaKind uint8 + +// collectState represents the state for collectHandlers +type collectState struct { + t f32.Affine2D + area int + node int + pass bool +} + +const ( + areaRect areaKind = iota + areaEllipse +) + +func (q *pointerQueue) save(id int, state collectState) { + if extra := id - len(q.states) + 1; extra > 0 { + q.states = append(q.states, make([]collectState, extra)...) + } + q.states[id] = state +} + +func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) { + state := collectState{ + area: -1, + node: -1, + } + q.save(opconst.InitialStateID, state) + for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeSave: + id := ops.DecodeSave(encOp.Data) + q.save(id, state) + case opconst.TypeLoad: + id, mask := ops.DecodeLoad(encOp.Data) + s := q.states[id] + if mask&opconst.TransformState != 0 { + state.t = s.t + } + if mask&^opconst.TransformState != 0 { + state = s + } + case opconst.TypePass: + state.pass = encOp.Data[1] != 0 + case opconst.TypeArea: + var op areaOp + op.Decode(encOp.Data) + q.areas = append(q.areas, areaNode{trans: state.t, next: state.area, area: op}) + state.area = len(q.areas) - 1 + q.hitTree = append(q.hitTree, hitNode{ + next: state.node, + area: state.area, + pass: state.pass, + }) + state.node = len(q.hitTree) - 1 + case opconst.TypeTransform: + dop := ops.DecodeTransform(encOp.Data) + state.t = state.t.Mul(dop) + case opconst.TypePointerInput: + op := pointer.InputOp{ + Tag: encOp.Refs[0].(event.Tag), + Grab: encOp.Data[1] != 0, + Types: pointer.Type(encOp.Data[2]), + } + q.hitTree = append(q.hitTree, hitNode{ + next: state.node, + area: state.area, + pass: state.pass, + tag: op.Tag, + }) + state.node = len(q.hitTree) - 1 + h, ok := q.handlers[op.Tag] + if !ok { + h = new(pointerHandler) + q.handlers[op.Tag] = h + // Cancel handlers on (each) first appearance, but don't + // trigger redraw. + events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel}) + } + h.active = true + h.area = state.area + h.wantsGrab = h.wantsGrab || op.Grab + h.types = h.types | op.Types + bo := binary.LittleEndian.Uint32 + h.scrollRange = image.Rectangle{ + Min: image.Point{ + X: int(int32(bo(encOp.Data[3:]))), + Y: int(int32(bo(encOp.Data[7:]))), + }, + Max: image.Point{ + X: int(int32(bo(encOp.Data[11:]))), + Y: int(int32(bo(encOp.Data[15:]))), + }, + } + case opconst.TypeCursor: + q.cursors = append(q.cursors, cursorNode{ + name: encOp.Refs[0].(pointer.CursorName), + area: len(q.areas) - 1, + }) + } + } +} + +func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) { + // Track whether we're passing through hits. + pass := true + idx := len(q.hitTree) - 1 + for idx >= 0 { + n := &q.hitTree[idx] + if !q.hit(n.area, pos) { + idx-- + continue + } + pass = pass && n.pass + if pass { + idx-- + } else { + idx = n.next + } + if n.tag != nil { + if _, exists := q.handlers[n.tag]; exists { + *handlers = append(*handlers, n.tag) + } + } + } +} + +func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point { + if areaIdx == -1 { + return p + } + return q.areas[areaIdx].trans.Invert().Transform(p) +} + +func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool { + for areaIdx != -1 { + a := &q.areas[areaIdx] + p := a.trans.Invert().Transform(p) + if !a.area.Hit(p) { + return false + } + areaIdx = a.next + } + return true +} + +func (q *pointerQueue) reset() { + if q.handlers == nil { + q.handlers = make(map[event.Tag]*pointerHandler) + } +} + +func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) { + q.reset() + for _, h := range q.handlers { + // Reset handler. + h.active = false + h.wantsGrab = false + h.types = 0 + } + q.hitTree = q.hitTree[:0] + q.areas = q.areas[:0] + q.cursors = q.cursors[:0] + q.reader.Reset(root) + q.collectHandlers(&q.reader, events) + for k, h := range q.handlers { + if !h.active { + q.dropHandlers(events, k) + delete(q.handlers, k) + } + if h.wantsGrab { + for _, p := range q.pointers { + if !p.pressed { + continue + } + for i, k2 := range p.handlers { + if k2 == k { + // Drop other handlers that lost their grab. + dropped := make([]event.Tag, 0, len(p.handlers)-1) + dropped = append(dropped, p.handlers[:i]...) + dropped = append(dropped, p.handlers[i+1:]...) + cancelHandlers(events, dropped...) + q.dropHandlers(events, dropped...) + break + } + } + } + } + } + for i := range q.pointers { + p := &q.pointers[i] + q.deliverEnterLeaveEvents(p, events, p.last) + } +} + +func cancelHandlers(events *handlerEvents, tags ...event.Tag) { + for _, k := range tags { + events.Add(k, pointer.Event{Type: pointer.Cancel}) + } +} + +func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) { + for _, k := range tags { + for i := range q.pointers { + p := &q.pointers[i] + for i := len(p.handlers) - 1; i >= 0; i-- { + if p.handlers[i] == k { + p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) + } + } + for i := len(p.entered) - 1; i >= 0; i-- { + if p.entered[i] == k { + p.entered = append(p.entered[:i], p.entered[i+1:]...) + } + } + } + } +} + +func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) { + q.reset() + if e.Type == pointer.Cancel { + q.pointers = q.pointers[:0] + for k := range q.handlers { + cancelHandlers(events, k) + q.dropHandlers(events, k) + } + return + } + pidx := -1 + for i, p := range q.pointers { + if p.id == e.PointerID { + pidx = i + break + } + } + if pidx == -1 { + q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) + pidx = len(q.pointers) - 1 + } + p := &q.pointers[pidx] + p.last = e + + if e.Type == pointer.Move && p.pressed { + e.Type = pointer.Drag + } + + if e.Type == pointer.Release { + q.deliverEvent(p, events, e) + p.pressed = false + } + q.deliverEnterLeaveEvents(p, events, e) + + if !p.pressed { + p.handlers = append(p.handlers[:0], q.scratch...) + } + if e.Type == pointer.Press { + p.pressed = true + } + switch e.Type { + case pointer.Release: + case pointer.Scroll: + q.deliverScrollEvent(p, events, e) + default: + q.deliverEvent(p, events, e) + } + if !p.pressed && len(p.entered) == 0 { + // No longer need to track pointer. + q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) + } +} + +func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) { + foremost := true + if p.pressed && len(p.handlers) == 1 { + e.Priority = pointer.Grabbed + foremost = false + } + for _, k := range p.handlers { + h := q.handlers[k] + if e.Type&h.types == 0 { + continue + } + e := e + if foremost { + foremost = false + e.Priority = pointer.Foremost + } + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } +} + +func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) { + foremost := true + if p.pressed && len(p.handlers) == 1 { + e.Priority = pointer.Grabbed + foremost = false + } + var sx, sy = e.Scroll.X, e.Scroll.Y + for _, k := range p.handlers { + if sx == 0 && sy == 0 { + return + } + h := q.handlers[k] + // Distribute the scroll to the handler based on its ScrollRange. + sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X) + sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y) + e := e + if foremost { + foremost = false + e.Priority = pointer.Foremost + } + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } +} + +func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) { + q.scratch = q.scratch[:0] + q.opHit(&q.scratch, e.Position) + if p.pressed { + // Filter out non-participating handlers. + for i := len(q.scratch) - 1; i >= 0; i-- { + if _, found := searchTag(p.handlers, q.scratch[i]); !found { + q.scratch = append(q.scratch[:i], q.scratch[i+1:]...) + } + } + } + hits := q.scratch + if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press { + // Consider non-mouse pointers leaving when they're released. + hits = nil + } + // Deliver Leave events. + for _, k := range p.entered { + if _, found := searchTag(hits, k); found { + continue + } + h := q.handlers[k] + e.Type = pointer.Leave + + if e.Type&h.types != 0 { + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } + } + // Deliver Enter events and update cursor. + q.cursor = pointer.CursorDefault + for _, k := range hits { + h := q.handlers[k] + for i := len(q.cursors) - 1; i >= 0; i-- { + if c := q.cursors[i]; c.area == h.area { + q.cursor = c.name + break + } + } + if _, found := searchTag(p.entered, k); found { + continue + } + e.Type = pointer.Enter + + if e.Type&h.types != 0 { + e.Position = q.invTransform(h.area, e.Position) + events.Add(k, e) + } + } + p.entered = append(p.entered[:0], hits...) +} + +func searchTag(tags []event.Tag, tag event.Tag) (int, bool) { + for i, t := range tags { + if t == tag { + return i, true + } + } + return 0, false +} + +func opDecodeFloat32(d []byte) float32 { + return float32(int32(binary.LittleEndian.Uint32(d))) +} + +func (op *areaOp) Decode(d []byte) { + if opconst.OpType(d[0]) != opconst.TypeArea { + panic("invalid op") + } + rect := f32.Rectangle{ + Min: f32.Point{ + X: opDecodeFloat32(d[2:]), + Y: opDecodeFloat32(d[6:]), + }, + Max: f32.Point{ + X: opDecodeFloat32(d[10:]), + Y: opDecodeFloat32(d[14:]), + }, + } + *op = areaOp{ + kind: areaKind(d[1]), + rect: rect, + } +} + +func (op *areaOp) Hit(pos f32.Point) bool { + pos = pos.Sub(op.rect.Min) + size := op.rect.Size() + switch op.kind { + case areaRect: + return 0 <= pos.X && pos.X < size.X && + 0 <= pos.Y && pos.Y < size.Y + case areaEllipse: + rx := size.X / 2 + ry := size.Y / 2 + xh := pos.X - rx + yk := pos.Y - ry + // The ellipse function works in all cases because + // 0/0 is not <= 1. + return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1 + default: + panic("invalid area kind") + } +} + +func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) { + if v := float32(max); scroll > v { + return scroll - v, v + } + if v := float32(min); scroll < v { + return scroll - v, v + } + return 0, scroll +} diff --git a/pkg/gel/gio/io/router/pointer_test.go b/pkg/gel/gio/io/router/pointer_test.go new file mode 100644 index 0000000..ef7c812 --- /dev/null +++ b/pkg/gel/gio/io/router/pointer_test.go @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package router + +import ( + "fmt" + "image" + "reflect" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestPointerWakeup(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + + var r Router + // Test that merely adding a handler doesn't trigger redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); wake { + t.Errorf("adding pointer.InputOp triggered a redraw") + } + // However, adding a handler queues a Cancel event. + assertEventSequence(t, r.Events(handler), pointer.Cancel) + // Verify that r.Events does trigger a redraw. + r.Frame(&ops) + if _, wake := r.WakeupTime(); !wake { + t.Errorf("pointer.Cancel event didn't trigger a redraw") + } +} + +func TestPointerDrag(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + + var r Router + r.Frame(&ops) + r.Queue( + // Press. + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + // Move outside the area. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) +} + +func TestPointerDragNegative(t *testing.T) { + handler := new(int) + var ops op.Ops + addPointerHandler(&ops, handler, image.Rect(-100, -100, 0, 0)) + + var r Router + r.Frame(&ops) + r.Queue( + // Press. + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(-50, -50), + }, + // Move outside the area. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(-150, -150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) +} + +func TestPointerGrab(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + handler3 := new(int) + var ops op.Ops + + types := pointer.Press | pointer.Release + + pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + pointer.InputOp{Tag: handler3, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press) + assertEventSequence(t, r.Events(handler3), pointer.Cancel, pointer.Press) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Release) + assertEventSequence(t, r.Events(handler2), pointer.Cancel) + assertEventSequence(t, r.Events(handler3), pointer.Cancel) +} + +func TestPointerMove(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + types := pointer.Move | pointer.Enter | pointer.Leave + + // Handler 1 area: (0, 0) - (100, 100) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{Tag: handler1, Types: types}.Add(&ops) + // Handler 2 area: (50, 50) - (100, 100) (areas intersect). + pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + // Hit both handlers. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + // Hit handler 1. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(49, 50), + }, + // Hit no handlers. + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(100, 50), + }, + pointer.Event{ + Type: pointer.Cancel, + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel) +} + +func TestPointerTypes(t *testing.T) { + handler := new(int) + var ops op.Ops + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{ + Tag: handler, + Types: pointer.Press | pointer.Release, + }.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(150, 150), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release) +} + +func TestPointerPriority(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + handler3 := new(int) + var ops op.Ops + + st := op.Save(&ops) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{ + Tag: handler1, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, + }.Add(&ops) + + pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops) + pointer.InputOp{ + Tag: handler2, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, + }.Add(&ops) + st.Load() + + pointer.Rect(image.Rect(0, 100, 100, 200)).Add(&ops) + pointer.InputOp{ + Tag: handler3, + Types: pointer.Scroll, + ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, + }.Add(&ops) + + var r Router + r.Frame(&ops) + r.Queue( + // Hit handler 1 and 2. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 25), + Scroll: f32.Pt(50, 0), + }, + // Hit handler 1. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 75), + Scroll: f32.Pt(50, 50), + }, + // Hit handler 3. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 150), + Scroll: f32.Pt(-30, -30), + }, + // Hit no handlers. + pointer.Event{ + Type: pointer.Scroll, + Position: f32.Pt(50, 225), + }, + ) + + hev1 := r.Events(handler1) + hev2 := r.Events(handler2) + hev3 := r.Events(handler3) + assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll) + assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll) + assertEventSequence(t, hev3, pointer.Cancel, pointer.Scroll) + assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared, pointer.Foremost) + assertEventPriorities(t, hev2, pointer.Shared, pointer.Foremost) + assertEventPriorities(t, hev3, pointer.Shared, pointer.Foremost) + assertScrollEvent(t, hev1[1], f32.Pt(30, 0)) + assertScrollEvent(t, hev2[1], f32.Pt(20, 0)) + assertScrollEvent(t, hev1[2], f32.Pt(50, 0)) + assertScrollEvent(t, hev3[1], f32.Pt(-20, -30)) +} + +func TestPointerEnterLeave(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + // Handler 1 area: (0, 0) - (100, 100) + addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + + // Handler 2 area: (50, 50) - (200, 200) (areas overlap). + addPointerHandler(&ops, handler2, image.Rect(50, 50, 200, 200)) + + var r Router + r.Frame(&ops) + // Hit both handlers. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + // First event for a handler is always a Cancel. + // Only handler2 should receive the enter/move events because it is on top + // and handler1 is not an ancestor in the hit tree. + assertEventSequence(t, r.Events(handler1), pointer.Cancel) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, pointer.Move) + + // Leave the second area by moving into the first. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(45, 45), + }, + ) + // The cursor leaves handler2 and enters handler1. + assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Leave) + + // Move, but stay within the same hit area. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(40, 40), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2)) + + // Move outside of both inputs. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(300, 300), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Leave) + assertEventSequence(t, r.Events(handler2)) + + // Check that a Press event generates Enter Events. + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(125, 125), + }, + ) + assertEventSequence(t, r.Events(handler1)) + assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + + // Check that a drag only affects the participating handlers. + r.Queue( + // Leave + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + // Enter + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1)) + assertEventSequence(t, r.Events(handler2), pointer.Leave, pointer.Drag, pointer.Enter, pointer.Drag) + + // Check that a Release event generates Enter/Leave Events. + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(25, + 25), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Enter) + // The second handler gets the release event because the press started inside it. + assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) + +} + +func TestMultipleAreas(t *testing.T) { + handler := new(int) + + var ops op.Ops + + addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + st := op.Save(&ops) + pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops) + // Second area has no Types set, yet should receive events because + // Types for the same handles are or-ed together. + pointer.InputOp{Tag: handler}.Add(&ops) + st.Load() + + var r Router + r.Frame(&ops) + // Hit first area, then second area, then both. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(150, 150), + }, + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Move) +} + +func TestPointerEnterLeaveNested(t *testing.T) { + handler1 := new(int) + handler2 := new(int) + var ops op.Ops + + types := pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave + + // Handler 1 area: (0, 0) - (100, 100) + pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) + pointer.InputOp{Tag: handler1, Types: types}.Add(&ops) + + // Handler 2 area: (25, 25) - (75, 75) (nested within first). + pointer.Rect(image.Rect(25, 25, 75, 75)).Add(&ops) + pointer.InputOp{Tag: handler2, Types: types}.Add(&ops) + + var r Router + r.Frame(&ops) + // Hit both handlers. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + // First event for a handler is always a Cancel. + // Both handlers should receive the Enter and Move events because handler2 is a child of handler1. + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, pointer.Move) + + // Leave the second area by moving into the first. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(20, 20), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2), pointer.Leave) + + // Move, but stay within the same hit area. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(10, 10), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Move) + assertEventSequence(t, r.Events(handler2)) + + // Move outside of both inputs. + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(200, 200), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Leave) + assertEventSequence(t, r.Events(handler2)) + + // Check that a Press event generates Enter Events. + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Press) + assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + + // Check that a Release event generates Enter/Leave Events. + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: f32.Pt(20, 20), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Release) + assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) +} + +func TestPointerActiveInputDisappears(t *testing.T) { + handler1 := new(int) + var ops op.Ops + var r Router + + // Draw handler. + ops.Reset() + addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + ) + assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move) + + // Re-render with handler missing. + ops.Reset() + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(25, 25), + }, + ) + assertEventSequence(t, r.Events(handler1)) +} + +func TestMultitouch(t *testing.T) { + var ops op.Ops + + // Add two separate handlers. + h1, h2 := new(int), new(int) + addPointerHandler(&ops, h1, image.Rect(0, 0, 100, 100)) + addPointerHandler(&ops, h2, image.Rect(0, 100, 100, 200)) + + h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100) + var p1, p2 pointer.ID = 0, 1 + + var r Router + r.Frame(&ops) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: h1pt, + PointerID: p1, + }, + ) + r.Queue( + pointer.Event{ + Type: pointer.Press, + Position: h2pt, + PointerID: p2, + }, + ) + r.Queue( + pointer.Event{ + Type: pointer.Release, + Position: h2pt, + PointerID: p2, + }, + ) + assertEventSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter, pointer.Press) + assertEventSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Release) +} + +func TestCursorNameOp(t *testing.T) { + ops := new(op.Ops) + var r Router + var h, h2 int + var widget2 func() + widget := func() { + // This is the area where the cursor is changed to CursorPointer. + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + // The cursor is checked and changed upon cursor movement. + pointer.InputOp{Tag: &h}.Add(ops) + pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(ops) + if widget2 != nil { + widget2() + } + } + // Register the handlers. + widget() + // No cursor change as the mouse has not moved yet. + if got, want := r.Cursor(), pointer.CursorDefault; got != want { + t.Errorf("got %q; want %q", got, want) + } + + _at := func(x, y float32) pointer.Event { + return pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Position: f32.Pt(x, y), + } + } + for _, tc := range []struct { + label string + event interface{} + want pointer.CursorName + }{ + {label: "move inside", + event: _at(50, 50), + want: pointer.CursorPointer, + }, + {label: "move outside", + event: _at(200, 200), + want: pointer.CursorDefault, + }, + {label: "move back inside", + event: _at(50, 50), + want: pointer.CursorPointer, + }, + {label: "send key events while inside", + event: []event.Event{ + key.Event{Name: "A", State: key.Press}, + key.Event{Name: "A", State: key.Release}, + }, + want: pointer.CursorPointer, + }, + {label: "send key events while outside", + event: []event.Event{ + _at(200, 200), + key.Event{Name: "A", State: key.Press}, + key.Event{Name: "A", State: key.Release}, + }, + want: pointer.CursorDefault, + }, + {label: "add new input on top while inside", + event: func() []event.Event { + widget2 = func() { + pointer.InputOp{Tag: &h2}.Add(ops) + pointer.CursorNameOp{Name: pointer.CursorCrossHair}.Add(ops) + } + return []event.Event{ + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + } + }, + want: pointer.CursorCrossHair, + }, + {label: "remove input on top while inside", + event: func() []event.Event { + widget2 = nil + return []event.Event{ + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + } + }, + want: pointer.CursorPointer, + }, + } { + t.Run(tc.label, func(t *testing.T) { + ops.Reset() + widget() + r.Frame(ops) + switch ev := tc.event.(type) { + case event.Event: + r.Queue(ev) + case []event.Event: + r.Queue(ev...) + case func() event.Event: + r.Queue(ev()) + case func() []event.Event: + r.Queue(ev()...) + default: + panic(fmt.Sprintf("unkown event %T", ev)) + } + widget() + r.Frame(ops) + // The cursor should now have been changed if the mouse moved over the declared area. + if got, want := r.Cursor(), tc.want; got != want { + t.Errorf("got %q; want %q", got, want) + } + }) + } +} + +// addPointerHandler adds a pointer.InputOp for the tag in a +// rectangular area. +func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) { + defer op.Save(ops).Load() + pointer.Rect(area).Add(ops) + pointer.InputOp{ + Tag: tag, + Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave, + }.Add(ops) +} + +// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes +// that all input events are of underlying type pointer.Event, and thus will +// panic if some are not. +func pointerTypes(events []event.Event) []pointer.Type { + var types []pointer.Type + for _, e := range events { + if e, ok := e.(pointer.Event); ok { + types = append(types, e.Type) + } + } + return types +} + +// assertEventSequence checks that the provided events match the expected pointer event types +// in the provided order. +func assertEventSequence(t *testing.T, events []event.Event, expected ...pointer.Type) { + t.Helper() + got := pointerTypes(events) + if !reflect.DeepEqual(got, expected) { + t.Errorf("expected %v events, got %v", expected, got) + } +} + +// assertEventPriorities checks that the pointer.Event priorities of events match prios. +func assertEventPriorities(t *testing.T, events []event.Event, prios ...pointer.Priority) { + t.Helper() + var got []pointer.Priority + for _, e := range events { + if e, ok := e.(pointer.Event); ok { + got = append(got, e.Priority) + } + } + if !reflect.DeepEqual(got, prios) { + t.Errorf("expected priorities %v, got %v", prios, got) + } +} + +// assertScrollEvent checks that the event scrolling amount matches the supplied value. +func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) { + t.Helper() + if got, want := ev.(pointer.Event).Scroll, scroll; got != want { + t.Errorf("got %v; want %v", got, want) + } +} + +func BenchmarkRouterAdd(b *testing.B) { + // Set this to the number of overlapping handlers that you want to + // evaluate performance for. Typical values for the example applications + // are 1-3, though checking highers values helps evaluate performance for + // more complex applications. + const startingHandlerCount = 3 + const maxHandlerCount = 100 + for i := startingHandlerCount; i < maxHandlerCount; i *= 3 { + handlerCount := i + b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) { + handlers := make([]event.Tag, handlerCount) + for i := 0; i < handlerCount; i++ { + h := new(int) + *h = i + handlers[i] = h + } + var ops op.Ops + + for i := range handlers { + pointer.Rect(image.Rectangle{ + Max: image.Point{ + X: 100, + Y: 100, + }, + }).Add(&ops) + pointer.InputOp{ + Tag: handlers[i], + Types: pointer.Move, + }.Add(&ops) + } + var r Router + r.Frame(&ops) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Queue( + pointer.Event{ + Type: pointer.Move, + Position: f32.Pt(50, 50), + }, + ) + } + }) + } +} + +var benchAreaOp areaOp + +func BenchmarkAreaOp_Decode(b *testing.B) { + ops := new(op.Ops) + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + for i := 0; i < b.N; i++ { + benchAreaOp.Decode(ops.Data()) + } +} + +func BenchmarkAreaOp_Hit(b *testing.B) { + ops := new(op.Ops) + pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops) + benchAreaOp.Decode(ops.Data()) + for i := 0; i < b.N; i++ { + benchAreaOp.Hit(f32.Pt(50, 50)) + } +} diff --git a/pkg/gel/gio/io/router/router.go b/pkg/gel/gio/io/router/router.go new file mode 100644 index 0000000..17e0a92 --- /dev/null +++ b/pkg/gel/gio/io/router/router.go @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package router implements Router, a event.Queue implementation +that that disambiguates and routes events to handlers declared +in operation lists. + +Router is used by app.Window and is otherwise only useful for +using Gio with external window implementations. +*/ +package router + +import ( + "encoding/binary" + "time" + + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/io/clipboard" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/io/profile" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Router is a Queue implementation that routes events +// to handlers declared in operation lists. +type Router struct { + pqueue pointerQueue + kqueue keyQueue + cqueue clipboardQueue + + handlers handlerEvents + + reader ops.Reader + + // InvalidateOp summary. + wakeup bool + wakeupTime time.Time + + // ProfileOp summary. + profHandlers map[event.Tag]struct{} + profile profile.Event +} + +type handlerEvents struct { + handlers map[event.Tag][]event.Event + hadEvents bool +} + +// Events returns the available events for the handler key. +func (q *Router) Events(k event.Tag) []event.Event { + events := q.handlers.Events(k) + if _, isprof := q.profHandlers[k]; isprof { + delete(q.profHandlers, k) + events = append(events, q.profile) + } + return events +} + +// Frame replaces the declared handlers from the supplied +// operation list. The text input state, wakeup time and whether +// there are active profile handlers is also saved. +func (q *Router) Frame(ops *op.Ops) { + q.handlers.Clear() + q.wakeup = false + for k := range q.profHandlers { + delete(q.profHandlers, k) + } + q.reader.Reset(ops) + q.collect() + + q.pqueue.Frame(ops, &q.handlers) + q.kqueue.Frame(ops, &q.handlers) + if q.handlers.HadEvents() { + q.wakeup = true + q.wakeupTime = time.Time{} + } +} + +// Queue an event and report whether at least one handler had an event queued. +func (q *Router) Queue(events ...event.Event) bool { + for _, e := range events { + switch e := e.(type) { + case profile.Event: + q.profile = e + case pointer.Event: + q.pqueue.Push(e, &q.handlers) + case key.EditEvent, key.Event, key.FocusEvent: + q.kqueue.Push(e, &q.handlers) + case clipboard.Event: + q.cqueue.Push(e, &q.handlers) + } + } + return q.handlers.HadEvents() +} + +// TextInputState returns the input state from the most recent +// call to Frame. +func (q *Router) TextInputState() TextInputState { + return q.kqueue.InputState() +} + +// WriteClipboard returns the most recent text to be copied +// to the clipboard, if any. +func (q *Router) WriteClipboard() (string, bool) { + return q.cqueue.WriteClipboard() +} + +// ReadClipboard reports if any new handler is waiting +// to read the clipboard. +func (q *Router) ReadClipboard() bool { + return q.cqueue.ReadClipboard() +} + +// Cursor returns the last cursor set. +func (q *Router) Cursor() pointer.CursorName { + return q.pqueue.cursor +} + +func (q *Router) collect() { + for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { + switch opconst.OpType(encOp.Data[0]) { + case opconst.TypeInvalidate: + op := decodeInvalidateOp(encOp.Data) + if !q.wakeup || op.At.Before(q.wakeupTime) { + q.wakeup = true + q.wakeupTime = op.At + } + case opconst.TypeProfile: + op := decodeProfileOp(encOp.Data, encOp.Refs) + if q.profHandlers == nil { + q.profHandlers = make(map[event.Tag]struct{}) + } + q.profHandlers[op.Tag] = struct{}{} + case opconst.TypeClipboardRead: + q.cqueue.ProcessReadClipboard(encOp.Data, encOp.Refs) + case opconst.TypeClipboardWrite: + q.cqueue.ProcessWriteClipboard(encOp.Data, encOp.Refs) + } + } +} + +// Profiling reports whether there was profile handlers in the +// most recent Frame call. +func (q *Router) Profiling() bool { + return len(q.profHandlers) > 0 +} + +// WakeupTime returns the most recent time for doing another frame, +// as determined from the last call to Frame. +func (q *Router) WakeupTime() (time.Time, bool) { + return q.wakeupTime, q.wakeup +} + +func (h *handlerEvents) init() { + if h.handlers == nil { + h.handlers = make(map[event.Tag][]event.Event) + } +} + +func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) { + h.init() + h.handlers[k] = append(h.handlers[k], e) +} + +func (h *handlerEvents) Add(k event.Tag, e event.Event) { + h.AddNoRedraw(k, e) + h.hadEvents = true +} + +func (h *handlerEvents) HadEvents() bool { + u := h.hadEvents + h.hadEvents = false + return u +} + +func (h *handlerEvents) Events(k event.Tag) []event.Event { + if events, ok := h.handlers[k]; ok { + h.handlers[k] = h.handlers[k][:0] + // Schedule another frame if we delivered events to the user + // to flush half-updated state. This is important when an + // event changes UI state that has already been laid out. In + // the worst case, we waste a frame, increasing power usage. + // + // Gio is expected to grow the ability to construct + // frame-to-frame differences and only render to changed + // areas. In that case, the waste of a spurious frame should + // be minimal. + h.hadEvents = h.hadEvents || len(events) > 0 + return events + } + return nil +} + +func (h *handlerEvents) Clear() { + for k := range h.handlers { + delete(h.handlers, k) + } +} + +func decodeProfileOp(d []byte, refs []interface{}) profile.Op { + if opconst.OpType(d[0]) != opconst.TypeProfile { + panic("invalid op") + } + return profile.Op{ + Tag: refs[0].(event.Tag), + } +} + +func decodeInvalidateOp(d []byte) op.InvalidateOp { + bo := binary.LittleEndian + if opconst.OpType(d[0]) != opconst.TypeInvalidate { + panic("invalid op") + } + var o op.InvalidateOp + if nanos := bo.Uint64(d[1:]); nanos > 0 { + o.At = time.Unix(0, int64(nanos)) + } + return o +} diff --git a/pkg/gel/gio/io/system/system.go b/pkg/gel/gio/io/system/system.go new file mode 100644 index 0000000..b646344 --- /dev/null +++ b/pkg/gel/gio/io/system/system.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package system contains events usually handled at the top-level +// program level. +package system + +import ( + "image" + "time" + + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +// A FrameEvent requests a new frame in the form of a list of +// operations that describes what to display and how to handle +// input. +type FrameEvent struct { + // Now is the current animation. Use Now instead of time.Now to + // synchronize animation and to avoid the time.Now call overhead. + Now time.Time + // Metric converts device independent dp and sp to device pixels. + Metric unit.Metric + // Size is the dimensions of the window. + Size image.Point + // Insets is the insets to apply. + Insets Insets + // Frame is the callback to supply the list of + // operations to complete the FrameEvent. + // + // Note that the operation list and the operations themselves + // may not be mutated until another FrameEvent is received from + // the same event source. + // That means that calls to frame.Reset and changes to referenced + // data such as ImageOp backing images should happen between + // receiving a FrameEvent and calling Frame. + // + // Example: + // + // var w *app.Window + // var frame *op.Ops + // for e := range w.Events() { + // if e, ok := e.(system.FrameEvent); ok { + // // Call frame.Reset and manipulate images for ImageOps + // // here. + // e.Frame(frame) + // } + // } + Frame func(frame *op.Ops) + // Queue supplies the events for event handlers. + Queue event.Queue +} + +// DestroyEvent is the last event sent through +// a window event channel. +type DestroyEvent struct { + // Err is nil for normal window closures. If a + // window is prematurely closed, Err is the cause. + Err error +} + +// Insets is the space taken up by +// system decoration such as translucent +// system bars and software keyboards. +type Insets struct { + Top, Bottom, Left, Right unit.Value +} + +// A StageEvent is generated whenever the stage of a +// Window changes. +type StageEvent struct { + Stage Stage +} + +// CommandEvent is a system event. Unlike most other events, CommandEvent is +// delivered as a pointer to allow Cancel to suppress it. +type CommandEvent struct { + Type CommandType + // Cancel suppress the default action of the command. + Cancel bool +} + +// Stage of a Window. +type Stage uint8 + +// CommandType is the type of a CommandEvent. +type CommandType uint8 + +const ( + // StagePaused is the Stage for inactive Windows. + // Inactive Windows don't receive FrameEvents. + StagePaused Stage = iota + // StateRunning is for active Windows. + StageRunning +) + +const ( + // CommandBack is the command for a back action + // such as the Android back button. + CommandBack CommandType = iota +) + +func (l Stage) String() string { + switch l { + case StagePaused: + return "StagePaused" + case StageRunning: + return "StageRunning" + default: + panic("unexpected Stage value") + } +} + +func (FrameEvent) ImplementsEvent() {} +func (StageEvent) ImplementsEvent() {} +func (*CommandEvent) ImplementsEvent() {} +func (DestroyEvent) ImplementsEvent() {} diff --git a/pkg/gel/gio/layout/alloc_test.go b/pkg/gel/gio/layout/alloc_test.go new file mode 100644 index 0000000..1b9486b --- /dev/null +++ b/pkg/gel/gio/layout/alloc_test.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build !race + +package layout + +import ( + "image" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestStackAllocs(t *testing.T) { + var ops op.Ops + allocs := testing.AllocsPerRun(1, func() { + ops.Reset() + gtx := Context{ + Ops: &ops, + } + Stack{}.Layout(gtx, + Stacked(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + }) + if allocs != 0 { + t.Errorf("expected no allocs, got %f", allocs) + } +} + +func TestFlexAllocs(t *testing.T) { + var ops op.Ops + allocs := testing.AllocsPerRun(1, func() { + ops.Reset() + gtx := Context{ + Ops: &ops, + } + Flex{}.Layout(gtx, + Rigid(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + }) + if allocs != 0 { + t.Errorf("expected no allocs, got %f", allocs) + } +} diff --git a/pkg/gel/gio/layout/context.go b/pkg/gel/gio/layout/context.go new file mode 100644 index 0000000..57f430f --- /dev/null +++ b/pkg/gel/gio/layout/context.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/system" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +// Context carries the state needed by almost all layouts and widgets. +// A zero value Context never returns events, map units to pixels +// with a scale of 1.0, and returns the zero time from Now. +type Context struct { + // Constraints track the constraints for the active widget or + // layout. + Constraints Constraints + + Metric unit.Metric + // By convention, a nil Queue is a signal to widgets to draw themselves + // in a disabled state. + Queue event.Queue + // Now is the animation time. + Now time.Time + + *op.Ops +} + +// NewContext is a shorthand for +// +// Context{ +// Ops: ops, +// Now: e.Now, +// Queue: e.Queue, +// Config: e.Config, +// Constraints: Exact(e.Size), +// } +// +// NewContext calls ops.Reset and adjusts ops for e.Insets. +func NewContext(ops *op.Ops, e system.FrameEvent) Context { + ops.Reset() + + size := e.Size + + if e.Insets != (system.Insets{}) { + left := e.Metric.Px(e.Insets.Left) + top := e.Metric.Px(e.Insets.Top) + op.Offset(f32.Point{ + X: float32(left), + Y: float32(top), + }).Add(ops) + + size.X -= left + e.Metric.Px(e.Insets.Right) + size.Y -= top + e.Metric.Px(e.Insets.Bottom) + } + + return Context{ + Ops: ops, + Now: e.Now, + Queue: e.Queue, + Metric: e.Metric, + Constraints: Exact(size), + } +} + +// Px maps the value to pixels. +func (c Context) Px(v unit.Value) int { + return c.Metric.Px(v) +} + +// Events returns the events available for the key. If no +// queue is configured, Events returns nil. +func (c Context) Events(k event.Tag) []event.Event { + if c.Queue == nil { + return nil + } + return c.Queue.Events(k) +} + +// Disabled returns a copy of this context with a nil Queue, +// blocking events to widgets using it. +// +// By convention, a nil Queue is a signal to widgets to draw themselves +// in a disabled state. +func (c Context) Disabled() Context { + c.Queue = nil + return c +} diff --git a/pkg/gel/gio/layout/doc.go b/pkg/gel/gio/layout/doc.go new file mode 100644 index 0000000..3824084 --- /dev/null +++ b/pkg/gel/gio/layout/doc.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package layout implements layouts common to GUI programs. + +Constraints and dimensions + +Constraints and dimensions form the interface between layouts and +interface child elements. This package operates on Widgets, functions +that compute Dimensions from a a set of constraints for acceptable +widths and heights. Both the constraints and dimensions are maintained +in an implicit Context to keep the Widget declaration short. + +For example, to add space above a widget: + + var gtx layout.Context + + // Configure a top inset. + inset := layout.Inset{Top: unit.Dp(8), ...} + // Use the inset to lay out a widget. + inset.Layout(gtx, func() { + // Lay out widget and determine its size given the constraints + // in gtx.Constraints. + ... + return layout.Dimensions{...} + }) + +Note that the example does not generate any garbage even though the +Inset is transient. Layouts that don't accept user input are designed +to not escape to the heap during their use. + +Layout operations are recursive: a child in a layout operation can +itself be another layout. That way, complex user interfaces can +be created from a few generic layouts. + +This example both aligns and insets a child: + + inset := layout.Inset{...} + inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + align := layout.Alignment(...) + return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return widget.Layout(gtx, ...) + }) + }) + +More complex layouts such as Stack and Flex lay out multiple children, +and stateful layouts such as List accept user input. + +*/ +package layout diff --git a/pkg/gel/gio/layout/example_test.go b/pkg/gel/gio/layout/example_test.go new file mode 100644 index 0000000..6176a12 --- /dev/null +++ b/pkg/gel/gio/layout/example_test.go @@ -0,0 +1,135 @@ +package layout_test + +import ( + "fmt" + "image" + + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +func ExampleInset() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Loose constraints with no minimal size. + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + // Inset all edges by 10. + inset := layout.UniformInset(unit.Dp(10)) + dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(gtx, 50, 50) + fmt.Println(dims.Size) + return dims + }) + + fmt.Println(dims.Size) + + // Output: + // (50,50) + // (70,70) +} + +func ExampleDirection() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + dims := layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + // Lay out a 50x50 sized widget. + dims := layoutWidget(gtx, 50, 50) + fmt.Println(dims.Size) + return dims + }) + + fmt.Println(dims.Size) + + // Output: + // (50,50) + // (100,100) +} + +func ExampleFlex() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + layout.Flex{WeightSum: 2}.Layout(gtx, + // Rigid 10x10 widget. + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + fmt.Printf("Rigid: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + // Child with 50% space allowance. + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + fmt.Printf("50%%: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + ) + + // Output: + // Rigid: {(0,100) (100,100)} + // 50%: {(45,100) (45,100)} +} + +func ExampleStack() { + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + layout.Stack{}.Layout(gtx, + // Force widget to the same size as the second. + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + fmt.Printf("Expand: %v\n", gtx.Constraints) + return layoutWidget(gtx, 10, 10) + }), + // Rigid 50x50 widget. + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layoutWidget(gtx, 50, 50) + }), + ) + + // Output: + // Expand: {(50,50) (100,100)} +} + +func ExampleList() { + gtx := layout.Context{ + Ops: new(op.Ops), + // Rigid constraints with both minimum and maximum set. + Constraints: layout.Exact(image.Point{X: 100, Y: 100}), + } + + // The list is 1e6 elements, but only 5 fit the constraints. + const listLen = 1e6 + + var list layout.List + list.Layout(gtx, listLen, func(gtx layout.Context, i int) layout.Dimensions { + return layoutWidget(gtx, 20, 20) + }) + + fmt.Println(list.Position.Count) + + // Output: + // 5 +} + +func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions { + return layout.Dimensions{ + Size: image.Point{ + X: width, + Y: height, + }, + } +} diff --git a/pkg/gel/gio/layout/flex.go b/pkg/gel/gio/layout/flex.go new file mode 100644 index 0000000..7b2804c --- /dev/null +++ b/pkg/gel/gio/layout/flex.go @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Flex lays out child elements along an axis, +// according to alignment and weights. +type Flex struct { + // Axis is the main axis, either Horizontal or Vertical. + Axis Axis + // Spacing controls the distribution of space left after + // layout. + Spacing Spacing + // Alignment is the alignment in the cross axis. + Alignment Alignment + // WeightSum is the sum of weights used for the weighted + // size of Flexed children. If WeightSum is zero, the sum + // of all Flexed weights is used. + WeightSum float32 +} + +// FlexChild is the descriptor for a Flex child. +type FlexChild struct { + flex bool + weight float32 + + widget Widget + + // Scratch space. + call op.CallOp + dims Dimensions +} + +// Spacing determine the spacing mode for a Flex. +type Spacing uint8 + +const ( + // SpaceEnd leaves space at the end. + SpaceEnd Spacing = iota + // SpaceStart leaves space at the start. + SpaceStart + // SpaceSides shares space between the start and end. + SpaceSides + // SpaceAround distributes space evenly between children, + // with half as much space at the start and end. + SpaceAround + // SpaceBetween distributes space evenly between children, + // leaving no space at the start and end. + SpaceBetween + // SpaceEvenly distributes space evenly between children and + // at the start and end. + SpaceEvenly +) + +// Rigid returns a Flex child with a maximal constraint of the +// remaining space. +func Rigid(widget Widget) FlexChild { + return FlexChild{ + widget: widget, + } +} + +// Flexed returns a Flex child forced to take up weight fraction of the +// space left over from Rigid children. The fraction is weight +// divided by either the weight sum of all Flexed children or the Flex +// WeightSum if non zero. +func Flexed(weight float32, widget Widget) FlexChild { + return FlexChild{ + flex: true, + weight: weight, + widget: widget, + } +} + +// Layout a list of children. The position of the children are +// determined by the specified order, but Rigid children are laid out +// before Flexed children. +func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions { + size := 0 + cs := gtx.Constraints + mainMin, mainMax := f.Axis.mainConstraint(cs) + crossMin, crossMax := f.Axis.crossConstraint(cs) + remaining := mainMax + var totalWeight float32 + cgtx := gtx + // Lay out Rigid children. + for i, child := range children { + if child.flex { + totalWeight += child.weight + continue + } + macro := op.Record(gtx.Ops) + cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax) + dims := child.widget(cgtx) + c := macro.Stop() + sz := f.Axis.Convert(dims.Size).X + size += sz + remaining -= sz + if remaining < 0 { + remaining = 0 + } + children[i].call = c + children[i].dims = dims + } + if w := f.WeightSum; w != 0 { + totalWeight = w + } + // fraction is the rounding error from a Flex weighting. + var fraction float32 + flexTotal := remaining + // Lay out Flexed children. + for i, child := range children { + if !child.flex { + continue + } + var flexSize int + if remaining > 0 && totalWeight > 0 { + // Apply weight and add any leftover fraction from a + // previous Flexed. + childSize := float32(flexTotal) * child.weight / totalWeight + flexSize = int(childSize + fraction + .5) + fraction = childSize - float32(flexSize) + if flexSize > remaining { + flexSize = remaining + } + } + macro := op.Record(gtx.Ops) + cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, crossMax) + dims := child.widget(cgtx) + c := macro.Stop() + sz := f.Axis.Convert(dims.Size).X + size += sz + remaining -= sz + if remaining < 0 { + remaining = 0 + } + children[i].call = c + children[i].dims = dims + } + var maxCross int + var maxBaseline int + for _, child := range children { + if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross { + maxCross = c + } + if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline { + maxBaseline = b + } + } + var space int + if mainMin > size { + space = mainMin - size + } + var mainSize int + switch f.Spacing { + case SpaceSides: + mainSize += space / 2 + case SpaceStart: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / (len(children) * 2) + } + } + for i, child := range children { + dims := child.dims + b := dims.Size.Y - dims.Baseline + var cross int + switch f.Alignment { + case End: + cross = maxCross - f.Axis.Convert(dims.Size).Y + case Middle: + cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2 + case Baseline: + if f.Axis == Horizontal { + cross = maxBaseline - b + } + } + stack := op.Save(gtx.Ops) + pt := f.Axis.Convert(image.Pt(mainSize, cross)) + op.Offset(FPt(pt)).Add(gtx.Ops) + child.call.Add(gtx.Ops) + stack.Load() + mainSize += f.Axis.Convert(dims.Size).X + if i < len(children)-1 { + switch f.Spacing { + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / len(children) + } + case SpaceBetween: + if len(children) > 1 { + mainSize += space / (len(children) - 1) + } + } + } + } + switch f.Spacing { + case SpaceSides: + mainSize += space / 2 + case SpaceEnd: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(children)) + case SpaceAround: + if len(children) > 0 { + mainSize += space / (len(children) * 2) + } + } + sz := f.Axis.Convert(image.Pt(mainSize, maxCross)) + return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline} +} + +func (s Spacing) String() string { + switch s { + case SpaceEnd: + return "SpaceEnd" + case SpaceStart: + return "SpaceStart" + case SpaceSides: + return "SpaceSides" + case SpaceAround: + return "SpaceAround" + case SpaceBetween: + return "SpaceAround" + case SpaceEvenly: + return "SpaceEvenly" + default: + panic("unreachable") + } +} diff --git a/pkg/gel/gio/layout/layout.go b/pkg/gel/gio/layout/layout.go new file mode 100644 index 0000000..45c8265 --- /dev/null +++ b/pkg/gel/gio/layout/layout.go @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +// Constraints represent the minimum and maximum size of a widget. +// +// A widget does not have to treat its constraints as "hard". For +// example, if it's passed a constraint with a minimum size that's +// smaller than its actual minimum size, it should return its minimum +// size dimensions instead. Parent widgets should deal appropriately +// with child widgets that return dimensions that do not fit their +// constraints (for example, by clipping). +type Constraints struct { + Min, Max image.Point +} + +// Dimensions are the resolved size and baseline for a widget. +// +// Baseline is the distance from the bottom of a widget to the baseline of +// any text it contains (or 0). The purpose is to be able to align text +// that span multiple widgets. +type Dimensions struct { + Size image.Point + Baseline int +} + +// Axis is the Horizontal or Vertical direction. +type Axis uint8 + +// Alignment is the mutual alignment of a list of widgets. +type Alignment uint8 + +// Direction is the alignment of widgets relative to a containing +// space. +type Direction uint8 + +// Widget is a function scope for drawing, processing events and +// computing dimensions for a user interface element. +type Widget func(gtx Context) Dimensions + +const ( + Start Alignment = iota + End + Middle + Baseline +) + +const ( + NW Direction = iota + N + NE + E + SE + S + SW + W + Center +) + +const ( + Horizontal Axis = iota + Vertical +) + +// Exact returns the Constraints with the minimum and maximum size +// set to size. +func Exact(size image.Point) Constraints { + return Constraints{ + Min: size, Max: size, + } +} + +// FPt converts an point to a f32.Point. +func FPt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} + +// FRect converts a rectangle to a f32.Rectangle. +func FRect(r image.Rectangle) f32.Rectangle { + return f32.Rectangle{ + Min: FPt(r.Min), Max: FPt(r.Max), + } +} + +// Constrain a size so each dimension is in the range [min;max]. +func (c Constraints) Constrain(size image.Point) image.Point { + if min := c.Min.X; size.X < min { + size.X = min + } + if min := c.Min.Y; size.Y < min { + size.Y = min + } + if max := c.Max.X; size.X > max { + size.X = max + } + if max := c.Max.Y; size.Y > max { + size.Y = max + } + return size +} + +// Inset adds space around a widget by decreasing its maximum +// constraints. The minimum constraints will be adjusted to ensure +// they do not exceed the maximum. +type Inset struct { + Top, Right, Bottom, Left unit.Value +} + +// Layout a widget. +func (in Inset) Layout(gtx Context, w Widget) Dimensions { + top := gtx.Px(in.Top) + right := gtx.Px(in.Right) + bottom := gtx.Px(in.Bottom) + left := gtx.Px(in.Left) + mcs := gtx.Constraints + mcs.Max.X -= left + right + if mcs.Max.X < 0 { + left = 0 + right = 0 + mcs.Max.X = 0 + } + if mcs.Min.X > mcs.Max.X { + mcs.Min.X = mcs.Max.X + } + mcs.Max.Y -= top + bottom + if mcs.Max.Y < 0 { + bottom = 0 + top = 0 + mcs.Max.Y = 0 + } + if mcs.Min.Y > mcs.Max.Y { + mcs.Min.Y = mcs.Max.Y + } + stack := op.Save(gtx.Ops) + op.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops) + gtx.Constraints = mcs + dims := w(gtx) + stack.Load() + return Dimensions{ + Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}), + Baseline: dims.Baseline + bottom, + } +} + +// UniformInset returns an Inset with a single inset applied to all +// edges. +func UniformInset(v unit.Value) Inset { + return Inset{Top: v, Right: v, Bottom: v, Left: v} +} + +// Layout a widget according to the direction. +// The widget is called with the context constraints minimum cleared. +func (d Direction) Layout(gtx Context, w Widget) Dimensions { + macro := op.Record(gtx.Ops) + cs := gtx.Constraints + gtx.Constraints.Min = image.Point{} + dims := w(gtx) + call := macro.Stop() + sz := dims.Size + if sz.X < cs.Min.X { + sz.X = cs.Min.X + } + if sz.Y < cs.Min.Y { + sz.Y = cs.Min.Y + } + + defer op.Save(gtx.Ops).Load() + p := d.Position(dims.Size, sz) + op.Offset(FPt(p)).Add(gtx.Ops) + call.Add(gtx.Ops) + + return Dimensions{ + Size: sz, + Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y, + } +} + +// Position calculates widget position according to the direction. +func (d Direction) Position(widget, bounds image.Point) image.Point { + var p image.Point + + switch d { + case N, S, Center: + p.X = (bounds.X - widget.X) / 2 + case NE, SE, E: + p.X = bounds.X - widget.X + } + + switch d { + case W, Center, E: + p.Y = (bounds.Y - widget.Y) / 2 + case SW, S, SE: + p.Y = bounds.Y - widget.Y + } + + return p +} + +// Spacer adds space between widgets. +type Spacer struct { + Width, Height unit.Value +} + +func (s Spacer) Layout(gtx Context) Dimensions { + return Dimensions{ + Size: image.Point{ + X: gtx.Px(s.Width), + Y: gtx.Px(s.Height), + }, + } +} + +func (a Alignment) String() string { + switch a { + case Start: + return "Start" + case End: + return "End" + case Middle: + return "Middle" + case Baseline: + return "Baseline" + default: + panic("unreachable") + } +} + +// Convert a point in (x, y) coordinates to (main, cross) coordinates, +// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged +// for the horizontal axis, or (y, x) for the vertical axis. +func (a Axis) Convert(pt image.Point) image.Point { + if a == Horizontal { + return pt + } + return image.Pt(pt.Y, pt.X) +} + +// mainConstraint returns the min and max main constraints for axis a. +func (a Axis) mainConstraint(cs Constraints) (int, int) { + if a == Horizontal { + return cs.Min.X, cs.Max.X + } + return cs.Min.Y, cs.Max.Y +} + +// crossConstraint returns the min and max cross constraints for axis a. +func (a Axis) crossConstraint(cs Constraints) (int, int) { + if a == Horizontal { + return cs.Min.Y, cs.Max.Y + } + return cs.Min.X, cs.Max.X +} + +// constraints returns the constraints for axis a. +func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints { + if a == Horizontal { + return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)} + } + return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)} +} + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("unreachable") + } +} + +func (d Direction) String() string { + switch d { + case NW: + return "NW" + case N: + return "N" + case NE: + return "NE" + case E: + return "E" + case SE: + return "SE" + case S: + return "S" + case SW: + return "SW" + case W: + return "W" + case Center: + return "Center" + default: + panic("unreachable") + } +} diff --git a/pkg/gel/gio/layout/layout_test.go b/pkg/gel/gio/layout/layout_test.go new file mode 100644 index 0000000..c444da8 --- /dev/null +++ b/pkg/gel/gio/layout/layout_test.go @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestStack(t *testing.T) { + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(100, 100), + }, + } + exp := image.Point{X: 60, Y: 70} + dims := Stack{Alignment: Center}.Layout(gtx, + Expanded(func(gtx Context) Dimensions { + return Dimensions{Size: exp} + }), + Stacked(func(gtx Context) Dimensions { + return Dimensions{Size: image.Point{X: 50, Y: 50}} + }), + ) + if got := dims.Size; got != exp { + t.Errorf("Stack ignored Expanded size, got %v expected %v", got, exp) + } +} diff --git a/pkg/gel/gio/layout/list.go b/pkg/gel/gio/layout/list.go new file mode 100644 index 0000000..9a6c4c1 --- /dev/null +++ b/pkg/gel/gio/layout/list.go @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/gesture" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" +) + +type scrollChild struct { + size image.Point + call op.CallOp +} + +// List displays a subsection of a potentially infinitely +// large underlying list. List accepts user input to scroll +// the subsection. +type List struct { + Axis Axis + // ScrollToEnd instructs the list to stay scrolled to the far end position + // once reached. A List with ScrollToEnd == true and Position.BeforeEnd == + // false draws its content with the last item at the bottom of the list + // area. + ScrollToEnd bool + // Alignment is the cross axis alignment of list elements. + Alignment Alignment + + cs Constraints + scroll gesture.Scroll + scrollDelta int + + // Position is updated during Layout. To save the list scroll position, + // just save Position after Layout finishes. To scroll the list + // programmatically, update Position (e.g. restore it from a saved value) + // before calling Layout. + Position Position + + len int + + // maxSize is the total size of visible children. + maxSize int + children []scrollChild + dir iterationDir +} + +// ListElement is a function that computes the dimensions of +// a list element. +type ListElement func(gtx Context, index int) Dimensions + +type iterationDir uint8 + +// Position is a List scroll offset represented as an offset from the top edge +// of a child element. +type Position struct { + // BeforeEnd tracks whether the List position is before the very end. We + // use "before end" instead of "at end" so that the zero value of a + // Position struct is useful. + // + // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, + // then First and Offset are ignored, and the list is drawn with the last + // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored. + BeforeEnd bool + // First is the index of the first visible child. + First int + // Offset is the distance in pixels from the top edge to the child at index + // First. + Offset int + // OffsetLast is the signed distance in pixels from the bottom edge to the + // bottom edge of the child at index First+Count. + OffsetLast int + // Count is the number of visible children. + Count int +} + +const ( + iterateNone iterationDir = iota + iterateForward + iterateBackward +) + +const inf = 1e6 + +// init prepares the list for iterating through its children with next. +func (l *List) init(gtx Context, len int) { + if l.more() { + panic("unfinished child") + } + l.cs = gtx.Constraints + l.maxSize = 0 + l.children = l.children[:0] + l.len = len + l.update(gtx) + if l.scrollToEnd() || l.Position.First > len { + l.Position.Offset = 0 + l.Position.First = len + } +} + +// Layout the List. +func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions { + l.init(gtx, len) + crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints) + gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax) + macro := op.Record(gtx.Ops) + for l.next(); l.more(); l.next() { + child := op.Record(gtx.Ops) + dims := w(gtx, l.index()) + call := child.Stop() + l.end(dims, call) + } + return l.layout(gtx.Ops, macro) +} + +func (l *List) scrollToEnd() bool { + return l.ScrollToEnd && !l.Position.BeforeEnd +} + +// Dragging reports whether the List is being dragged. +func (l *List) Dragging() bool { + return l.scroll.State() == gesture.StateDragging +} + +func (l *List) update(gtx Context) { + d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis)) + l.scrollDelta = d + l.Position.Offset += d +} + +// next advances to the next child. +func (l *List) next() { + l.dir = l.nextDir() + // The user scroll offset is applied after scrolling to + // list end. + if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { + l.Position.BeforeEnd = true + l.Position.Offset += l.scrollDelta + l.dir = l.nextDir() + } +} + +// index is current child's position in the underlying list. +func (l *List) index() int { + switch l.dir { + case iterateBackward: + return l.Position.First - 1 + case iterateForward: + return l.Position.First + len(l.children) + default: + panic("Index called before Next") + } +} + +// more reports whether more children are needed. +func (l *List) more() bool { + return l.dir != iterateNone +} + +func (l *List) nextDir() iterationDir { + _, vsize := l.Axis.mainConstraint(l.cs) + last := l.Position.First + len(l.children) + // Clamp offset. + if l.maxSize-l.Position.Offset < vsize && last == l.len { + l.Position.Offset = l.maxSize - vsize + } + if l.Position.Offset < 0 && l.Position.First == 0 { + l.Position.Offset = 0 + } + switch { + case len(l.children) == l.len: + return iterateNone + case l.maxSize-l.Position.Offset < vsize: + return iterateForward + case l.Position.Offset < 0: + return iterateBackward + } + return iterateNone +} + +// End the current child by specifying its dimensions. +func (l *List) end(dims Dimensions, call op.CallOp) { + child := scrollChild{dims.Size, call} + mainSize := l.Axis.Convert(child.size).X + l.maxSize += mainSize + switch l.dir { + case iterateForward: + l.children = append(l.children, child) + case iterateBackward: + l.children = append(l.children, scrollChild{}) + copy(l.children[1:], l.children) + l.children[0] = child + l.Position.First-- + l.Position.Offset += mainSize + default: + panic("call Next before End") + } + l.dir = iterateNone +} + +// Layout the List and return its dimensions. +func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { + if l.more() { + panic("unfinished child") + } + mainMin, mainMax := l.Axis.mainConstraint(l.cs) + children := l.children + // Skip invisible children + for len(children) > 0 { + sz := children[0].size + mainSize := l.Axis.Convert(sz).X + if l.Position.Offset < mainSize { + // First child is partially visible. + break + } + l.Position.First++ + l.Position.Offset -= mainSize + children = children[1:] + } + size := -l.Position.Offset + var maxCross int + for i, child := range children { + sz := l.Axis.Convert(child.size) + if c := sz.Y; c > maxCross { + maxCross = c + } + size += sz.X + if size >= mainMax { + children = children[:i+1] + break + } + } + l.Position.Count = len(children) + l.Position.OffsetLast = mainMax - size + pos := -l.Position.Offset + // ScrollToEnd lists are end aligned. + if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { + pos += space + } + for _, child := range children { + sz := l.Axis.Convert(child.size) + var cross int + switch l.Alignment { + case End: + cross = maxCross - sz.Y + case Middle: + cross = (maxCross - sz.Y) / 2 + } + childSize := sz.X + max := childSize + pos + if max > mainMax { + max = mainMax + } + min := pos + if min < 0 { + min = 0 + } + r := image.Rectangle{ + Min: l.Axis.Convert(image.Pt(min, -inf)), + Max: l.Axis.Convert(image.Pt(max, inf)), + } + stack := op.Save(ops) + clip.Rect(r).Add(ops) + pt := l.Axis.Convert(image.Pt(pos, cross)) + op.Offset(FPt(pt)).Add(ops) + child.call.Add(ops) + stack.Load() + pos += childSize + } + atStart := l.Position.First == 0 && l.Position.Offset <= 0 + atEnd := l.Position.First+len(children) == l.len && mainMax >= pos + if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { + l.scroll.Stop() + } + l.Position.BeforeEnd = !atEnd + if pos < mainMin { + pos = mainMin + } + if pos > mainMax { + pos = mainMax + } + dims := l.Axis.Convert(image.Pt(pos, maxCross)) + call := macro.Stop() + defer op.Save(ops).Load() + pointer.Rect(image.Rectangle{Max: dims}).Add(ops) + + var min, max int + if o := l.Position.Offset; o > 0 { + // Use the size of the invisible part as scroll boundary. + min = -o + } else if l.Position.First > 0 { + min = -inf + } + if o := l.Position.OffsetLast; o < 0 { + max = -o + } else if l.Position.First+l.Position.Count < l.len { + max = inf + } + scrollRange := image.Rectangle{ + Min: l.Axis.Convert(image.Pt(min, 0)), + Max: l.Axis.Convert(image.Pt(max, 0)), + } + l.scroll.Add(ops, scrollRange) + + call.Add(ops) + return Dimensions{Size: dims} +} diff --git a/pkg/gel/gio/layout/list_test.go b/pkg/gel/gio/layout/list_test.go new file mode 100644 index 0000000..5e8a52c --- /dev/null +++ b/pkg/gel/gio/layout/list_test.go @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/io/router" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestListPosition(t *testing.T) { + _s := func(e ...event.Event) []event.Event { return e } + r := new(router.Router) + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(20, 10), + }, + Queue: r, + } + el := func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + } + for _, tc := range []struct { + label string + num int + scroll []event.Event + first int + count int + offset int + last int + }{ + {label: "no item", last: 20}, + {label: "1 visible 0 hidden", num: 1, count: 1, last: 10}, + {label: "2 visible 0 hidden", num: 2, count: 2}, + {label: "2 visible 1 hidden", num: 3, count: 2}, + {label: "3 visible 0 hidden small scroll", num: 3, count: 3, offset: 5, last: -5, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(5, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "3 visible 0 hidden small scroll 2", num: 3, count: 3, offset: 3, last: -7, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(3, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(10, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(15, 0), + }, + )}, + } { + t.Run(tc.label, func(t *testing.T) { + gtx.Ops.Reset() + + var list List + // Initialize the list. + list.Layout(gtx, tc.num, el) + // Generate the scroll events. + r.Frame(gtx.Ops) + r.Queue(tc.scroll...) + // Let the list process the events. + list.Layout(gtx, tc.num, el) + + pos := list.Position + if got, want := pos.First, tc.first; got != want { + t.Errorf("List: invalid first position: got %v; want %v", got, want) + } + if got, want := pos.Count, tc.count; got != want { + t.Errorf("List: invalid number of visible children: got %v; want %v", got, want) + } + if got, want := pos.Offset, tc.offset; got != want { + t.Errorf("List: invalid first visible offset: got %v; want %v", got, want) + } + if got, want := pos.OffsetLast, tc.last; got != want { + t.Errorf("List: invalid last visible offset: got %v; want %v", got, want) + } + }) + } +} diff --git a/pkg/gel/gio/layout/stack.go b/pkg/gel/gio/layout/stack.go new file mode 100644 index 0000000..e657ca1 --- /dev/null +++ b/pkg/gel/gio/layout/stack.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Stack lays out child elements on top of each other, +// according to an alignment direction. +type Stack struct { + // Alignment is the direction to align children + // smaller than the available space. + Alignment Direction +} + +// StackChild represents a child for a Stack layout. +type StackChild struct { + expanded bool + widget Widget + + // Scratch space. + call op.CallOp + dims Dimensions +} + +// Stacked returns a Stack child that is laid out with no minimum +// constraints and the maximum constraints passed to Stack.Layout. +func Stacked(w Widget) StackChild { + return StackChild{ + widget: w, + } +} + +// Expanded returns a Stack child with the minimum constraints set +// to the largest Stacked child. The maximum constraints are set to +// the same as passed to Stack.Layout. +func Expanded(w Widget) StackChild { + return StackChild{ + expanded: true, + widget: w, + } +} + +// Layout a stack of children. The position of the children are +// determined by the specified order, but Stacked children are laid out +// before Expanded children. +func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions { + var maxSZ image.Point + // First lay out Stacked children. + cgtx := gtx + cgtx.Constraints.Min = image.Point{} + for i, w := range children { + if w.expanded { + continue + } + macro := op.Record(gtx.Ops) + dims := w.widget(cgtx) + call := macro.Stop() + if w := dims.Size.X; w > maxSZ.X { + maxSZ.X = w + } + if h := dims.Size.Y; h > maxSZ.Y { + maxSZ.Y = h + } + children[i].call = call + children[i].dims = dims + } + // Then lay out Expanded children. + for i, w := range children { + if !w.expanded { + continue + } + macro := op.Record(gtx.Ops) + cgtx.Constraints.Min = maxSZ + dims := w.widget(cgtx) + call := macro.Stop() + if w := dims.Size.X; w > maxSZ.X { + maxSZ.X = w + } + if h := dims.Size.Y; h > maxSZ.Y { + maxSZ.Y = h + } + children[i].call = call + children[i].dims = dims + } + + maxSZ = gtx.Constraints.Constrain(maxSZ) + var baseline int + for _, ch := range children { + sz := ch.dims.Size + var p image.Point + switch s.Alignment { + case N, S, Center: + p.X = (maxSZ.X - sz.X) / 2 + case NE, SE, E: + p.X = maxSZ.X - sz.X + } + switch s.Alignment { + case W, Center, E: + p.Y = (maxSZ.Y - sz.Y) / 2 + case SW, S, SE: + p.Y = maxSZ.Y - sz.Y + } + stack := op.Save(gtx.Ops) + op.Offset(FPt(p)).Add(gtx.Ops) + ch.call.Add(gtx.Ops) + stack.Load() + if baseline == 0 { + if b := ch.dims.Baseline; b != 0 { + baseline = b + maxSZ.Y - sz.Y - p.Y + } + } + } + return Dimensions{ + Size: maxSZ, + Baseline: baseline, + } +} diff --git a/pkg/gel/gio/op/clip/clip.go b/pkg/gel/gio/op/clip/clip.go new file mode 100644 index 0000000..c9d8377 --- /dev/null +++ b/pkg/gel/gio/op/clip/clip.go @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "encoding/binary" + "image" + "math" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/internal/ops" + "github.com/p9c/p9/pkg/gel/gio/internal/scene" + "github.com/p9c/p9/pkg/gel/gio/internal/stroke" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Op represents a clip area. Op intersects the current clip area with +// itself. +type Op struct { + bounds image.Rectangle + path PathSpec + + outline bool + stroke StrokeStyle + dashes DashSpec +} + +func (p Op) Add(o *op.Ops) { + str := p.stroke + dashes := p.dashes + path := p.path + outline := p.outline + approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap) + if approx { + // If the stroke is not natively supported by the compute renderer, construct a filled path + // that approximates it. + path = p.approximateStroke(o) + dashes = DashSpec{} + str = StrokeStyle{} + outline = true + } + + if path.hasSegments { + data := o.Write(opconst.TypePathLen) + data[0] = byte(opconst.TypePath) + path.spec.Add(o) + } + + if str.Width > 0 { + data := o.Write(opconst.TypeStrokeLen) + data[0] = byte(opconst.TypeStroke) + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(str.Width)) + } + + data := o.Write(opconst.TypeClipLen) + data[0] = byte(opconst.TypeClip) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(p.bounds.Min.X)) + bo.PutUint32(data[5:], uint32(p.bounds.Min.Y)) + bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) + bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) + if outline { + data[17] = byte(1) + } +} + +func (p Op) approximateStroke(o *op.Ops) PathSpec { + if !p.path.hasSegments { + return PathSpec{} + } + + var r ops.Reader + // Add path op for us to decode. Use a macro to omit it from later decodes. + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.path.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt path data") + } + pathData := encOp.Data[opconst.TypeAuxLen:] + + // Decode dashes in a similar way. + var dashes stroke.DashOp + if p.dashes.phase != 0 || p.dashes.size > 0 { + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.dashes.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt dash data") + } + dashes.Dashes = make([]float32, p.dashes.size) + dashData := encOp.Data[opconst.TypeAuxLen:] + bo := binary.LittleEndian + for i := range dashes.Dashes { + dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:])) + } + dashes.Phase = p.dashes.phase + } + + // Approximate and output path data. + var outline Path + outline.Begin(o) + ss := stroke.StrokeStyle{ + Width: p.stroke.Width, + Miter: p.stroke.Miter, + Cap: stroke.StrokeCap(p.stroke.Cap), + Join: stroke.StrokeJoin(p.stroke.Join), + } + quads := stroke.StrokePathCommands(ss, dashes, pathData) + pen := f32.Pt(0, 0) + for _, quad := range quads { + q := quad.Quad + if q.From != pen { + pen = q.From + outline.MoveTo(pen) + } + outline.contour = int(quad.Contour) + outline.QuadTo(q.Ctrl, q.To) + } + return outline.End() +} + +type PathSpec struct { + spec op.CallOp + // open is true if any path contour is not closed. A closed contour starts + // and ends in the same point. + open bool + // hasSegments tracks whether there are any segments in the path. + hasSegments bool +} + +// Path constructs a Op clip path described by lines and +// Bézier curves, where drawing outside the Path is discarded. +// The inside-ness of a pixel is determines by the non-zero winding rule, +// similar to the SVG rule of the same name. +// +// Path generates no garbage and can be used for dynamic paths; path +// data is stored directly in the Ops list supplied to Begin. +type Path struct { + ops *op.Ops + open bool + contour int + pen f32.Point + macro op.MacroOp + start f32.Point + hasSegments bool +} + +// Pos returns the current pen position. +func (p *Path) Pos() f32.Point { return p.pen } + +// Begin the path, storing the path data and final Op into ops. +func (p *Path) Begin(ops *op.Ops) { + p.ops = ops + p.macro = op.Record(ops) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) + data[0] = byte(opconst.TypeAux) +} + +// End returns a PathSpec ready to use in clipping operations. +func (p *Path) End() PathSpec { + c := p.macro.Stop() + return PathSpec{ + spec: c, + open: p.open || p.pen != p.start, + hasSegments: p.hasSegments, + } +} + +// Move moves the pen by the amount specified by delta. +func (p *Path) Move(delta f32.Point) { + to := delta.Add(p.pen) + p.MoveTo(to) +} + +// MoveTo moves the pen to the specified absolute coordinate. +func (p *Path) MoveTo(to f32.Point) { + p.open = p.open || p.pen != p.start + p.end() + p.pen = to + p.start = to +} + +// end completes the current contour. +func (p *Path) end() { + p.contour++ +} + +// Line moves the pen by the amount specified by delta, recording a line. +func (p *Path) Line(delta f32.Point) { + to := delta.Add(p.pen) + p.LineTo(to) +} + +// LineTo moves the pen to the absolute point specified, recording a line. +func (p *Path) LineTo(to f32.Point) { + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Line(p.pen, to)) + p.pen = to + p.hasSegments = true +} + +// Quad records a quadratic Bézier from the pen to end +// with the control point ctrl. +func (p *Path) Quad(ctrl, to f32.Point) { + ctrl = ctrl.Add(p.pen) + to = to.Add(p.pen) + p.QuadTo(ctrl, to) +} + +// QuadTo records a quadratic Bézier from the pen to end +// with the control point ctrl, with absolute coordinates. +func (p *Path) QuadTo(ctrl, to f32.Point) { + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to)) + p.pen = to + p.hasSegments = true +} + +// Arc adds an elliptical arc to the path. The implied ellipse is defined +// by its focus points f1 and f2. +// The arc starts in the current point and ends angle radians along the ellipse boundary. +// The sign of angle determines the direction; positive being counter-clockwise, +// negative clockwise. +func (p *Path) Arc(f1, f2 f32.Point, angle float32) { + f1 = f1.Add(p.pen) + f2 = f2.Add(p.pen) + const segments = 16 + m := stroke.ArcTransform(p.pen, f1, f2, angle, segments) + + for i := 0; i < segments; i++ { + p0 := p.pen + p1 := m.Transform(p0) + p2 := m.Transform(p1) + ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) + p.QuadTo(ctl, p2) + } +} + +// Cube records a cubic Bézier from the pen through +// two control points ending in to. +func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) { + p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to)) +} + +// CubeTo records a cubic Bézier from the pen through +// two control points ending in to, with absolute coordinates. +func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) { + if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen { + return + } + data := p.ops.Write(scene.CommandSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) + p.pen = to + p.hasSegments = true +} + +// Close closes the path contour. +func (p *Path) Close() { + if p.pen != p.start { + p.LineTo(p.start) + } + p.end() +} + +// Outline represents the area inside of a path, according to the +// non-zero winding rule. +type Outline struct { + Path PathSpec +} + +// Op returns a clip operation representing the outline. +func (o Outline) Op() Op { + if o.Path.open { + panic("not all path contours are closed") + } + return Op{ + path: o.Path, + outline: true, + } +} diff --git a/pkg/gel/gio/op/clip/clip_test.go b/pkg/gel/gio/op/clip/clip_test.go new file mode 100644 index 0000000..8124ed6 --- /dev/null +++ b/pkg/gel/gio/op/clip/clip_test.go @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "testing" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestOpenPathOutlinePanic(t *testing.T) { + defer func() { + if err := recover(); err == nil { + t.Error("Outline of an open path didn't panic") + } + }() + var p Path + p.Begin(new(op.Ops)) + p.Line(f32.Pt(10, 10)) + Outline{Path: p.End()}.Op() +} diff --git a/pkg/gel/gio/op/clip/doc.go b/pkg/gel/gio/op/clip/doc.go new file mode 100644 index 0000000..6ba5546 --- /dev/null +++ b/pkg/gel/gio/op/clip/doc.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package clip provides operations for clipping paint operations. +Drawing outside the current clip area is ignored. + +The current clip is initially the infinite set. An Op sets the clip +to the intersection of the current clip and the clip area it +represents. If you need to reset the current clip to its value +before applying an Op, use op.StackOp. + +General clipping areas are constructed with Path. Simpler special +cases such as rectangular clip areas also exist as convenient +constructors. +*/ +package clip diff --git a/pkg/gel/gio/op/clip/shapes.go b/pkg/gel/gio/op/clip/shapes.go new file mode 100644 index 0000000..ee14bf3 --- /dev/null +++ b/pkg/gel/gio/op/clip/shapes.go @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "image" + "math" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Rect represents the clip area of a pixel-aligned rectangle. +type Rect image.Rectangle + +// Op returns the op for the rectangle. +func (r Rect) Op() Op { + return Op{ + bounds: image.Rectangle(r), + outline: true, + } +} + +// Add the clip operation. +func (r Rect) Add(ops *op.Ops) { + r.Op().Add(ops) +} + +// UniformRRect returns an RRect with all corner radii set to the +// provided radius. +func UniformRRect(rect f32.Rectangle, radius float32) RRect { + return RRect{ + Rect: rect, + SE: radius, + SW: radius, + NE: radius, + NW: radius, + } +} + +// RRect represents the clip area of a rectangle with rounded +// corners. +// +// Specify a square with corner radii equal to half the square size to +// construct a circular clip area. +type RRect struct { + Rect f32.Rectangle + // The corner radii. + SE, SW, NW, NE float32 +} + +// Op returns the op for the rounded rectangle. +func (rr RRect) Op(ops *op.Ops) Op { + if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 { + r := image.Rectangle{ + Min: image.Point{X: int(rr.Rect.Min.X), Y: int(rr.Rect.Min.Y)}, + Max: image.Point{X: int(rr.Rect.Max.X), Y: int(rr.Rect.Max.Y)}, + } + // Only use Rect if rr is pixel-aligned, as Rect is guaranteed to be. + if fPt(r.Min) == rr.Rect.Min && fPt(r.Max) == rr.Rect.Max { + return Rect(r).Op() + } + } + return Outline{Path: rr.Path(ops)}.Op() +} + +// Add the rectangle clip. +func (rr RRect) Add(ops *op.Ops) { + rr.Op(ops).Add(ops) +} + +// Path returns the PathSpec for the rounded rectangle. +func (rr RRect) Path(ops *op.Ops) PathSpec { + var p Path + p.Begin(ops) + + // https://pomax.github.io/bezierinfo/#circles_cubic. + const q = 4 * (math.Sqrt2 - 1) / 3 + const iq = 1 - q + + se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE + w, n, e, s := rr.Rect.Min.X, rr.Rect.Min.Y, rr.Rect.Max.X, rr.Rect.Max.Y + + p.MoveTo(f32.Point{X: w + nw, Y: n}) + p.LineTo(f32.Point{X: e - ne, Y: n}) // N + p.CubeTo( // NE + f32.Point{X: e - ne*iq, Y: n}, + f32.Point{X: e, Y: n + ne*iq}, + f32.Point{X: e, Y: n + ne}) + p.LineTo(f32.Point{X: e, Y: s - se}) // E + p.CubeTo( // SE + f32.Point{X: e, Y: s - se*iq}, + f32.Point{X: e - se*iq, Y: s}, + f32.Point{X: e - se, Y: s}) + p.LineTo(f32.Point{X: w + sw, Y: s}) // S + p.CubeTo( // SW + f32.Point{X: w + sw*iq, Y: s}, + f32.Point{X: w, Y: s - sw*iq}, + f32.Point{X: w, Y: s - sw}) + p.LineTo(f32.Point{X: w, Y: n + nw}) // W + p.CubeTo( // NW + f32.Point{X: w, Y: n + nw*iq}, + f32.Point{X: w + nw*iq, Y: n}, + f32.Point{X: w + nw, Y: n}) + + return p.End() +} + +// Circle represents the clip area of a circle. +type Circle struct { + Center f32.Point + Radius float32 +} + +// Op returns the op for the circle. +func (c Circle) Op(ops *op.Ops) Op { + return Outline{Path: c.Path(ops)}.Op() +} + +// Add the circle clip. +func (c Circle) Add(ops *op.Ops) { + c.Op(ops).Add(ops) +} + +// Path returns the PathSpec for the circle. +func (c Circle) Path(ops *op.Ops) PathSpec { + var p Path + p.Begin(ops) + + center := c.Center + r := c.Radius + + // https://pomax.github.io/bezierinfo/#circles_cubic. + const q = 4 * (math.Sqrt2 - 1) / 3 + + curve := r * q + top := f32.Point{X: center.X, Y: center.Y - r} + + p.MoveTo(top) + p.CubeTo( + f32.Point{X: center.X + curve, Y: center.Y - r}, + f32.Point{X: center.X + r, Y: center.Y - curve}, + f32.Point{X: center.X + r, Y: center.Y}, + ) + p.CubeTo( + f32.Point{X: center.X + r, Y: center.Y + curve}, + f32.Point{X: center.X + curve, Y: center.Y + r}, + f32.Point{X: center.X, Y: center.Y + r}, + ) + p.CubeTo( + f32.Point{X: center.X - curve, Y: center.Y + r}, + f32.Point{X: center.X - r, Y: center.Y + curve}, + f32.Point{X: center.X - r, Y: center.Y}, + ) + p.CubeTo( + f32.Point{X: center.X - r, Y: center.Y - curve}, + f32.Point{X: center.X - curve, Y: center.Y - r}, + top, + ) + return p.End() +} + +func fPt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} diff --git a/pkg/gel/gio/op/clip/stroke.go b/pkg/gel/gio/op/clip/stroke.go new file mode 100644 index 0000000..c90c65d --- /dev/null +++ b/pkg/gel/gio/op/clip/stroke.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package clip + +import ( + "encoding/binary" + "math" + + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Stroke represents a stroked path. +type Stroke struct { + Path PathSpec + Style StrokeStyle + + // Dashes specify the dashes of the stroke. + // The empty value denotes no dashes. + Dashes DashSpec +} + +// Op returns a clip operation representing the stroke. +func (s Stroke) Op() Op { + return Op{ + path: s.Path, + stroke: s.Style, + dashes: s.Dashes, + } +} + +// StrokeStyle describes how a path should be stroked. +type StrokeStyle struct { + Width float32 // Width of the stroked path. + + // Miter is the limit to apply to a miter joint. + // The zero Miter disables the miter joint; setting Miter to +∞ + // unconditionally enables the miter joint. + Miter float32 + Cap StrokeCap // Cap describes the head or tail of a stroked path. + Join StrokeJoin // Join describes how stroked paths are collated. +} + +// StrokeCap describes the head or tail of a stroked path. +type StrokeCap uint8 + +const ( + // RoundCap caps stroked paths with a round cap, joining the right-hand and + // left-hand sides of a stroked path with a half disc of diameter the + // stroked path's width. + RoundCap StrokeCap = iota + + // FlatCap caps stroked paths with a flat cap, joining the right-hand + // and left-hand sides of a stroked path with a straight line. + FlatCap + + // SquareCap caps stroked paths with a square cap, joining the right-hand + // and left-hand sides of a stroked path with a half square of length + // the stroked path's width. + SquareCap +) + +// StrokeJoin describes how stroked paths are collated. +type StrokeJoin uint8 + +const ( + // RoundJoin joins path segments with a round segment. + RoundJoin StrokeJoin = iota + + // BevelJoin joins path segments with sharp bevels. + BevelJoin +) + +// Dash records dashes' lengths and phase for a stroked path. +type Dash struct { + ops *op.Ops + macro op.MacroOp + phase float32 + size uint8 // size of the pattern +} + +func (d *Dash) Begin(ops *op.Ops) { + d.ops = ops + d.macro = op.Record(ops) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) + data[0] = byte(opconst.TypeAux) +} + +func (d *Dash) Phase(v float32) { + d.phase = v +} + +func (d *Dash) Dash(length float32) { + if d.size == math.MaxUint8 { + panic("clip: dash pattern too large") + } + data := d.ops.Write(4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], math.Float32bits(length)) + d.size++ +} + +func (d *Dash) End() DashSpec { + c := d.macro.Stop() + return DashSpec{ + spec: c, + phase: d.phase, + size: d.size, + } +} + +// DashSpec describes a dashed pattern. +type DashSpec struct { + spec op.CallOp + phase float32 + size uint8 // size of the pattern +} diff --git a/pkg/gel/gio/op/op.go b/pkg/gel/gio/op/op.go new file mode 100644 index 0000000..4cf03ea --- /dev/null +++ b/pkg/gel/gio/op/op.go @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* + +Package op implements operations for updating a user interface. + +Gio programs use operations, or ops, for describing their user +interfaces. There are operations for drawing, defining input +handlers, changing window properties as well as operations for +controlling the execution of other operations. + +Ops represents a list of operations. The most important use +for an Ops list is to describe a complete user interface update +to a ui/app.Window's Update method. + +Drawing a colored square: + + import "github.com/p9c/p9/pkg/gel/gio/unit" + import "github.com/p9c/p9/pkg/gel/gio/app" + import "github.com/p9c/p9/pkg/gel/gio/op/paint" + + var w app.Window + var e system.FrameEvent + ops := new(op.Ops) + ... + ops.Reset() + paint.ColorOp{Color: ...}.Add(ops) + paint.PaintOp{Rect: ...}.Add(ops) + e.Frame(ops) + +State + +An Ops list can be viewed as a very simple virtual machine: it has an implicit +mutable state stack and execution flow can be controlled with macros. + +The Save function saves the current state for later restoring: + + ops := new(op.Ops) + // Save the current state, in particular the transform. + state := op.Save(ops) + // Apply a transform to subsequent operations. + op.Offset(...).Add(ops) + ... + // Restore the previous transform. + state.Load() + +You can also use this one-line to save the current state and restore it at the +end of a function : + + defer op.Save(ops).Load() + +The MacroOp records a list of operations to be executed later: + + ops := new(op.Ops) + macro := op.Record(ops) + // Record operations by adding them. + op.InvalidateOp{}.Add(ops) + ... + // End recording. + call := macro.Stop() + + // replay the recorded operations: + call.Add(ops) + +*/ +package op + +import ( + "encoding/binary" + "math" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" +) + +// Ops holds a list of operations. Operations are stored in +// serialized form to avoid garbage during construction of +// the ops list. +type Ops struct { + // version is incremented at each Reset. + version int + // data contains the serialized operations. + data []byte + // refs hold external references for operations. + refs []interface{} + // nextStateID is the id allocated for the next + // StateOp. + nextStateID int + + macroStack stack +} + +// StateOp represents a saved operation snapshop to be restored +// later. +type StateOp struct { + id int + macroID int + ops *Ops +} + +// MacroOp records a list of operations for later use. +type MacroOp struct { + ops *Ops + id stackID + pc pc +} + +// CallOp invokes the operations recorded by Record. +type CallOp struct { + // Ops is the list of operations to invoke. + ops *Ops + pc pc +} + +// InvalidateOp requests a redraw at the given time. Use +// the zero value to request an immediate redraw. +type InvalidateOp struct { + At time.Time +} + +// TransformOp applies a transform to the current transform. The zero value +// for TransformOp represents the identity transform. +type TransformOp struct { + t f32.Affine2D +} + +// stack tracks the integer identities of MacroOp +// operations to ensure correct pairing of Record/End. +type stack struct { + currentID int + nextID int +} + +type stackID struct { + id int + prev int +} + +type pc struct { + data int + refs int +} + +// Defer executes c after all other operations have completed, +// including previously deferred operations. +// Defer saves the current transformation and restores it prior +// to execution. All other operation state is reset. +// +// Note that deferred operations are executed in first-in-first-out +// order, unlike the Go facility of the same name. +func Defer(o *Ops, c CallOp) { + if c.ops == nil { + return + } + state := Save(o) + // Wrap c in a macro that loads the saved state before execution. + m := Record(o) + load(o, opconst.InitialStateID, opconst.AllState) + load(o, state.id, opconst.TransformState) + c.Add(o) + c = m.Stop() + // A Defer is recorded as a TypeDefer followed by the + // wrapped macro. + data := o.Write(opconst.TypeDeferLen) + data[0] = byte(opconst.TypeDefer) + c.Add(o) +} + +// Save the current operations state. +func Save(o *Ops) StateOp { + o.nextStateID++ + s := StateOp{ + ops: o, + id: o.nextStateID, + macroID: o.macroStack.currentID, + } + save(o, s.id) + return s +} + +// save records a save of the operations state to +// id. +func save(o *Ops, id int) { + bo := binary.LittleEndian + data := o.Write(opconst.TypeSaveLen) + data[0] = byte(opconst.TypeSave) + bo.PutUint32(data[1:], uint32(id)) +} + +// Load a previously saved operations state. +func (s StateOp) Load() { + if s.ops.macroStack.currentID != s.macroID { + panic("load in a different macro than save") + } + if s.id == 0 { + panic("zero-value op") + } + load(s.ops, s.id, opconst.AllState) +} + +// load a previously saved operations state given +// its ID. Only state included in mask is affected. +func load(o *Ops, id int, m opconst.StateMask) { + bo := binary.LittleEndian + data := o.Write(opconst.TypeLoadLen) + data[0] = byte(opconst.TypeLoad) + data[1] = byte(m) + bo.PutUint32(data[2:], uint32(id)) +} + +// Reset the Ops, preparing it for re-use. Reset invalidates +// any recorded macros. +func (o *Ops) Reset() { + o.macroStack = stack{} + // Leave references to the GC. + for i := range o.refs { + o.refs[i] = nil + } + o.data = o.data[:0] + o.refs = o.refs[:0] + o.nextStateID = 0 + o.version++ +} + +// Data is for internal use only. +func (o *Ops) Data() []byte { + return o.data +} + +// Refs is for internal use only. +func (o *Ops) Refs() []interface{} { + return o.refs +} + +// Version is for internal use only. +func (o *Ops) Version() int { + return o.version +} + +// Write is for internal use only. +func (o *Ops) Write(n int) []byte { + o.data = append(o.data, make([]byte, n)...) + return o.data[len(o.data)-n:] +} + +// Write1 is for internal use only. +func (o *Ops) Write1(n int, ref1 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1) + return o.data[len(o.data)-n:] +} + +// Write2 is for internal use only. +func (o *Ops) Write2(n int, ref1, ref2 interface{}) []byte { + o.data = append(o.data, make([]byte, n)...) + o.refs = append(o.refs, ref1, ref2) + return o.data[len(o.data)-n:] +} + +func (o *Ops) pc() pc { + return pc{data: len(o.data), refs: len(o.refs)} +} + +// Record a macro of operations. +func Record(o *Ops) MacroOp { + m := MacroOp{ + ops: o, + id: o.macroStack.push(), + pc: o.pc(), + } + // Reserve room for a macro definition. Updated in Stop. + m.ops.Write(opconst.TypeMacroLen) + m.fill() + return m +} + +// Stop ends a previously started recording and returns an +// operation for replaying it. +func (m MacroOp) Stop() CallOp { + m.ops.macroStack.pop(m.id) + m.fill() + return CallOp{ + ops: m.ops, + pc: m.pc, + } +} + +func (m MacroOp) fill() { + pc := m.ops.pc() + // Fill out the macro definition reserved in Record. + data := m.ops.data[m.pc.data:] + data = data[:opconst.TypeMacroLen] + data[0] = byte(opconst.TypeMacro) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(pc.data)) + bo.PutUint32(data[5:], uint32(pc.refs)) +} + +// Add the recorded list of operations. Add +// panics if the Ops containing the recording +// has been reset. +func (c CallOp) Add(o *Ops) { + if c.ops == nil { + return + } + data := o.Write1(opconst.TypeCallLen, c.ops) + data[0] = byte(opconst.TypeCall) + bo := binary.LittleEndian + bo.PutUint32(data[1:], uint32(c.pc.data)) + bo.PutUint32(data[5:], uint32(c.pc.refs)) +} + +func (r InvalidateOp) Add(o *Ops) { + data := o.Write(opconst.TypeRedrawLen) + data[0] = byte(opconst.TypeInvalidate) + bo := binary.LittleEndian + // UnixNano cannot represent the zero time. + if t := r.At; !t.IsZero() { + nanos := t.UnixNano() + if nanos > 0 { + bo.PutUint64(data[1:], uint64(nanos)) + } + } +} + +// Offset creates a TransformOp with the offset o. +func Offset(o f32.Point) TransformOp { + return TransformOp{t: f32.Affine2D{}.Offset(o)} +} + +// Affine creates a TransformOp representing the transformation a. +func Affine(a f32.Affine2D) TransformOp { + return TransformOp{t: a} +} + +func (t TransformOp) Add(o *Ops) { + data := o.Write(opconst.TypeTransformLen) + data[0] = byte(opconst.TypeTransform) + bo := binary.LittleEndian + a, b, c, d, e, f := t.t.Elems() + bo.PutUint32(data[1:], math.Float32bits(a)) + bo.PutUint32(data[1+4*1:], math.Float32bits(b)) + bo.PutUint32(data[1+4*2:], math.Float32bits(c)) + bo.PutUint32(data[1+4*3:], math.Float32bits(d)) + bo.PutUint32(data[1+4*4:], math.Float32bits(e)) + bo.PutUint32(data[1+4*5:], math.Float32bits(f)) +} + +func (s *stack) push() stackID { + s.nextID++ + sid := stackID{ + id: s.nextID, + prev: s.currentID, + } + s.currentID = s.nextID + return sid +} + +func (s *stack) check(sid stackID) { + if s.currentID != sid.id { + panic("unbalanced operation") + } +} + +func (s *stack) pop(sid stackID) { + s.check(sid) + s.currentID = sid.prev +} diff --git a/pkg/gel/gio/op/paint/doc.go b/pkg/gel/gio/op/paint/doc.go new file mode 100644 index 0000000..79054ab --- /dev/null +++ b/pkg/gel/gio/op/paint/doc.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* +Package paint provides drawing operations for 2D graphics. + +The PaintOp operation fills the current clip with the current brush, +taking the current transformation into account. + +The current brush is set by either a ColorOp for a constant color, or +ImageOp for an image, or LinearGradientOp for gradients. + +All color.NRGBA values are in the sRGB color space. +*/ +package paint diff --git a/pkg/gel/gio/op/paint/paint.go b/pkg/gel/gio/op/paint/paint.go new file mode 100644 index 0000000..953f685 --- /dev/null +++ b/pkg/gel/gio/op/paint/paint.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package paint + +import ( + "encoding/binary" + "image" + "image/color" + "image/draw" + "math" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/opconst" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" +) + +// ImageOp sets the brush to an image. +// +// Note: the ImageOp may keep a reference to the backing image. +// See NewImageOp for details. +type ImageOp struct { + uniform bool + color color.NRGBA + src *image.RGBA + + // handle is a key to uniquely identify this ImageOp + // in a map of cached textures. + handle interface{} +} + +// ColorOp sets the brush to a constant color. +type ColorOp struct { + Color color.NRGBA +} + +// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and +// ending at stop2 with color2. +type LinearGradientOp struct { + Stop1 f32.Point + Color1 color.NRGBA + Stop2 f32.Point + Color2 color.NRGBA +} + +// PaintOp fills fills the current clip area with the current brush. +type PaintOp struct { +} + +// NewImageOp creates an ImageOp backed by src. See +// github.com/p9c/p9/pkg/gel/gio/io/system.FrameEvent for a description of when data +// referenced by operations is safe to re-use. +// +// NewImageOp assumes the backing image is immutable, and may cache a +// copy of its contents in a GPU-friendly way. Create new ImageOps to +// ensure that changes to an image is reflected in the display of +// it. +func NewImageOp(src image.Image) ImageOp { + switch src := src.(type) { + case *image.Uniform: + col := color.NRGBAModel.Convert(src.C).(color.NRGBA) + return ImageOp{ + uniform: true, + color: col, + } + case *image.RGBA: + bounds := src.Bounds() + if bounds.Min == (image.Point{}) && src.Stride == bounds.Dx()*4 { + return ImageOp{ + src: src, + handle: new(int), + } + } + } + + sz := src.Bounds().Size() + // Copy the image into a GPU friendly format. + dst := image.NewRGBA(image.Rectangle{ + Max: sz, + }) + draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) + return ImageOp{ + src: dst, + handle: new(int), + } +} + +func (i ImageOp) Size() image.Point { + if i.src == nil { + return image.Point{} + } + return i.src.Bounds().Size() +} + +func (i ImageOp) Add(o *op.Ops) { + if i.uniform { + ColorOp{ + Color: i.color, + }.Add(o) + return + } + data := o.Write2(opconst.TypeImageLen, i.src, i.handle) + data[0] = byte(opconst.TypeImage) +} + +func (c ColorOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeColorLen) + data[0] = byte(opconst.TypeColor) + data[1] = c.Color.R + data[2] = c.Color.G + data[3] = c.Color.B + data[4] = c.Color.A +} + +func (c LinearGradientOp) Add(o *op.Ops) { + data := o.Write(opconst.TypeLinearGradientLen) + data[0] = byte(opconst.TypeLinearGradient) + + bo := binary.LittleEndian + bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X)) + bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y)) + bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X)) + bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y)) + + data[17+0] = c.Color1.R + data[17+1] = c.Color1.G + data[17+2] = c.Color1.B + data[17+3] = c.Color1.A + data[21+0] = c.Color2.R + data[21+1] = c.Color2.G + data[21+2] = c.Color2.B + data[21+3] = c.Color2.A +} + +func (d PaintOp) Add(o *op.Ops) { + data := o.Write(opconst.TypePaintLen) + data[0] = byte(opconst.TypePaint) +} + +// FillShape fills the clip shape with a color. +func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) { + defer op.Save(ops).Load() + shape.Add(ops) + Fill(ops, c) +} + +// Fill paints an infinitely large plane with the provided color. It +// is intended to be used with a clip.Op already in place to limit +// the painted area. Use FillShape unless you need to paint several +// times within the same clip.Op. +func Fill(ops *op.Ops, c color.NRGBA) { + defer op.Save(ops).Load() + ColorOp{Color: c}.Add(ops) + PaintOp{}.Add(ops) +} diff --git a/pkg/gel/gio/text/lru.go b/pkg/gel/gio/text/lru.go new file mode 100644 index 0000000..2e15d3a --- /dev/null +++ b/pkg/gel/gio/text/lru.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "golang.org/x/image/math/fixed" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +type layoutCache struct { + m map[layoutKey]*layoutElem + head, tail *layoutElem +} + +type pathCache struct { + m map[pathKey]*path + head, tail *path +} + +type layoutElem struct { + next, prev *layoutElem + key layoutKey + layout []Line +} + +type path struct { + next, prev *path + key pathKey + val op.CallOp +} + +type layoutKey struct { + ppem fixed.Int26_6 + maxWidth int + str string +} + +type pathKey struct { + ppem fixed.Int26_6 + str string +} + +const maxSize = 1000 + +func (l *layoutCache) Get(k layoutKey) ([]Line, bool) { + if lt, ok := l.m[k]; ok { + l.remove(lt) + l.insert(lt) + return lt.layout, true + } + return nil, false +} + +func (l *layoutCache) Put(k layoutKey, lt []Line) { + if l.m == nil { + l.m = make(map[layoutKey]*layoutElem) + l.head = new(layoutElem) + l.tail = new(layoutElem) + l.head.prev = l.tail + l.tail.next = l.head + } + val := &layoutElem{key: k, layout: lt} + l.m[k] = val + l.insert(val) + if len(l.m) > maxSize { + oldest := l.tail.next + l.remove(oldest) + delete(l.m, oldest.key) + } +} + +func (l *layoutCache) remove(lt *layoutElem) { + lt.next.prev = lt.prev + lt.prev.next = lt.next +} + +func (l *layoutCache) insert(lt *layoutElem) { + lt.next = l.head + lt.prev = l.head.prev + lt.prev.next = lt + lt.next.prev = lt +} + +func (c *pathCache) Get(k pathKey) (op.CallOp, bool) { + if v, ok := c.m[k]; ok { + c.remove(v) + c.insert(v) + return v.val, true + } + return op.CallOp{}, false +} + +func (c *pathCache) Put(k pathKey, v op.CallOp) { + if c.m == nil { + c.m = make(map[pathKey]*path) + c.head = new(path) + c.tail = new(path) + c.head.prev = c.tail + c.tail.next = c.head + } + val := &path{key: k, val: v} + c.m[k] = val + c.insert(val) + if len(c.m) > maxSize { + oldest := c.tail.next + c.remove(oldest) + delete(c.m, oldest.key) + } +} + +func (c *pathCache) remove(v *path) { + v.next.prev = v.prev + v.prev.next = v.next +} + +func (c *pathCache) insert(v *path) { + v.next = c.head + v.prev = c.head.prev + v.prev.next = v + v.next.prev = v +} diff --git a/pkg/gel/gio/text/lru_test.go b/pkg/gel/gio/text/lru_test.go new file mode 100644 index 0000000..38d9fc5 --- /dev/null +++ b/pkg/gel/gio/text/lru_test.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "strconv" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestLayoutLRU(t *testing.T) { + c := new(layoutCache) + put := func(i int) { + c.Put(layoutKey{str: strconv.Itoa(i)}, nil) + } + get := func(i int) bool { + _, ok := c.Get(layoutKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func TestPathLRU(t *testing.T) { + c := new(pathCache) + put := func(i int) { + c.Put(pathKey{str: strconv.Itoa(i)}, op.CallOp{}) + } + get := func(i int) bool { + _, ok := c.Get(pathKey{str: strconv.Itoa(i)}) + return ok + } + testLRU(t, put, get) +} + +func testLRU(t *testing.T, put func(i int), get func(i int) bool) { + for i := 0; i < maxSize; i++ { + put(i) + } + for i := 0; i < maxSize; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + put(maxSize) + for i := 1; i < maxSize+1; i++ { + if !get(i) { + t.Fatalf("key %d was evicted", i) + } + } + if i := 0; get(i) { + t.Fatalf("key %d was not evicted", i) + } +} diff --git a/pkg/gel/gio/text/shaper.go b/pkg/gel/gio/text/shaper.go new file mode 100644 index 0000000..90bbb9f --- /dev/null +++ b/pkg/gel/gio/text/shaper.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "io" + "strings" + + "golang.org/x/image/math/fixed" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Shaper implements layout and shaping of text. +type Shaper interface { + // Layout a text according to a set of options. + Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) + // LayoutString is Layout for strings. + LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line + // Shape a line of text and return a clipping operation for its outline. + Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp +} + +// A FontFace is a Font and a matching Face. +type FontFace struct { + Font Font + Face Face +} + +// Cache implements cached layout and shaping of text from a set of +// registered fonts. +// +// If a font matches no registered shape, Cache falls back to the +// first registered face. +// +// The LayoutString and ShapeString results are cached and re-used if +// possible. +type Cache struct { + def Typeface + faces map[Font]*faceCache +} + +type faceCache struct { + face Face + layoutCache layoutCache + pathCache pathCache +} + +func (c *Cache) lookup(font Font) *faceCache { + f := c.faceForStyle(font) + if f == nil { + font.Typeface = c.def + f = c.faceForStyle(font) + } + return f +} + +func (c *Cache) faceForStyle(font Font) *faceCache { + tf := c.faces[font] + if tf == nil { + font := font + font.Weight = Normal + tf = c.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + tf = c.faces[font] + } + if tf == nil { + font := font + font.Style = Regular + font.Weight = Normal + tf = c.faces[font] + } + return tf +} + +func NewCache(collection []FontFace) *Cache { + c := &Cache{ + faces: make(map[Font]*faceCache), + } + for i, ff := range collection { + if i == 0 { + c.def = ff.Font.Typeface + } + c.faces[ff.Font] = &faceCache{face: ff.Face} + } + return c +} + +// Layout implements the Shaper interface. +func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) { + cache := s.lookup(font) + return cache.face.Layout(size, maxWidth, txt) +} + +// LayoutString is a caching implementation of the Shaper interface. +func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line { + cache := s.lookup(font) + return cache.layout(size, maxWidth, str) +} + +// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout +// argument is unchanged from a call to Layout or LayoutString. +func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp { + cache := s.lookup(font) + return cache.shape(size, layout) +} + +func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line { + if f == nil { + return nil + } + lk := layoutKey{ + ppem: ppem, + maxWidth: maxWidth, + str: str, + } + if l, ok := f.layoutCache.Get(lk); ok { + return l + } + l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str)) + f.layoutCache.Put(lk, l) + return l +} + +func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp { + if f == nil { + return op.CallOp{} + } + pk := pathKey{ + ppem: ppem, + str: layout.Text, + } + if clip, ok := f.pathCache.Get(pk); ok { + return clip + } + clip := f.face.Shape(ppem, layout) + f.pathCache.Put(pk, clip) + return clip +} diff --git a/pkg/gel/gio/text/text.go b/pkg/gel/gio/text/text.go new file mode 100644 index 0000000..68bcac0 --- /dev/null +++ b/pkg/gel/gio/text/text.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "io" + + "golang.org/x/image/math/fixed" + + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// A Line contains the measurements of a line of text. +type Line struct { + Layout Layout + // Width is the width of the line. + Width fixed.Int26_6 + // Ascent is the height above the baseline. + Ascent fixed.Int26_6 + // Descent is the height below the baseline, including + // the line gap. + Descent fixed.Int26_6 + // Bounds is the visible bounds of the line. + Bounds fixed.Rectangle26_6 +} + +type Layout struct { + Text string + Advances []fixed.Int26_6 +} + +// Style is the font style. +type Style int + +// Weight is a font weight, in CSS units subtracted 400 so the zero value +// is normal text weight. +type Weight int + +// Font specify a particular typeface variant, style and weight. +type Font struct { + Typeface Typeface + Variant Variant + Style Style + // Weight is the text weight. If zero, Normal is used instead. + Weight Weight +} + +// Face implements text layout and shaping for a particular font. All +// methods must be safe for concurrent use. +type Face interface { + Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) + Shape(ppem fixed.Int26_6, str Layout) op.CallOp +} + +// Typeface identifies a particular typeface design. The empty +// string denotes the default typeface. +type Typeface string + +// Variant denotes a typeface variant such as "Mono" or "Smallcaps". +type Variant string + +type Alignment uint8 + +const ( + Start Alignment = iota + End + Middle +) + +const ( + Regular Style = iota + Italic +) + +const ( + Normal Weight = 400 - 400 + Medium Weight = 500 - 400 + Bold Weight = 600 - 400 +) + +func (a Alignment) String() string { + switch a { + case Start: + return "Start" + case End: + return "End" + case Middle: + return "Middle" + default: + panic("unreachable") + } +} diff --git a/pkg/gel/gio/unit/unit.go b/pkg/gel/gio/unit/unit.go new file mode 100644 index 0000000..fd2245c --- /dev/null +++ b/pkg/gel/gio/unit/unit.go @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +/* + +Package unit implements device independent units and values. + +A Value is a value with a Unit attached. + +Device independent pixel, or dp, is the unit for sizes independent of +the underlying display device. + +Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with +text scaling applied. + +Finally, pixels, or px, is the unit for display dependent pixels. Their +size vary between platforms and displays. + +To maintain a constant visual size across platforms and displays, always +use dps or sps to define user interfaces. Only use pixels for derived +values. + +*/ +package unit + +import ( + "fmt" + "math" +) + +// Value is a value with a unit. +type Value struct { + V float32 + U Unit +} + +// Unit represents a unit for a Value. +type Unit uint8 + +// Metric converts Values to device-dependent pixels, px. The zero +// value represents a 1-to-1 scale from dp, sp to pixels. +type Metric struct { + // PxPerDp is the device-dependent pixels per dp. + PxPerDp float32 + // PxPerSp is the device-dependent pixels per sp. + PxPerSp float32 +} + +const ( + // UnitPx represent device pixels in the resolution of + // the underlying display. + UnitPx Unit = iota + // UnitDp represents device independent pixels. 1 dp will + // have the same apparent size across platforms and + // display resolutions. + UnitDp + // UnitSp is like UnitDp but for font sizes. + UnitSp +) + +// Px returns the Value for v device pixels. +func Px(v float32) Value { + return Value{V: v, U: UnitPx} +} + +// Dp returns the Value for v device independent +// pixels. +func Dp(v float32) Value { + return Value{V: v, U: UnitDp} +} + +// Sp returns the Value for v scaled dps. +func Sp(v float32) Value { + return Value{V: v, U: UnitSp} +} + +// Scale returns the value scaled by s. +func (v Value) Scale(s float32) Value { + v.V *= s + return v +} + +func (v Value) String() string { + return fmt.Sprintf("%g%s", v.V, v.U) +} + +func (u Unit) String() string { + switch u { + case UnitPx: + return "px" + case UnitDp: + return "dp" + case UnitSp: + return "sp" + default: + panic("unknown unit") + } +} + +// Add a list of Values. +func Add(c Metric, values ...Value) Value { + var sum Value + for _, v := range values { + sum, v = compatible(c, sum, v) + sum.V += v.V + } + return sum +} + +// Max returns the maximum of a list of Values. +func Max(c Metric, values ...Value) Value { + var max Value + for _, v := range values { + max, v = compatible(c, max, v) + if v.V > max.V { + max.V = v.V + } + } + return max +} + +func (c Metric) Px(v Value) int { + var r float32 + switch v.U { + case UnitPx: + r = v.V + case UnitDp: + s := c.PxPerDp + if s == 0 { + s = 1 + } + r = s * v.V + case UnitSp: + s := c.PxPerSp + if s == 0 { + s = 1 + } + r = s * v.V + default: + panic("unknown unit") + } + return int(math.Round(float64(r))) +} + +func compatible(c Metric, v1, v2 Value) (Value, Value) { + if v1.U == v2.U { + return v1, v2 + } + if v1.V == 0 { + v1.U = v2.U + return v1, v2 + } + if v2.V == 0 { + v2.U = v1.U + return v1, v2 + } + return Px(float32(c.Px(v1))), Px(float32(c.Px(v2))) +} diff --git a/pkg/gel/gio/widget/bool.go b/pkg/gel/gio/widget/bool.go new file mode 100644 index 0000000..33b11d9 --- /dev/null +++ b/pkg/gel/gio/widget/bool.go @@ -0,0 +1,44 @@ +package widget + +import ( + "github.com/p9c/p9/pkg/gel/gio/layout" +) + +type Bool struct { + Value bool + + clk Clickable + + changed bool +} + +// Changed reports whether Value has changed since the last +// call to Changed. +func (b *Bool) Changed() bool { + changed := b.changed + b.changed = false + return changed +} + +// Hovered returns whether pointer is over the element. +func (b *Bool) Hovered() bool { + return b.clk.Hovered() +} + +// Pressed returns whether pointer is pressing the element. +func (b *Bool) Pressed() bool { + return b.clk.Pressed() +} + +func (b *Bool) History() []Press { + return b.clk.History() +} + +func (b *Bool) Layout(gtx layout.Context) layout.Dimensions { + dims := b.clk.Layout(gtx) + for b.clk.Clicked() { + b.Value = !b.Value + b.changed = true + } + return dims +} diff --git a/pkg/gel/gio/widget/border.go b/pkg/gel/gio/widget/border.go new file mode 100644 index 0000000..eec3226 --- /dev/null +++ b/pkg/gel/gio/widget/border.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +// Border lays out a widget and draws a border inside it. +type Border struct { + Color color.NRGBA + CornerRadius unit.Value + Width unit.Value +} + +func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { + dims := w(gtx) + sz := layout.FPt(dims.Size) + + rr := float32(gtx.Px(b.CornerRadius)) + width := float32(gtx.Px(b.Width)) + sz.X -= width + sz.Y -= width + + r := f32.Rectangle{Max: sz} + r = r.Add(f32.Point{X: width * 0.5, Y: width * 0.5}) + + paint.FillShape(gtx.Ops, + b.Color, + clip.Stroke{ + Path: clip.UniformRRect(r, rr).Path(gtx.Ops), + Style: clip.StrokeStyle{Width: width}, + }.Op(), + ) + + return dims +} diff --git a/pkg/gel/gio/widget/buffer.go b/pkg/gel/gio/widget/buffer.go new file mode 100644 index 0000000..e658d56 --- /dev/null +++ b/pkg/gel/gio/widget/buffer.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "io" + "strings" + "unicode/utf8" +) + +// editBuffer implements a gap buffer for text editing. +type editBuffer struct { + // pos is the byte position for Read and ReadRune. + pos int + + // The gap start and end in bytes. + gapstart, gapend int + text []byte + + // changed tracks whether the buffer content + // has changed since the last call to Changed. + changed bool +} + +const minSpace = 5 + +func (e *editBuffer) Changed() bool { + c := e.changed + e.changed = false + return c +} + +func (e *editBuffer) deleteRunes(caret, runes int) int { + e.moveGap(caret, 0) + for ; runes < 0 && e.gapstart > 0; runes++ { + _, s := utf8.DecodeLastRune(e.text[:e.gapstart]) + e.gapstart -= s + caret -= s + e.changed = e.changed || s > 0 + } + for ; runes > 0 && e.gapend < len(e.text); runes-- { + _, s := utf8.DecodeRune(e.text[e.gapend:]) + e.gapend += s + e.changed = e.changed || s > 0 + } + return caret +} + +// moveGap moves the gap to the caret position. After returning, +// the gap is guaranteed to be at least space bytes long. +func (e *editBuffer) moveGap(caret, space int) { + if e.gapLen() < space { + if space < minSpace { + space = minSpace + } + txt := make([]byte, e.len()+space) + // Expand to capacity. + txt = txt[:cap(txt)] + gaplen := len(txt) - e.len() + if caret > e.gapstart { + copy(txt, e.text[:e.gapstart]) + copy(txt[caret+gaplen:], e.text[caret:]) + copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) + } else { + copy(txt, e.text[:caret]) + copy(txt[e.gapstart+gaplen:], e.text[e.gapend:]) + copy(txt[caret+gaplen:], e.text[caret:e.gapstart]) + } + e.text = txt + e.gapstart = caret + e.gapend = e.gapstart + gaplen + } else { + if caret > e.gapstart { + copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) + } else { + copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart]) + } + l := e.gapLen() + e.gapstart = caret + e.gapend = e.gapstart + l + } +} + +func (e *editBuffer) len() int { + return len(e.text) - e.gapLen() +} + +func (e *editBuffer) gapLen() int { + return e.gapend - e.gapstart +} + +func (e *editBuffer) Reset() { + e.Seek(0, io.SeekStart) +} + +// Seek implements io.Seeker +func (e *editBuffer) Seek(offset int64, whence int) (ret int64, err error) { + switch whence { + case io.SeekStart: + e.pos = int(offset) + case io.SeekCurrent: + e.pos += int(offset) + case io.SeekEnd: + e.pos = e.len() - int(offset) + } + if e.pos < 0 { + e.pos = 0 + } else if e.pos > e.len() { + e.pos = e.len() + } + return int64(e.pos), nil +} + +func (e *editBuffer) Read(p []byte) (int, error) { + if e.pos == e.len() { + return 0, io.EOF + } + var total int + if e.pos < e.gapstart { + n := copy(p, e.text[e.pos:e.gapstart]) + p = p[n:] + total += n + e.pos += n + } + if e.pos >= e.gapstart { + n := copy(p, e.text[e.pos+e.gapLen():]) + total += n + e.pos += n + } + if e.pos > e.len() { + panic("hey!") + } + return total, nil +} + +func (e *editBuffer) ReadRune() (rune, int, error) { + if e.pos == e.len() { + return 0, 0, io.EOF + } + r, s := e.runeAt(e.pos) + e.pos += s + return r, s, nil +} + +func (e *editBuffer) String() string { + var b strings.Builder + b.Grow(e.len()) + b.Write(e.text[:e.gapstart]) + b.Write(e.text[e.gapend:]) + return b.String() +} + +func (e *editBuffer) prepend(caret int, s string) { + e.moveGap(caret, len(s)) + copy(e.text[caret:], s) + e.gapstart += len(s) + e.changed = e.changed || len(s) > 0 +} + +func (e *editBuffer) runeBefore(idx int) (rune, int) { + if idx > e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeLastRune(e.text[:idx]) +} + +func (e *editBuffer) runeAt(idx int) (rune, int) { + if idx >= e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeRune(e.text[idx:]) +} diff --git a/pkg/gel/gio/widget/button.go b/pkg/gel/gio/widget/button.go new file mode 100644 index 0000000..e9f8190 --- /dev/null +++ b/pkg/gel/gio/widget/button.go @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/gesture" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Clickable represents a clickable area. +type Clickable struct { + click gesture.Click + clicks []Click + // prevClicks is the index into clicks that marks the clicks + // from the most recent Layout call. prevClicks is used to keep + // clicks bounded. + prevClicks int + history []Press +} + +// Click represents a click. +type Click struct { + Modifiers key.Modifiers + NumClicks int +} + +// Press represents a past pointer press. +type Press struct { + // Position of the press. + Position f32.Point + // Start is when the press began. + Start time.Time + // End is when the press was ended by a release or cancel. + // A zero End means it hasn't ended yet. + End time.Time + // Cancelled is true for cancelled presses. + Cancelled bool +} + +// Click executes a simple programmatic click +func (b *Clickable) Click() { + b.clicks = append(b.clicks, Click{ + Modifiers: 0, + NumClicks: 1, + }) +} + +// Clicked reports whether there are pending clicks as would be +// reported by Clicks. If so, Clicked removes the earliest click. +func (b *Clickable) Clicked() bool { + if len(b.clicks) == 0 { + return false + } + n := copy(b.clicks, b.clicks[1:]) + b.clicks = b.clicks[:n] + if b.prevClicks > 0 { + b.prevClicks-- + } + return true +} + +// Hovered returns whether pointer is over the element. +func (b *Clickable) Hovered() bool { + return b.click.Hovered() +} + +// Pressed returns whether pointer is pressing the element. +func (b *Clickable) Pressed() bool { + return b.click.Pressed() +} + +// Clicks returns and clear the clicks since the last call to Clicks. +func (b *Clickable) Clicks() []Click { + clicks := b.clicks + b.clicks = nil + b.prevClicks = 0 + return clicks +} + +// History is the past pointer presses useful for drawing markers. +// History is retained for a short duration (about a second). +func (b *Clickable) History() []Press { + return b.history +} + +// Layout and update the button state +func (b *Clickable) Layout(gtx layout.Context) layout.Dimensions { + b.update(gtx) + stack := op.Save(gtx.Ops) + pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + b.click.Add(gtx.Ops) + stack.Load() + for len(b.history) > 0 { + c := b.history[0] + if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second { + break + } + n := copy(b.history, b.history[1:]) + b.history = b.history[:n] + } + return layout.Dimensions{Size: gtx.Constraints.Min} +} + +// update the button state by processing events. +func (b *Clickable) update(gtx layout.Context) { + // Flush clicks from before the last update. + n := copy(b.clicks, b.clicks[b.prevClicks:]) + b.clicks = b.clicks[:n] + b.prevClicks = n + + for _, e := range b.click.Events(gtx) { + switch e.Type { + case gesture.TypeClick: + b.clicks = append(b.clicks, Click{ + Modifiers: e.Modifiers, + NumClicks: e.NumClicks, + }) + if l := len(b.history); l > 0 { + b.history[l-1].End = gtx.Now + } + case gesture.TypeCancel: + for i := range b.history { + b.history[i].Cancelled = true + if b.history[i].End.IsZero() { + b.history[i].End = gtx.Now + } + } + case gesture.TypePress: + b.history = append(b.history, Press{ + Position: e.Position, + Start: gtx.Now, + }) + } + } +} diff --git a/pkg/gel/gio/widget/doc.go b/pkg/gel/gio/widget/doc.go new file mode 100644 index 0000000..dc1be7d --- /dev/null +++ b/pkg/gel/gio/widget/doc.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package widget implements state tracking and event handling of +// common user interface controls. To draw widgets, use a theme +// packages such as package github.com/p9c/p9/pkg/gel/gio/widget/material. +package widget diff --git a/pkg/gel/gio/widget/editor.go b/pkg/gel/gio/widget/editor.go new file mode 100644 index 0000000..2ddb9f7 --- /dev/null +++ b/pkg/gel/gio/widget/editor.go @@ -0,0 +1,1319 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "bufio" + "bytes" + "image" + "io" + "math" + "runtime" + "sort" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/gesture" + "github.com/p9c/p9/pkg/gel/gio/io/clipboard" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Editor implements an editable and scrollable text area. +type Editor struct { + Alignment text.Alignment + // SingleLine force the text to stay on a single line. + // SingleLine also sets the scrolling direction to + // horizontal. + SingleLine bool + // Submit enabled translation of carriage return keys to SubmitEvents. + // If not enabled, carriage returns are inserted as newlines in the text. + Submit bool + // Mask replaces the visual display of each rune in the contents with the given rune. + // Newline characters are not masked. When non-zero, the unmasked contents + // are accessed by Len, Text, and SetText. + Mask rune + + eventKey int + font text.Font + shaper text.Shaper + textSize fixed.Int26_6 + blinkStart time.Time + focused bool + rr editBuffer + maskReader maskReader + lastMask rune + maxWidth int + viewSize image.Point + valid bool + lines []text.Line + shapes []line + dims layout.Dimensions + requestFocus bool + + caret struct { + on bool + scroll bool + // start is the current caret position, and also the start position of + // selected text. end is the end positon of selected text. If start.ofs + // == end.ofs, then there's no selection. Note that it's possible (and + // common) that the caret (start) is after the end, e.g. after + // Shift-DownArrow. + start combinedPos + end combinedPos + } + + dragging bool + dragger gesture.Drag + scroller gesture.Scroll + scrollOff image.Point + + clicker gesture.Click + + // events is the list of events not yet processed. + events []EditorEvent + // prevEvents is the number of events from the previous frame. + prevEvents int +} + +type maskReader struct { + // rr is the underlying reader. + rr io.RuneReader + maskBuf [utf8.UTFMax]byte + // mask is the utf-8 encoded mask rune. + mask []byte + // overflow contains excess mask bytes left over after the last Read call. + overflow []byte +} + +// combinedPos is a point in the editor. +type combinedPos struct { + // editorBuffer offset. The other three fields are based off of this one. + ofs int + + // lineCol.Y = line (offset into Editor.lines), and X = col (offset into + // Editor.lines[Y]) + lineCol screenPos + + // Pixel coordinates + x fixed.Int26_6 + y int + + // xoff is the offset to the current position when moving between lines. + xoff fixed.Int26_6 +} + +type selectionAction int + +const ( + selectionExtend selectionAction = iota + selectionClear +) + +func (m *maskReader) Reset(r io.RuneReader, mr rune) { + m.rr = r + n := utf8.EncodeRune(m.maskBuf[:], mr) + m.mask = m.maskBuf[:n] +} + +// Read reads from the underlying reader and replaces every +// rune with the mask rune. +func (m *maskReader) Read(b []byte) (n int, err error) { + for len(b) > 0 { + var replacement []byte + if len(m.overflow) > 0 { + replacement = m.overflow + } else { + var r rune + r, _, err = m.rr.ReadRune() + if err != nil { + break + } + if r == '\n' { + replacement = []byte{'\n'} + } else { + replacement = m.mask + } + } + nn := copy(b, replacement) + m.overflow = replacement[nn:] + n += nn + b = b[nn:] + } + return n, err +} + +type EditorEvent interface { + isEditorEvent() +} + +// A ChangeEvent is generated for every user change to the text. +type ChangeEvent struct{} + +// A SubmitEvent is generated when Submit is set +// and a carriage return key is pressed. +type SubmitEvent struct { + Text string +} + +// A SelectEvent is generated when the user selects some text, or changes the +// selection (e.g. with a shift-click), including if they remove the +// selection. The selected text is not part of the event, on the theory that +// it could be a relatively expensive operation (for a large editor), most +// applications won't actually care about it, and those that do can call +// Editor.SelectedText() (which can be empty). +type SelectEvent struct{} + +type line struct { + offset image.Point + clip op.CallOp + selected bool + selectionYOffs int + selectionSize image.Point +} + +const ( + blinksPerSecond = 1 + maxBlinkDuration = 10 * time.Second +) + +// Events returns available editor events. +func (e *Editor) Events() []EditorEvent { + events := e.events + e.events = nil + e.prevEvents = 0 + return events +} + +func (e *Editor) processEvents(gtx layout.Context) { + // Flush events from before the previous Layout. + n := copy(e.events, e.events[e.prevEvents:]) + e.events = e.events[:n] + e.prevEvents = n + + if e.shaper == nil { + // Can't process events without a shaper. + return + } + oldStart, oldLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen() + e.processPointer(gtx) + e.processKey(gtx) + // Queue a SelectEvent if the selection changed, including if it went away. + if newStart, newLen := min(e.caret.start.ofs, e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen { + e.events = append(e.events, SelectEvent{}) + } +} + +func (e *Editor) makeValid(positions ...*combinedPos) { + if e.valid { + return + } + e.lines, e.dims = e.layoutText(e.shaper) + e.makeValidCaret(positions...) + e.valid = true +} + +func (e *Editor) processPointer(gtx layout.Context) { + sbounds := e.scrollBounds() + var smin, smax int + var axis gesture.Axis + if e.SingleLine { + axis = gesture.Horizontal + smin, smax = sbounds.Min.X, sbounds.Max.X + } else { + axis = gesture.Vertical + smin, smax = sbounds.Min.Y, sbounds.Max.Y + } + sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis) + var soff int + if e.SingleLine { + e.scrollRel(sdist, 0) + soff = e.scrollOff.X + } else { + e.scrollRel(0, sdist) + soff = e.scrollOff.Y + } + for _, evt := range e.clickDragEvents(gtx) { + switch evt := evt.(type) { + case gesture.ClickEvent: + switch { + case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse, + evt.Type == gesture.TypeClick: + prevCaretPos := e.caret.start + e.blinkStart = gtx.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.requestFocus = true + if e.scroller.State() != gesture.StateFlinging { + e.caret.scroll = true + } + + if evt.Modifiers == key.ModShift { + // If they clicked closer to the end, then change the end to + // where the caret used to be (effectively swapping start & end). + if abs(e.caret.end.ofs-e.caret.start.ofs) < abs(e.caret.start.ofs-prevCaretPos.ofs) { + e.caret.end = prevCaretPos + } + } else { + e.ClearSelection() + } + e.dragging = true + + // Process a double-click. + if evt.NumClicks == 2 { + e.moveWord(-1, selectionClear) + e.moveWord(1, selectionExtend) + e.dragging = false + } + } + case pointer.Event: + release := false + switch { + case evt.Type == pointer.Release && evt.Source == pointer.Mouse: + release = true + fallthrough + case evt.Type == pointer.Drag && evt.Source == pointer.Mouse: + if e.dragging { + e.blinkStart = gtx.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.caret.scroll = true + + if release { + e.dragging = false + } + } + } + } + } + + if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) { + e.scroller.Stop() + } +} + +func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event { + var combinedEvents []event.Event + for _, evt := range e.clicker.Events(gtx) { + combinedEvents = append(combinedEvents, evt) + } + for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) { + combinedEvents = append(combinedEvents, evt) + } + return combinedEvents +} + +func (e *Editor) processKey(gtx layout.Context) { + if e.rr.Changed() { + e.events = append(e.events, ChangeEvent{}) + } + for _, ke := range gtx.Events(&e.eventKey) { + e.blinkStart = gtx.Now + switch ke := ke.(type) { + case key.FocusEvent: + e.focused = ke.Focus + case key.Event: + if !e.focused || ke.State != key.Press { + break + } + if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { + if !ke.Modifiers.Contain(key.ModShift) { + e.events = append(e.events, SubmitEvent{ + Text: e.Text(), + }) + continue + } + } + if e.command(gtx, ke) { + e.caret.scroll = true + e.scroller.Stop() + } + case key.EditEvent: + e.caret.scroll = true + e.scroller.Stop() + e.append(ke.Text) + // Complete a paste event, initiated by Shortcut-V in Editor.command(). + case clipboard.Event: + e.caret.scroll = true + e.scroller.Stop() + e.append(ke.Text) + } + if e.rr.Changed() { + e.events = append(e.events, ChangeEvent{}) + } + } +} + +func (e *Editor) moveLines(distance int, selAct selectionAction) { + e.caret.start = e.movePosToLine(e.caret.start, e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance) + e.updateSelection(selAct) +} + +func (e *Editor) command(gtx layout.Context, k key.Event) bool { + modSkip := key.ModCtrl + if runtime.GOOS == "darwin" { + modSkip = key.ModAlt + } + moveByWord := k.Modifiers.Contain(modSkip) + selAct := selectionClear + if k.Modifiers.Contain(key.ModShift) { + selAct = selectionExtend + } + switch k.Name { + case key.NameReturn, key.NameEnter: + e.append("\n") + case key.NameDeleteBackward: + if moveByWord { + e.deleteWord(-1) + } else { + e.Delete(-1) + } + case key.NameDeleteForward: + if moveByWord { + e.deleteWord(1) + } else { + e.Delete(1) + } + case key.NameUpArrow: + e.moveLines(-1, selAct) + case key.NameDownArrow: + e.moveLines(+1, selAct) + case key.NameLeftArrow: + if moveByWord { + e.moveWord(-1, selAct) + } else { + if selAct == selectionClear { + e.ClearSelection() + } + e.MoveCaret(-1, -1*int(selAct)) + } + case key.NameRightArrow: + if moveByWord { + e.moveWord(1, selAct) + } else { + if selAct == selectionClear { + e.ClearSelection() + } + e.MoveCaret(1, int(selAct)) + } + case key.NamePageUp: + e.movePages(-1, selAct) + case key.NamePageDown: + e.movePages(+1, selAct) + case key.NameHome: + e.moveStart(selAct) + case key.NameEnd: + e.moveEnd(selAct) + // Initiate a paste operation, by requesting the clipboard contents; other + // half is in Editor.processKey() under clipboard.Event. + case "V": + if k.Modifiers != key.ModShortcut { + return false + } + clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops) + // Copy or Cut selection -- ignored if nothing selected. + case "C", "X": + if k.Modifiers != key.ModShortcut { + return false + } + if text := e.SelectedText(); text != "" { + clipboard.WriteOp{Text: text}.Add(gtx.Ops) + if k.Name == "X" { + e.Delete(1) + } + } + // Select all + case "A": + if k.Modifiers != key.ModShortcut { + return false + } + e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len()) + default: + return false + } + return true +} + +// Focus requests the input focus for the Editor. +func (e *Editor) Focus() { + e.requestFocus = true +} + +// Focused returns whether the editor is focused or not. +func (e *Editor) Focused() bool { + return e.focused +} + +// Layout lays out the editor. +func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, size unit.Value) layout.Dimensions { + textSize := fixed.I(gtx.Px(size)) + if e.font != font || e.textSize != textSize { + e.invalidate() + e.font = font + e.textSize = textSize + } + maxWidth := gtx.Constraints.Max.X + if e.SingleLine { + maxWidth = inf + } + if maxWidth != e.maxWidth { + e.maxWidth = maxWidth + e.invalidate() + } + if sh != e.shaper { + e.shaper = sh + e.invalidate() + } + if e.Mask != e.lastMask { + e.lastMask = e.Mask + e.invalidate() + } + + e.makeValid() + e.processEvents(gtx) + e.makeValid() + + if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize { + e.viewSize = viewSize + e.invalidate() + } + e.makeValid() + + return e.layout(gtx) +} + +func (e *Editor) layout(gtx layout.Context) layout.Dimensions { + // Adjust scrolling for new viewport and layout. + e.scrollRel(0, 0) + + if e.caret.scroll { + e.caret.scroll = false + e.scrollToCaret() + } + + off := image.Point{ + X: -e.scrollOff.X, + Y: -e.scrollOff.Y, + } + clip := textPadding(e.lines) + clip.Max = clip.Max.Add(e.viewSize) + startSel, endSel := sortPoints(e.caret.start.lineCol, e.caret.end.lineCol) + it := segmentIterator{ + startSel: startSel, + endSel: endSel, + Lines: e.lines, + Clip: clip, + Alignment: e.Alignment, + Width: e.viewSize.X, + Offset: off, + } + e.shapes = e.shapes[:0] + for { + layout, off, selected, yOffs, size, ok := it.Next() + if !ok { + break + } + path := e.shaper.Shape(e.font, e.textSize, layout) + e.shapes = append(e.shapes, line{off, path, selected, yOffs, size}) + } + + key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops) + if e.requestFocus { + key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops) + key.SoftKeyboardOp{Show: true}.Add(gtx.Ops) + } + e.requestFocus = false + pointerPadding := gtx.Px(unit.Dp(4)) + r := image.Rectangle{Max: e.viewSize} + r.Min.X -= pointerPadding + r.Min.Y -= pointerPadding + r.Max.X += pointerPadding + r.Max.X += pointerPadding + pointer.Rect(r).Add(gtx.Ops) + pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops) + + var scrollRange image.Rectangle + if e.SingleLine { + scrollRange.Min.X = -e.scrollOff.X + scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X)) + } else { + scrollRange.Min.Y = -e.scrollOff.Y + scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y)) + } + e.scroller.Add(gtx.Ops, scrollRange) + + e.clicker.Add(gtx.Ops) + e.dragger.Add(gtx.Ops) + e.caret.on = false + if e.focused { + now := gtx.Now + dt := now.Sub(e.blinkStart) + blinking := dt < maxBlinkDuration + const timePerBlink = time.Second / blinksPerSecond + nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) + if blinking { + redraw := op.InvalidateOp{At: nextBlink} + redraw.Add(gtx.Ops) + } + e.caret.on = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2) + } + + return layout.Dimensions{Size: e.viewSize, Baseline: e.dims.Baseline} +} + +// PaintSelection paints the contrasting background for selected text. +func (e *Editor) PaintSelection(gtx layout.Context) { + cl := textPadding(e.lines) + cl.Max = cl.Max.Add(e.viewSize) + clip.Rect(cl).Add(gtx.Ops) + for _, shape := range e.shapes { + if !shape.selected { + continue + } + stack := op.Save(gtx.Ops) + offset := shape.offset + offset.Y += shape.selectionYOffs + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + clip.Rect(image.Rectangle{Max: shape.selectionSize}).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } +} + +func (e *Editor) PaintText(gtx layout.Context) { + cl := textPadding(e.lines) + cl.Max = cl.Max.Add(e.viewSize) + clip.Rect(cl).Add(gtx.Ops) + for _, shape := range e.shapes { + stack := op.Save(gtx.Ops) + op.Offset(layout.FPt(shape.offset)).Add(gtx.Ops) + shape.clip.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } +} + +func (e *Editor) PaintCaret(gtx layout.Context) { + if !e.caret.on { + return + } + e.makeValid() + carWidth := fixed.I(gtx.Px(unit.Dp(1))) + carX := e.caret.start.x + carY := e.caret.start.y + + defer op.Save(gtx.Ops).Load() + carX -= carWidth / 2 + carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y + carRect := image.Rectangle{ + Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()}, + Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()}, + } + carRect = carRect.Add(image.Point{ + X: -e.scrollOff.X, + Y: -e.scrollOff.Y, + }) + cl := textPadding(e.lines) + // Account for caret width to each side. + whalf := (carWidth / 2).Ceil() + if cl.Max.X < whalf { + cl.Max.X = whalf + } + if cl.Min.X > -whalf { + cl.Min.X = -whalf + } + cl.Max = cl.Max.Add(e.viewSize) + carRect = cl.Intersect(carRect) + if !carRect.Empty() { + st := op.Save(gtx.Ops) + clip.Rect(carRect).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Load() + } +} + +// Len is the length of the editor contents. +func (e *Editor) Len() int { + return e.rr.len() +} + +// Text returns the contents of the editor. +func (e *Editor) Text() string { + return e.rr.String() +} + +// SetText replaces the contents of the editor, clearing any selection first. +func (e *Editor) SetText(s string) { + e.rr = editBuffer{} + e.caret.start = combinedPos{} + e.caret.end = combinedPos{} + e.prepend(s) +} + +func (e *Editor) scrollBounds() image.Rectangle { + var b image.Rectangle + if e.SingleLine { + if len(e.lines) > 0 { + b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewSize.X).Floor() + if b.Min.X > 0 { + b.Min.X = 0 + } + } + b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X + } else { + b.Max.Y = e.dims.Size.Y - e.viewSize.Y + } + return b +} + +func (e *Editor) scrollRel(dx, dy int) { + e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy) +} + +func (e *Editor) scrollAbs(x, y int) { + e.scrollOff.X = x + e.scrollOff.Y = y + b := e.scrollBounds() + if e.scrollOff.X > b.Max.X { + e.scrollOff.X = b.Max.X + } + if e.scrollOff.X < b.Min.X { + e.scrollOff.X = b.Min.X + } + if e.scrollOff.Y > b.Max.Y { + e.scrollOff.Y = b.Max.Y + } + if e.scrollOff.Y < b.Min.Y { + e.scrollOff.Y = b.Min.Y + } +} + +func (e *Editor) moveCoord(pos image.Point) { + var ( + prevDesc fixed.Int26_6 + carLine int + y int + ) + for _, l := range e.lines { + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y { + break + } + carLine++ + } + x := fixed.I(pos.X + e.scrollOff.X) + e.caret.start = e.movePosToLine(e.caret.start, x, carLine) + e.caret.start.xoff = 0 +} + +func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) { + e.rr.Reset() + var r io.Reader = &e.rr + if e.Mask != 0 { + e.maskReader.Reset(&e.rr, e.Mask) + r = &e.maskReader + } + var lines []text.Line + if s != nil { + lines, _ = s.Layout(e.font, e.textSize, e.maxWidth, r) + } else { + lines, _ = nullLayout(r) + } + dims := linesDimens(lines) + for i := 0; i < len(lines)-1; i++ { + // To avoid layout flickering while editing, assume a soft newline takes + // up all available space. + if layout := lines[i].Layout; len(layout.Text) > 0 { + r := layout.Text[len(layout.Text)-1] + if r != '\n' { + dims.Size.X = e.maxWidth + break + } + } + } + return lines, dims +} + +// CaretPos returns the line & column numbers of the caret. +func (e *Editor) CaretPos() (line, col int) { + e.makeValid() + return e.caret.start.lineCol.Y, e.caret.start.lineCol.X +} + +// CaretCoords returns the coordinates of the caret, relative to the +// editor itself. +func (e *Editor) CaretCoords() f32.Point { + e.makeValid() + return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y)) +} + +// offsetToScreenPos2 is a utility function to shortcut the common case of +// wanting the positions of exactly two offsets. +func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) { + cp1, iter := e.offsetToScreenPos(o1) + return cp1, iter(o2) +} + +// offsetToScreenPos takes an offset into the editor text (e.g. +// e.caret.end.ofs) and returns a combinedPos that corresponds to its current +// screen position, as well as an iterator that lets you get the combinedPos +// of a later offset. The offsets given to offsetToScreenPos and to the +// returned iterator must be sorted, lowest first, and they must be valid (0 +// <= offset <= e.Len()). +// +// This function is written this way to take advantage of previous work done +// for offsets after the first. Otherwise you have to start from the top each +// time. +func (e *Editor) offsetToScreenPos(offset int) (combinedPos, func(int) combinedPos) { + var col, line, idx int + var x fixed.Int26_6 + + l := e.lines[line] + y := l.Ascent.Ceil() + prevDesc := l.Descent + + iter := func(offset int) combinedPos { + LOOP: + for { + for ; col < len(l.Layout.Advances); col++ { + if idx >= offset { + break LOOP + } + + x += l.Layout.Advances[col] + _, s := e.rr.runeAt(idx) + idx += s + } + if lastLine := line == len(e.lines)-1; lastLine || idx > offset { + break LOOP + } + + line++ + x = 0 + col = 0 + l = e.lines[line] + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + } + return combinedPos{ + lineCol: screenPos{Y: line, X: col}, + x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X), + y: y, + ofs: offset, + } + } + return iter(offset), iter +} + +func (e *Editor) invalidate() { + e.valid = false +} + +// Delete runes from the caret position. The sign of runes specifies the +// direction to delete: positive is forward, negative is backward. +// +// If there is a selection, it is deleted and counts as a single rune. +func (e *Editor) Delete(runes int) { + if runes == 0 { + return + } + + if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 { + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l) + runes -= sign(runes) + } + + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes) + e.caret.start.xoff = 0 + e.ClearSelection() + e.invalidate() +} + +// Insert inserts text at the caret, moving the caret forward. If there is a +// selection, Insert overwrites it. +func (e *Editor) Insert(s string) { + e.append(s) + e.caret.scroll = true +} + +// append inserts s at the cursor, leaving the caret is at the end of s. If +// there is a selection, append overwrites it. +// xxx|yyy + append zzz => xxxzzz|yyy +func (e *Editor) append(s string) { + e.prepend(s) + e.caret.start.ofs += len(s) + e.caret.end.ofs = e.caret.start.ofs +} + +// prepend inserts s after the cursor; the caret does not change. If there is +// a selection, prepend overwrites it. +// xxx|yyy + prepend zzz => xxx|zzzyyy +func (e *Editor) prepend(s string) { + if e.SingleLine { + s = strings.ReplaceAll(s, "\n", " ") + } + e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first. + e.rr.prepend(e.caret.start.ofs, s) + e.caret.start.xoff = 0 + e.invalidate() +} + +func (e *Editor) movePages(pages int, selAct selectionAction) { + e.makeValid() + y := e.caret.start.y + pages*e.viewSize.Y + var ( + prevDesc fixed.Int26_6 + carLine2 int + ) + y2 := e.lines[0].Ascent.Ceil() + for i := 1; i < len(e.lines); i++ { + if y2 >= y { + break + } + l := e.lines[i] + h := (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y2+h-y >= y-y2 { + break + } + y2 += h + carLine2++ + } + e.caret.start = e.movePosToLine(e.caret.start, e.caret.start.x+e.caret.start.xoff, carLine2) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6, line int) combinedPos { + e.makeValid(&pos) + if line < 0 { + line = 0 + } + if line >= len(e.lines) { + line = len(e.lines) - 1 + } + + prevDesc := e.lines[line].Descent + for pos.lineCol.Y < line { + pos = e.movePosToEnd(pos) + l := e.lines[pos.lineCol.Y] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.y += (prevDesc + l.Ascent).Ceil() + pos.lineCol.X = 0 + prevDesc = l.Descent + pos.lineCol.Y++ + } + for pos.lineCol.Y > line { + pos = e.movePosToStart(pos) + l := e.lines[pos.lineCol.Y] + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.y -= (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + pos.lineCol.Y-- + l = e.lines[pos.lineCol.Y] + pos.lineCol.X = len(l.Layout.Advances) - 1 + } + + pos = e.movePosToStart(pos) + l := e.lines[line] + pos.x = align(e.Alignment, l.Width, e.viewSize.X) + // Only move past the end of the last line + end := 0 + if line < len(e.lines)-1 { + end = 1 + } + // Move to rune closest to x. + for i := 0; i < len(l.Layout.Advances)-end; i++ { + adv := l.Layout.Advances[i] + if pos.x >= x { + break + } + if pos.x+adv-x >= x-pos.x { + break + } + pos.x += adv + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.lineCol.X++ + } + pos.xoff = x - pos.x + return pos +} + +// MoveCaret moves the caret (aka selection start) and the selection end +// relative to their current positions. Positive distances moves forward, +// negative distances moves backward. Distances are in runes. +func (e *Editor) MoveCaret(startDelta, endDelta int) { + e.makeValid() + keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta + e.caret.start = e.movePos(e.caret.start, startDelta) + e.caret.start.xoff = 0 + // If they were in the same place, and we're moving them the same distance, + // just assign the new position, instead of recalculating it. + if keepSame { + e.caret.end = e.caret.start + } else { + e.caret.end = e.movePos(e.caret.end, endDelta) + e.caret.end.xoff = 0 + } +} + +func (e *Editor) movePos(pos combinedPos, distance int) combinedPos { + for ; distance < 0 && pos.ofs > 0; distance++ { + if pos.lineCol.X == 0 { + // Move to end of previous line. + pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1) + continue + } + l := e.lines[pos.lineCol.Y].Layout + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.lineCol.X-- + pos.x -= l.Advances[pos.lineCol.X] + } + for ; distance > 0 && pos.ofs < e.rr.len(); distance-- { + l := e.lines[pos.lineCol.Y].Layout + // Only move past the end of the last line + end := 0 + if pos.lineCol.Y < len(e.lines)-1 { + end = 1 + } + if pos.lineCol.X >= len(l.Advances)-end { + // Move to start of next line. + pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1) + continue + } + pos.x += l.Advances[pos.lineCol.X] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.lineCol.X++ + } + return pos +} + +func (e *Editor) moveStart(selAct selectionAction) { + e.caret.start = e.movePosToStart(e.caret.start) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToStart(pos combinedPos) combinedPos { + e.makeValid(&pos) + layout := e.lines[pos.lineCol.Y].Layout + for i := pos.lineCol.X - 1; i >= 0; i-- { + _, s := e.rr.runeBefore(pos.ofs) + pos.ofs -= s + pos.x -= layout.Advances[i] + } + pos.lineCol.X = 0 + pos.xoff = -pos.x + return pos +} + +func (e *Editor) moveEnd(selAct selectionAction) { + e.caret.start = e.movePosToEnd(e.caret.start) + e.updateSelection(selAct) +} + +func (e *Editor) movePosToEnd(pos combinedPos) combinedPos { + e.makeValid(&pos) + l := e.lines[pos.lineCol.Y] + // Only move past the end of the last line + end := 0 + if pos.lineCol.Y < len(e.lines)-1 { + end = 1 + } + layout := l.Layout + for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ { + adv := layout.Advances[i] + _, s := e.rr.runeAt(pos.ofs) + pos.ofs += s + pos.x += adv + pos.lineCol.X++ + } + a := align(e.Alignment, l.Width, e.viewSize.X) + pos.xoff = l.Width + a - pos.x + return pos +} + +// moveWord moves the caret to the next word in the specified direction. +// Positive is forward, negative is backward. +// Absolute values greater than one will skip that many words. +func (e *Editor) moveWord(distance int, selAct selectionAction) { + e.makeValid() + // split the distance information into constituent parts to be + // used independently. + words, direction := distance, 1 + if distance < 0 { + words, direction = distance*-1, -1 + } + // atEnd if caret is at either side of the buffer. + atEnd := func() bool { + return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len() + } + // next returns the appropriate rune given the direction. + next := func() (r rune) { + if direction < 0 { + r, _ = e.rr.runeBefore(e.caret.start.ofs) + } else { + r, _ = e.rr.runeAt(e.caret.start.ofs) + } + return r + } + for ii := 0; ii < words; ii++ { + for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() { + e.MoveCaret(direction, 0) + } + e.MoveCaret(direction, 0) + for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() { + e.MoveCaret(direction, 0) + } + } + e.updateSelection(selAct) +} + +// deleteWord deletes the next word(s) in the specified direction. +// Unlike moveWord, deleteWord treats whitespace as a word itself. +// Positive is forward, negative is backward. +// Absolute values greater than one will delete that many words. +// The selection counts as a single word. +func (e *Editor) deleteWord(distance int) { + if distance == 0 { + return + } + + e.makeValid() + + if e.caret.start.ofs != e.caret.end.ofs { + e.Delete(1) + distance -= sign(distance) + } + if distance == 0 { + return + } + + // split the distance information into constituent parts to be + // used independently. + words, direction := distance, 1 + if distance < 0 { + words, direction = distance*-1, -1 + } + // atEnd if offset is at or beyond either side of the buffer. + atEnd := func(offset int) bool { + idx := e.caret.start.ofs + offset*direction + return idx <= 0 || idx >= e.rr.len() + } + // next returns the appropriate rune given the direction and offset. + next := func(offset int) (r rune) { + idx := e.caret.start.ofs + offset*direction + if idx < 0 { + idx = 0 + } else if idx > e.rr.len() { + idx = e.rr.len() + } + if direction < 0 { + r, _ = e.rr.runeBefore(idx) + } else { + r, _ = e.rr.runeAt(idx) + } + return r + } + var runes = 1 + for ii := 0; ii < words; ii++ { + if r := next(runes); unicode.IsSpace(r) { + for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) { + runes += 1 + } + } else { + for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) { + runes += 1 + } + } + } + e.Delete(runes * direction) +} + +func (e *Editor) scrollToCaret() { + e.makeValid() + l := e.lines[e.caret.start.lineCol.Y] + if e.SingleLine { + var dist int + if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 { + dist = d + } else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 { + dist = d + } + e.scrollRel(dist, 0) + } else { + miny := e.caret.start.y - l.Ascent.Ceil() + maxy := e.caret.start.y + l.Descent.Ceil() + var dist int + if d := miny - e.scrollOff.Y; d < 0 { + dist = d + } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 { + dist = d + } + e.scrollRel(0, dist) + } +} + +// NumLines returns the number of lines in the editor. +func (e *Editor) NumLines() int { + e.makeValid() + return len(e.lines) +} + +// SelectionLen returns the length of the selection, in bytes; it is +// equivalent to len(e.SelectedText()). +func (e *Editor) SelectionLen() int { + return abs(e.caret.start.ofs - e.caret.end.ofs) +} + +// Selection returns the start and end of the selection, as offsets into the +// editor text. start can be > end. +func (e *Editor) Selection() (start, end int) { + return e.caret.start.ofs, e.caret.end.ofs +} + +// SetCaret moves the caret to start, and sets the selection end to end. start +// and end are in bytes, and represent offsets into the editor text. start and +// end must be at a rune boundary. +func (e *Editor) SetCaret(start, end int) { + e.makeValid() + // Constrain start and end to [0, e.Len()]. + l := e.Len() + start = max(min(start, l), 0) + end = max(min(end, l), 0) + e.caret.start.ofs, e.caret.end.ofs = start, end + e.makeValidCaret() + e.caret.scroll = true + e.scroller.Stop() +} + +func (e *Editor) makeValidCaret(positions ...*combinedPos) { + // Jump through some hoops to order the offsets given to offsetToScreenPos, + // but still be able to update them correctly with the results thereof. + positions = append(positions, &e.caret.start, &e.caret.end) + sort.Slice(positions, func(i, j int) bool { + return positions[i].ofs < positions[j].ofs + }) + var iter func(offset int) combinedPos + *positions[0], iter = e.offsetToScreenPos(positions[0].ofs) + for _, cp := range positions[1:] { + *cp = iter(cp.ofs) + } +} + +// SelectedText returns the currently selected text (if any) from the editor. +func (e *Editor) SelectedText() string { + l := e.SelectionLen() + if l == 0 { + return "" + } + buf := make([]byte, l) + e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart) + _, err := e.rr.Read(buf) + if err != nil { + // The only error that rr.Read can return is EOF, which just means no + // selection, but we've already made sure that shouldn't happen. + panic("impossible error because end is before e.rr.Len()") + } + return string(buf) +} + +func (e *Editor) updateSelection(selAct selectionAction) { + if selAct == selectionClear { + e.ClearSelection() + } +} + +// ClearSelection clears the selection, by setting the selection end equal to +// the selection start. +func (e *Editor) ClearSelection() { + e.caret.end = e.caret.start +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +func sign(n int) int { + switch { + case n < 0: + return -1 + case n > 0: + return 1 + default: + return 0 + } +} + +// sortPoints returns a and b sorted such that a2 <= b2. +func sortPoints(a, b screenPos) (a2, b2 screenPos) { + if b.Less(a) { + return b, a + } + return a, b +} + +func nullLayout(r io.Reader) ([]text.Line, error) { + rr := bufio.NewReader(r) + var rerr error + var n int + var buf bytes.Buffer + for { + r, s, err := rr.ReadRune() + n += s + buf.WriteRune(r) + if err != nil { + rerr = err + break + } + } + return []text.Line{ + { + Layout: text.Layout{ + Text: buf.String(), + Advances: make([]fixed.Int26_6, n), + }, + }, + }, rerr +} + +func (s ChangeEvent) isEditorEvent() {} +func (s SubmitEvent) isEditorEvent() {} +func (s SelectEvent) isEditorEvent() {} diff --git a/pkg/gel/gio/widget/editor_test.go b/pkg/gel/gio/widget/editor_test.go new file mode 100644 index 0000000..45710a0 --- /dev/null +++ b/pkg/gel/gio/widget/editor_test.go @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "fmt" + "image" + "math/rand" + "reflect" + "strings" + "testing" + "testing/quick" + "unicode" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/font/gofont" + "github.com/p9c/p9/pkg/gel/gio/io/event" + "github.com/p9c/p9/pkg/gel/gio/io/key" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "golang.org/x/image/math/fixed" +) + +func TestEditor(t *testing.T) { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + + e.SetCaret(0, 0) // shouldn't panic + assertCaret(t, e, 0, 0, 0) + e.SetText("æbc\naøå•") + e.Layout(gtx, cache, font, fontSize) + assertCaret(t, e, 0, 0, 0) + e.moveEnd(selectionClear) + assertCaret(t, e, 0, 3, len("æbc")) + e.MoveCaret(+1, +1) + assertCaret(t, e, 1, 0, len("æbc\n")) + e.MoveCaret(-1, -1) + assertCaret(t, e, 0, 3, len("æbc")) + e.moveLines(+1, +1) + assertCaret(t, e, 1, 3, len("æbc\naøå")) + e.moveEnd(selectionClear) + assertCaret(t, e, 1, 4, len("æbc\naøå•")) + e.MoveCaret(+1, +1) + assertCaret(t, e, 1, 4, len("æbc\naøå•")) + + e.SetCaret(0, 0) + assertCaret(t, e, 0, 0, 0) + e.SetCaret(len("æ"), len("æ")) + assertCaret(t, e, 0, 1, 2) + e.SetCaret(len("æbc\naøå•"), len("æbc\naøå•")) + assertCaret(t, e, 1, 4, len("æbc\naøå•")) + + // Ensure that password masking does not affect caret behavior + e.MoveCaret(-3, -3) + assertCaret(t, e, 1, 1, len("æbc\na")) + e.Mask = '*' + e.Layout(gtx, cache, font, fontSize) + assertCaret(t, e, 1, 1, len("æbc\na")) + e.MoveCaret(-3, -3) + assertCaret(t, e, 0, 2, len("æb")) + e.Mask = '\U0001F92B' + e.Layout(gtx, cache, font, fontSize) + e.moveEnd(selectionClear) + assertCaret(t, e, 0, 3, len("æbc")) + + // When a password mask is applied, it should replace all visible glyphs + for i, line := range e.lines { + for j, r := range line.Layout.Text { + if r != e.Mask && !unicode.IsSpace(r) { + t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r) + } + } + } +} + +func TestEditorDimensions(t *testing.T) { + e := new(Editor) + tq := &testQueue{ + events: []event.Event{ + key.EditEvent{Text: "A"}, + }, + } + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Constraints{Max: image.Pt(100, 100)}, + Queue: tq, + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + dims := e.Layout(gtx, cache, font, fontSize) + if dims.Size.X == 0 { + t.Errorf("EditEvent was not reflected in Editor width") + } +} + +// assertCaret asserts that the editor caret is at a particular line +// and column, and that the byte position matches as well. +func assertCaret(t *testing.T, e *Editor, line, col, bytes int) { + t.Helper() + gotLine, gotCol := e.CaretPos() + if gotLine != line || gotCol != col { + t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col) + } + if bytes != e.caret.start.ofs { + t.Errorf("caret at buffer position %d, expected %d", e.caret.start.ofs, bytes) + } +} + +type editMutation int + +const ( + setText editMutation = iota + moveRune + moveLine + movePage + moveStart + moveEnd + moveCoord + moveWord + deleteWord + moveLast // Mark end; never generated. +) + +func TestEditorCaretConsistency(t *testing.T) { + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + for _, a := range []text.Alignment{text.Start, text.Middle, text.End} { + e := &Editor{ + Alignment: a, + } + e.Layout(gtx, cache, font, fontSize) + + consistent := func() error { + t.Helper() + gotLine, gotCol := e.CaretPos() + gotCoords := e.CaretCoords() + want, _ := e.offsetToScreenPos(e.caret.start.ofs) + wantCoords := f32.Pt(float32(want.x)/64, float32(want.y)) + if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords { + return nil + } + return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", + gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X, wantCoords) + } + if err := consistent(); err != nil { + t.Errorf("initial editor inconsistency (alignment %s): %v", a, err) + } + + move := func(mutation editMutation, str string, distance int8, x, y uint16) bool { + switch mutation { + case setText: + e.SetText(str) + e.Layout(gtx, cache, font, fontSize) + case moveRune: + e.MoveCaret(int(distance), int(distance)) + case moveLine: + e.moveLines(int(distance), selectionClear) + case movePage: + e.movePages(int(distance), selectionClear) + case moveStart: + e.moveStart(selectionClear) + case moveEnd: + e.moveEnd(selectionClear) + case moveCoord: + e.moveCoord(image.Pt(int(x), int(y))) + case moveWord: + e.moveWord(int(distance), selectionClear) + case deleteWord: + e.deleteWord(int(distance)) + default: + return false + } + if err := consistent(); err != nil { + t.Error(err) + return false + } + return true + } + if err := quick.Check(move, nil); err != nil { + t.Errorf("editor inconsistency (alignment %s): %v", a, err) + } + } +} + +func TestEditorMoveWord(t *testing.T) { + type Test struct { + Text string + Start int + Skip int + Want int + } + tests := []Test{ + {"", 0, 0, 0}, + {"", 0, -1, 0}, + {"", 0, 1, 0}, + {"hello", 0, -1, 0}, + {"hello", 0, 1, 5}, + {"hello world", 3, 1, 5}, + {"hello world", 3, -1, 0}, + {"hello world", 8, -1, 6}, + {"hello world", 8, 1, 11}, + {"hello world", 3, 1, 5}, + {"hello world", 3, 2, 14}, + {"hello world", 8, 1, 14}, + {"hello world", 8, -1, 0}, + {"hello brave new world", 0, 3, 15}, + } + setup := func(t string) *Editor { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + e.SetText(t) + e.Layout(gtx, cache, font, fontSize) + return e + } + for ii, tt := range tests { + e := setup(tt.Text) + e.MoveCaret(tt.Start, tt.Start) + e.moveWord(tt.Skip, selectionClear) + if e.caret.start.ofs != tt.Want { + t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, e.caret.start.ofs, tt.Want) + } + } +} + +func TestEditorDeleteWord(t *testing.T) { + type Test struct { + Text string + Start int + Selection int + Delete int + + Want int + Result string + } + tests := []Test{ + // No text selected + {"", 0, 0, 0, 0, ""}, + {"", 0, 0, -1, 0, ""}, + {"", 0, 0, 1, 0, ""}, + {"", 0, 0, -2, 0, ""}, + {"", 0, 0, 2, 0, ""}, + {"hello", 0, 0, -1, 0, "hello"}, + {"hello", 0, 0, 1, 0, ""}, + + // Document (imho) incorrect behavior w.r.t. deleting spaces following + // words. + {"hello world", 0, 0, 1, 0, " world"}, // Should be "world", if you ask me. + {"hello world", 0, 0, 2, 0, "world"}, // Should be "". + {"hello ", 0, 0, 1, 0, " "}, // Should be "". + {"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello". + {"hello world", 11, 0, -2, 5, "hello"}, // Should be "". + {"hello ", 6, 0, -1, 0, ""}, // Correct result. + + {"hello world", 3, 0, 1, 3, "hel world"}, + {"hello world", 3, 0, -1, 0, "lo world"}, + {"hello world", 8, 0, -1, 6, "hello rld"}, + {"hello world", 8, 0, 1, 8, "hello wo"}, + {"hello world", 3, 0, 1, 3, "hel world"}, + {"hello world", 3, 0, 2, 3, "helworld"}, + {"hello world", 8, 0, 1, 8, "hello "}, + {"hello world", 8, 0, -1, 5, "hello world"}, + {"hello brave new world", 0, 0, 3, 0, " new world"}, + // Add selected text. + // + // Several permutations must be tested: + // - select from the left or right + // - Delete + or - + // - abs(Delete) == 1 or > 1 + // + // "brave |" selected; caret at | + {"hello there brave new world", 12, 6, 1, 12, "hello there new world"}, // #16 + {"hello there brave new world", 12, 6, 2, 12, "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases. + {"hello there brave new world", 12, 6, -1, 12, "hello there new world"}, + {"hello there brave new world", 12, 6, -2, 6, "hello new world"}, + // "|brave " selected + {"hello there brave new world", 18, -6, 1, 12, "hello there new world"}, // #20 + {"hello there brave new world", 18, -6, 2, 12, "hello there world"}, // ditto + {"hello there brave new world", 18, -6, -1, 12, "hello there new world"}, + {"hello there brave new world", 18, -6, -2, 6, "hello new world"}, + // Random edge cases + {"hello there brave new world", 12, 6, 99, 12, "hello there "}, + {"hello there brave new world", 18, -6, -99, 0, "new world"}, + } + setup := func(t string) *Editor { + e := new(Editor) + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + cache := text.NewCache(gofont.Collection()) + fontSize := unit.Px(10) + font := text.Font{} + e.SetText(t) + e.Layout(gtx, cache, font, fontSize) + return e + } + for ii, tt := range tests { + e := setup(tt.Text) + e.MoveCaret(tt.Start, tt.Start) + e.MoveCaret(0, tt.Selection) + e.deleteWord(tt.Delete) + if e.caret.start.ofs != tt.Want { + t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, e.caret.start.ofs, tt.Want) + } + if e.Text() != tt.Result { + t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result) + } + } +} + +func TestEditorNoLayout(t *testing.T) { + var e Editor + e.SetText("hi!\n") + e.MoveCaret(1, 1) +} + +// Generate generates a value of itself, for testing/quick. +func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value { + t := editMutation(rand.Intn(int(moveLast))) + return reflect.ValueOf(t) +} + +// TestSelect tests the selection code. It lays out an editor with several +// lines in it, selects some text, verifies the selection, resizes the editor +// to make it much narrower (which makes the lines in the editor reflow), and +// then verifies that the updated (col, line) positions of the selected text +// are where we expect. +func TestSelect(t *testing.T) { + e := new(Editor) + e.SetText(`a123456789a +b123456789b +c123456789c +d123456789d +e123456789e +f123456789f +g123456789g +`) + + gtx := layout.Context{Ops: new(op.Ops)} + cache := text.NewCache(gofont.Collection()) + font := text.Font{} + fontSize := unit.Px(10) + + selected := func(start, end int) string { + // Layout once with no events; populate e.lines. + gtx.Queue = nil + e.Layout(gtx, cache, font, fontSize) + _ = e.Events() // throw away any events from this layout + + // Build the selection events + startPos, endPos := e.offsetToScreenPos2(sortInts(start, end)) + tq := &testQueue{ + events: []event.Event{ + pointer.Event{ + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Source: pointer.Mouse, + Position: f32.Pt(textWidth(e, startPos.lineCol.Y, 0, startPos.lineCol.X), textHeight(e, startPos.lineCol.Y)), + }, + pointer.Event{ + Type: pointer.Release, + Source: pointer.Mouse, + Position: f32.Pt(textWidth(e, endPos.lineCol.Y, 0, endPos.lineCol.X), textHeight(e, endPos.lineCol.Y)), + }, + }, + } + gtx.Queue = tq + + e.Layout(gtx, cache, font, fontSize) + for _, evt := range e.Events() { + switch evt.(type) { + case SelectEvent: + return e.SelectedText() + } + } + return "" + } + + type testCase struct { + // input text offsets + start, end int + + // expected selected text + selection string + // expected line/col positions of selection after resize + startPos, endPos screenPos + } + + for n, tst := range []testCase{ + {0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}}, + {0, 4, "a123", screenPos{}, screenPos{Y: 0, X: 4}}, + {0, 11, "a123456789a", screenPos{}, screenPos{Y: 1, X: 5}}, + {2, 6, "2345", screenPos{Y: 0, X: 2}, screenPos{Y: 1, X: 0}}, + {41, 66, "56789d\ne123456789e\nf12345", screenPos{Y: 6, X: 5}, screenPos{Y: 11, X: 0}}, + } { + // printLines(e) + + gtx.Constraints = layout.Exact(image.Pt(100, 100)) + if got := selected(tst.start, tst.end); got != tst.selection { + t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got) + continue + } + + // Constrain the editor to roughly 6 columns wide and redraw + gtx.Constraints = layout.Exact(image.Pt(36, 36)) + // Keep existing selection + gtx.Queue = nil + e.Layout(gtx, cache, font, fontSize) + + if e.caret.end.lineCol != tst.startPos || e.caret.start.lineCol != tst.endPos { + t.Errorf("Test %d pt2: Expected %#v, %#v; got %#v, %#v", + n, + e.caret.end.lineCol, e.caret.start.lineCol, + tst.startPos, tst.endPos) + continue + } + + // printLines(e) + } +} + +// Verify that an existing selection is dismissed when you press arrow keys. +func TestSelectMove(t *testing.T) { + e := new(Editor) + e.SetText(`0123456789`) + + gtx := layout.Context{Ops: new(op.Ops)} + cache := text.NewCache(gofont.Collection()) + font := text.Font{} + fontSize := unit.Px(10) + + // Layout once to populate e.lines and get focus. + gtx.Queue = newQueue(key.FocusEvent{Focus: true}) + e.Layout(gtx, cache, font, fontSize) + + testKey := func(keyName string) { + // Select 345 + e.SetCaret(3, 6) + if expected, got := "345", e.SelectedText(); expected != got { + t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) + } + + // Press the key + gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName}) + e.Layout(gtx, cache, font, fontSize) + + if expected, got := "", e.SelectedText(); expected != got { + t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) + } + } + + testKey(key.NameLeftArrow) + testKey(key.NameRightArrow) + testKey(key.NameUpArrow) + testKey(key.NameDownArrow) +} + +func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 { + var w fixed.Int26_6 + advances := e.lines[lineNum].Layout.Advances + if colEnd > len(advances) { + colEnd = len(advances) + } + for _, adv := range advances[colStart:colEnd] { + w += adv + } + return float32(w.Floor()) +} + +func textHeight(e *Editor, lineNum int) float32 { + var h fixed.Int26_6 + for _, line := range e.lines[0:lineNum] { + h += line.Ascent + line.Descent + } + return float32(h.Floor() + 1) +} + +type testQueue struct { + events []event.Event +} + +func newQueue(e ...event.Event) *testQueue { + return &testQueue{events: e} +} + +func (q *testQueue) Events(_ event.Tag) []event.Event { + return q.events +} + +func printLines(e *Editor) { + for n, line := range e.lines { + text := strings.TrimSuffix(line.Layout.Text, "\n") + fmt.Printf("%d: %s\n", n, text) + } +} + +// sortInts returns a and b sorted such that a2 <= b2. +func sortInts(a, b int) (a2, b2 int) { + if b < a { + return b, a + } + return a, b +} diff --git a/pkg/gel/gio/widget/enum.go b/pkg/gel/gio/widget/enum.go new file mode 100644 index 0000000..59fbb3f --- /dev/null +++ b/pkg/gel/gio/widget/enum.go @@ -0,0 +1,77 @@ +package widget + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/gesture" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +type Enum struct { + Value string + hovered string + hovering bool + + changed bool + + clicks []gesture.Click + values []string +} + +func index(vs []string, t string) int { + for i, v := range vs { + if v == t { + return i + } + } + return -1 +} + +// Changed reports whether Value has changed by user interaction since the last +// call to Changed. +func (e *Enum) Changed() bool { + changed := e.changed + e.changed = false + return changed +} + +// Hovered returns the key that is highlighted, or false if none are. +func (e *Enum) Hovered() (string, bool) { + return e.hovered, e.hovering +} + +// Layout adds the event handler for key. +func (e *Enum) Layout(gtx layout.Context, key string) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + + if index(e.values, key) == -1 { + e.values = append(e.values, key) + e.clicks = append(e.clicks, gesture.Click{}) + e.clicks[len(e.clicks)-1].Add(gtx.Ops) + } else { + idx := index(e.values, key) + clk := &e.clicks[idx] + for _, ev := range clk.Events(gtx) { + switch ev.Type { + case gesture.TypeClick: + if new := e.values[idx]; new != e.Value { + e.Value = new + e.changed = true + } + } + } + if e.hovering && e.hovered == key { + e.hovering = false + } + if clk.Hovered() { + e.hovered = key + e.hovering = true + } + clk.Add(gtx.Ops) + } + + return layout.Dimensions{Size: gtx.Constraints.Min} +} diff --git a/pkg/gel/gio/widget/example_test.go b/pkg/gel/gio/widget/example_test.go new file mode 100644 index 0000000..e439ea6 --- /dev/null +++ b/pkg/gel/gio/widget/example_test.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget_test + +import ( + "fmt" + "image" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/io/router" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +func ExampleClickable_passthrough() { + // When laying out clickable widgets on top of each other, + // pointer events can be passed down for the underlying + // widgets to pick them up. + var button1, button2 widget.Clickable + var r router.Router + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + Queue: &r, + } + + // widget lays out two buttons on top of each other. + widget := func() { + // button2 completely covers button1, but PassOp allows pointer + // events to pass through to button1. + button1.Layout(gtx) + // PassOp is applied to the area defined by button1. + pointer.PassOp{Pass: true}.Add(gtx.Ops) + button2.Layout(gtx) + } + + // The first layout and call to Frame declare the Clickable handlers + // to the input router, so the following pointer events are propagated. + widget() + r.Frame(gtx.Ops) + // Simulate one click on the buttons by sending a Press and Release event. + r.Queue( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Press, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonPrimary, + Type: pointer.Release, + Position: f32.Pt(50, 50), + }, + ) + // The second layout ensures that the click event is registered by the buttons. + widget() + + if button1.Clicked() { + fmt.Println("button1 clicked!") + } + if button2.Clicked() { + fmt.Println("button2 clicked!") + } + + // Output: + // button1 clicked! + // button2 clicked! +} diff --git a/pkg/gel/gio/widget/fit.go b/pkg/gel/gio/widget/fit.go new file mode 100644 index 0000000..58dc3d4 --- /dev/null +++ b/pkg/gel/gio/widget/fit.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" +) + +// Fit scales a widget to fit and clip to the constraints. +type Fit uint8 + +const ( + // Unscaled does not alter the scale of a widget. + Unscaled Fit = iota + // Contain scales widget as large as possible without cropping + // and it preserves aspect-ratio. + Contain + // Cover scales the widget to cover the constraint area and + // preserves aspect-ratio. + Cover + // ScaleDown scales the widget smaller without cropping, + // when it exceeds the constraint area. + // It preserves aspect-ratio. + ScaleDown + // Fill stretches the widget to the constraints and does not + // preserve aspect-ratio. + Fill +) + +// scale adds clip and scale operations to fit dims to the constraints. +// It positions the widget to the appropriate position. +// It returns dimensions modified accordingly. +func (fit Fit) scale(gtx layout.Context, pos layout.Direction, dims layout.Dimensions) layout.Dimensions { + widgetSize := dims.Size + + if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 { + dims.Size = gtx.Constraints.Constrain(dims.Size) + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(widgetSize, dims.Size) + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + dims.Baseline += offset.Y + return dims + } + + scale := f32.Point{ + X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X), + Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y), + } + + switch fit { + case Contain: + if scale.Y < scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + case Cover: + if scale.Y > scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + case ScaleDown: + if scale.Y < scale.X { + scale.X = scale.Y + } else { + scale.Y = scale.X + } + + // The widget would need to be scaled up, no change needed. + if scale.X >= 1 { + dims.Size = gtx.Constraints.Constrain(dims.Size) + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(widgetSize, dims.Size) + op.Offset(layout.FPt(offset)).Add(gtx.Ops) + dims.Baseline += offset.Y + return dims + } + case Fill: + } + + var scaledSize image.Point + scaledSize.X = int(float32(widgetSize.X) * scale.X) + scaledSize.Y = int(float32(widgetSize.Y) * scale.Y) + dims.Size = gtx.Constraints.Constrain(scaledSize) + dims.Baseline = int(float32(dims.Baseline) * scale.Y) + + clip.Rect{Max: dims.Size}.Add(gtx.Ops) + + offset := pos.Position(scaledSize, dims.Size) + op.Affine(f32.Affine2D{}. + Scale(f32.Point{}, scale). + Offset(layout.FPt(offset)), + ).Add(gtx.Ops) + + dims.Baseline += offset.Y + + return dims +} diff --git a/pkg/gel/gio/widget/fit_test.go b/pkg/gel/gio/widget/fit_test.go new file mode 100644 index 0000000..901632a --- /dev/null +++ b/pkg/gel/gio/widget/fit_test.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "bytes" + "encoding/binary" + "image" + "math" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +func TestFit(t *testing.T) { + type test struct { + Dims image.Point + Scale f32.Point + Result image.Point + } + + fittests := [...][]test{ + Unscaled: { + { + Dims: image.Point{0, 0}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 0, Y: 0}, + }, { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 25}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 100}, + }}, + Contain: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 2, Y: 2}, + Result: image.Point{X: 100, Y: 50}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 0.5, Y: 0.5}, + Result: image.Point{X: 25, Y: 100}, + }}, + Cover: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 4, Y: 4}, + Result: image.Point{X: 100, Y: 100}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 2, Y: 2}, + Result: image.Point{X: 100, Y: 100}, + }}, + ScaleDown: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 1, Y: 1}, + Result: image.Point{X: 50, Y: 25}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 0.5, Y: 0.5}, + Result: image.Point{X: 25, Y: 100}, + }}, + Fill: { + { + Dims: image.Point{50, 25}, + Scale: f32.Point{X: 2, Y: 4}, + Result: image.Point{X: 100, Y: 100}, + }, { + Dims: image.Point{50, 200}, + Scale: f32.Point{X: 2, Y: 0.5}, + Result: image.Point{X: 100, Y: 100}, + }}, + } + + for fit, tests := range fittests { + fit := Fit(fit) + for i, test := range tests { + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Constraints{ + Max: image.Point{X: 100, Y: 100}, + }, + } + + result := fit.scale(gtx, layout.NW, layout.Dimensions{Size: test.Dims}) + + if test.Scale.X != 1 || test.Scale.Y != 1 { + opsdata := gtx.Ops.Data() + scaleX := float32Bytes(test.Scale.X) + scaleY := float32Bytes(test.Scale.Y) + if !bytes.Contains(opsdata, scaleX) { + t.Errorf("did not find scale.X:%v (%x) in ops: %x", test.Scale.X, scaleX, opsdata) + } + if !bytes.Contains(opsdata, scaleY) { + t.Errorf("did not find scale.Y:%v (%x) in ops: %x", test.Scale.Y, scaleY, opsdata) + } + } + + if result.Size != test.Result { + t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i, test.Result, result.Size) + } + } + } +} + +func float32Bytes(v float32) []byte { + var dst [4]byte + binary.LittleEndian.PutUint32(dst[:], math.Float32bits(v)) + return dst[:] +} diff --git a/pkg/gel/gio/widget/float.go b/pkg/gel/gio/widget/float.go new file mode 100644 index 0000000..ef8f7bf --- /dev/null +++ b/pkg/gel/gio/widget/float.go @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/gesture" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +// Float is for selecting a value in a range. +type Float struct { + Value float32 + Axis layout.Axis + + drag gesture.Drag + pos float32 // position normalized to [0, 1] + length float32 + changed bool +} + +// Dragging returns whether the value is being interacted with. +func (f *Float) Dragging() bool { return f.drag.Dragging() } + +// Layout updates the value according to drag events along the f's main axis. +// +// The range of f is set by the minimum constraints main axis value. +func (f *Float) Layout(gtx layout.Context, pointerMargin int, min, max float32) layout.Dimensions { + size := gtx.Constraints.Min + f.length = float32(f.Axis.Convert(size).X) + + var de *pointer.Event + for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(f.Axis)) { + if e.Type == pointer.Press || e.Type == pointer.Drag { + de = &e + } + } + + value := f.Value + if de != nil { + xy := de.Position.X + if f.Axis == layout.Vertical { + xy = de.Position.Y + } + f.pos = xy / f.length + value = min + (max-min)*f.pos + } else if min != max { + f.pos = (value - min) / (max - min) + } + // Unconditionally call setValue in case min, max, or value changed. + f.setValue(value, min, max) + + if f.pos < 0 { + f.pos = 0 + } else if f.pos > 1 { + f.pos = 1 + } + + defer op.Save(gtx.Ops).Load() + margin := f.Axis.Convert(image.Pt(pointerMargin, 0)) + rect := image.Rectangle{ + Min: margin.Mul(-1), + Max: size.Add(margin), + } + pointer.Rect(rect).Add(gtx.Ops) + f.drag.Add(gtx.Ops) + + return layout.Dimensions{Size: size} +} + +func (f *Float) setValue(value, min, max float32) { + if min > max { + min, max = max, min + } + if value < min { + value = min + } else if value > max { + value = max + } + if f.Value != value { + f.Value = value + f.changed = true + } +} + +// Pos reports the selected position. +func (f *Float) Pos() float32 { + return f.pos * f.length +} + +// Changed reports whether the value has changed since +// the last call to Changed. +func (f *Float) Changed() bool { + changed := f.changed + f.changed = false + return changed +} diff --git a/pkg/gel/gio/widget/icon.go b/pkg/gel/gio/widget/icon.go new file mode 100644 index 0000000..a20a915 --- /dev/null +++ b/pkg/gel/gio/widget/icon.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "image/color" + "image/draw" + + "golang.org/x/exp/shiny/iconvg" + + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +type Icon struct { + Color color.NRGBA + src []byte + // Cached values. + op paint.ImageOp + imgSize int + imgColor color.NRGBA +} + +// NewIcon returns a new Icon from IconVG data. +func NewIcon(data []byte) (*Icon, error) { + _, err := iconvg.DecodeMetadata(data) + if err != nil { + return nil, err + } + return &Icon{src: data, Color: color.NRGBA{A: 0xff}}, nil +} + +func (ic *Icon) Layout(gtx layout.Context, sz unit.Value) layout.Dimensions { + ico := ic.image(gtx.Px(sz)) + ico.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + return layout.Dimensions{ + Size: ico.Size(), + } +} + +func (ic *Icon) image(sz int) paint.ImageOp { + if sz == ic.imgSize && ic.Color == ic.imgColor { + return ic.op + } + m, _ := iconvg.DecodeMetadata(ic.src) + dx, dy := m.ViewBox.AspectRatio() + img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, Y: int(float32(sz) * dy / dx)}}) + var ico iconvg.Rasterizer + ico.SetDstImage(img, img.Bounds(), draw.Src) + m.Palette[0] = f32color.NRGBAToLinearRGBA(ic.Color) + iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{ + Palette: &m.Palette, + }) + ic.op = paint.NewImageOp(img) + ic.imgSize = sz + ic.imgColor = ic.Color + return ic.op +} diff --git a/pkg/gel/gio/widget/icon_test.go b/pkg/gel/gio/widget/icon_test.go new file mode 100644 index 0000000..e359e7e --- /dev/null +++ b/pkg/gel/gio/widget/icon_test.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "image/color" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +func TestIcon_Alpha(t *testing.T) { + icon, err := NewIcon(icons.ToggleCheckBox) + if err != nil { + t.Fatal(err) + } + + icon.Color = color.NRGBA{B: 0xff, A: 0x40} + + gtx := layout.Context{ + Ops: new(op.Ops), + Constraints: layout.Exact(image.Pt(100, 100)), + } + + _ = icon.Layout(gtx, unit.Sp(18)) +} diff --git a/pkg/gel/gio/widget/image.go b/pkg/gel/gio/widget/image.go new file mode 100644 index 0000000..615ed11 --- /dev/null +++ b/pkg/gel/gio/widget/image.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +// Image is a widget that displays an image. +type Image struct { + // Src is the image to display. + Src paint.ImageOp + // Fit specifies how to scale the image to the constraints. + // By default it does not do any scaling. + Fit Fit + // Position specifies where to position the image within + // the constraints. + Position layout.Direction + // Scale is the ratio of image pixels to + // dps. If Scale is zero Image falls back to + // a scale that match a standard 72 DPI. + Scale float32 +} + +const defaultScale = float32(160.0 / 72.0) + +func (im Image) Layout(gtx layout.Context) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + + scale := im.Scale + if scale == 0 { + scale = defaultScale + } + + size := im.Src.Size() + wf, hf := float32(size.X), float32(size.Y) + w, h := gtx.Px(unit.Dp(wf*scale)), gtx.Px(unit.Dp(hf*scale)) + + dims := im.Fit.scale(gtx, im.Position, layout.Dimensions{Size: image.Pt(w, h)}) + + pixelScale := scale * gtx.Metric.PxPerDp + op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(pixelScale, pixelScale))).Add(gtx.Ops) + + im.Src.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return dims +} diff --git a/pkg/gel/gio/widget/image_test.go b/pkg/gel/gio/widget/image_test.go new file mode 100644 index 0000000..d5a98c7 --- /dev/null +++ b/pkg/gel/gio/widget/image_test.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + "testing" + + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +func TestImageScale(t *testing.T) { + var ops op.Ops + gtx := layout.Context{ + Ops: &ops, + Constraints: layout.Constraints{ + Max: image.Pt(50, 50), + }, + } + imgSize := image.Pt(10, 10) + img := image.NewNRGBA(image.Rectangle{Max: imgSize}) + imgOp := paint.NewImageOp(img) + + // Ensure the default scales correctly. + dims := Image{Src: imgOp}.Layout(gtx) + expectedSize := imgSize + expectedSize.X = int(float32(expectedSize.X) * defaultScale) + expectedSize.Y = int(float32(expectedSize.Y) * defaultScale) + if dims.Size != expectedSize { + t.Fatalf("non-scaled image is wrong size, expected %v, got %v", expectedSize, dims.Size) + } + + // Ensure scaling the image via the Scale field works. + currentScale := float32(0.5) + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale) + if dims.Size != expectedSize { + t.Fatalf(".5 scale image is wrong size, expected %v, got %v", expectedSize, dims.Size) + } + + // Ensure the image responds to changes in DPI. + currentScale = float32(1) + gtx.Metric.PxPerDp = 2 + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) + if dims.Size != expectedSize { + t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v", expectedSize, dims.Size) + } + + // Ensure scaling the image responds to changes in DPI. + currentScale = float32(.5) + gtx.Metric.PxPerDp = 2 + dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) + expectedSize = imgSize + expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) + expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) + if dims.Size != expectedSize { + t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v", expectedSize, dims.Size) + } +} diff --git a/pkg/gel/gio/widget/label.go b/pkg/gel/gio/widget/label.go new file mode 100644 index 0000000..ea32acc --- /dev/null +++ b/pkg/gel/gio/widget/label.go @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "fmt" + "image" + "unicode/utf8" + + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Label is a widget for laying out and drawing text. +type Label struct { + // Alignment specify the text alignment. + Alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + MaxLines int +} + +// screenPos describes a character position (in text line and column numbers, +// not pixels): Y = line number, X = rune column. +type screenPos image.Point + +type segmentIterator struct { + Lines []text.Line + Clip image.Rectangle + Alignment text.Alignment + Width int + Offset image.Point + startSel screenPos + endSel screenPos + + pos screenPos // current position + line text.Line // current line + layout text.Layout // current line's Layout + + // pixel positions + off fixed.Point26_6 + y, prevDesc fixed.Int26_6 +} + +const inf = 1e6 + +func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) { + for l.pos.Y < len(l.Lines) { + if l.pos.X == 0 { + l.line = l.Lines[l.pos.Y] + + // Calculate X & Y pixel coordinates of left edge of line. We need y + // for the next line, so it's in l, but we only need x here, so it's + // not. + x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X) + l.y += l.prevDesc + l.line.Ascent + l.prevDesc = l.line.Descent + // Align baseline and line start to the pixel grid. + l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())} + l.y = l.off.Y + l.off.Y += fixed.I(l.Offset.Y) + if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { + break + } + + if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { + // This line is outside/before the clip area; go on to the next line. + l.pos.Y++ + continue + } + + // Copy the line's Layout, since we slice it up later. + l.layout = l.line.Layout + + // Find the left edge of the text visible in the l.Clip clipping + // area. + for len(l.layout.Advances) > 0 { + _, n := utf8.DecodeRuneInString(l.layout.Text) + adv := l.layout.Advances[0] + if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X { + break + } + l.off.X += adv + l.layout.Text = l.layout.Text[n:] + l.layout.Advances = l.layout.Advances[1:] + l.pos.X++ + } + } + + selected := l.inSelection() + endx := l.off.X + rune := 0 + nextLine := true + retLayout := l.layout + for n := range l.layout.Text { + selChanged := selected != l.inSelection() + beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X + if selChanged || beyondClipEdge { + retLayout.Advances = l.layout.Advances[:rune] + retLayout.Text = l.layout.Text[:n] + if selChanged { + // Save the rest of the line + l.layout.Advances = l.layout.Advances[rune:] + l.layout.Text = l.layout.Text[n:] + nextLine = false + } + break + } + endx += l.layout.Advances[rune] + rune++ + l.pos.X++ + } + offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()} + + // Calculate the width & height if the returned text. + // + // If there's a better way to do this, I'm all ears. + var d fixed.Int26_6 + for _, adv := range retLayout.Advances { + d += adv + } + size := image.Point{ + X: d.Ceil(), + Y: (l.line.Ascent + l.line.Descent).Ceil(), + } + + if nextLine { + l.pos.Y++ + l.pos.X = 0 + } else { + l.off.X = endx + } + + return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true + } + return text.Layout{}, image.Point{}, false, 0, image.Point{}, false +} + +func (l *segmentIterator) inSelection() bool { + return l.startSel.LessOrEqual(l.pos) && + l.pos.Less(l.endSel) +} + +func (p1 screenPos) LessOrEqual(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X) +} + +func (p1 screenPos) Less(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X) +} + +func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font, size unit.Value, txt string) layout.Dimensions { + cs := gtx.Constraints + textSize := fixed.I(gtx.Px(size)) + lines := s.LayoutString(font, textSize, cs.Max.X, txt) + if max := l.MaxLines; max > 0 && len(lines) > max { + lines = lines[:max] + } + dims := linesDimens(lines) + dims.Size = cs.Constrain(dims.Size) + cl := textPadding(lines) + cl.Max = cl.Max.Add(dims.Size) + it := segmentIterator{ + Lines: lines, + Clip: cl, + Alignment: l.Alignment, + Width: dims.Size.X, + } + for { + l, off, _, _, _, ok := it.Next() + if !ok { + break + } + stack := op.Save(gtx.Ops) + op.Offset(layout.FPt(off)).Add(gtx.Ops) + s.Shape(font, textSize, l).Add(gtx.Ops) + clip.Rect(cl.Sub(off)).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } + return dims +} + +func textPadding(lines []text.Line) (padding image.Rectangle) { + if len(lines) == 0 { + return + } + first := lines[0] + if d := first.Ascent + first.Bounds.Min.Y; d < 0 { + padding.Min.Y = d.Ceil() + } + last := lines[len(lines)-1] + if d := last.Bounds.Max.Y - last.Descent; d > 0 { + padding.Max.Y = d.Ceil() + } + if d := first.Bounds.Min.X; d < 0 { + padding.Min.X = d.Ceil() + } + if d := first.Bounds.Max.X - first.Width; d > 0 { + padding.Max.X = d.Ceil() + } + return +} + +func linesDimens(lines []text.Line) layout.Dimensions { + var width fixed.Int26_6 + var h int + var baseline int + if len(lines) > 0 { + baseline = lines[0].Ascent.Ceil() + var prevDesc fixed.Int26_6 + for _, l := range lines { + h += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if l.Width > width { + width = l.Width + } + } + h += lines[len(lines)-1].Descent.Ceil() + } + w := width.Ceil() + return layout.Dimensions{ + Size: image.Point{ + X: w, + Y: h, + }, + Baseline: h - baseline, + } +} + +func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { + mw := fixed.I(maxWidth) + switch align { + case text.Middle: + return fixed.I(((mw - width) / 2).Floor()) + case text.End: + return fixed.I((mw - width).Floor()) + case text.Start: + return 0 + default: + panic(fmt.Errorf("unknown alignment %v", align)) + } +} diff --git a/pkg/gel/gio/widget/material/button.go b/pkg/gel/gio/widget/material/button.go new file mode 100644 index 0000000..986c40f --- /dev/null +++ b/pkg/gel/gio/widget/material/button.go @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + "math" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type ButtonStyle struct { + Text string + // Color is the text color. + Color color.NRGBA + Font text.Font + TextSize unit.Value + Background color.NRGBA + CornerRadius unit.Value + Inset layout.Inset + Button *widget.Clickable + shaper text.Shaper +} + +type ButtonLayoutStyle struct { + Background color.NRGBA + CornerRadius unit.Value + Button *widget.Clickable +} + +type IconButtonStyle struct { + Background color.NRGBA + // Color is the icon color. + Color color.NRGBA + Icon *widget.Icon + // Size is the icon size. + Size unit.Value + Inset layout.Inset + Button *widget.Clickable +} + +func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle { + return ButtonStyle{ + Text: txt, + Color: th.Palette.ContrastFg, + CornerRadius: unit.Dp(4), + Background: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Inset: layout.Inset{ + Top: unit.Dp(10), Bottom: unit.Dp(10), + Left: unit.Dp(12), Right: unit.Dp(12), + }, + Button: button, + shaper: th.Shaper, + } +} + +func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle { + return ButtonLayoutStyle{ + Button: button, + Background: th.Palette.ContrastBg, + CornerRadius: unit.Dp(4), + } +} + +func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon) IconButtonStyle { + return IconButtonStyle{ + Background: th.Palette.ContrastBg, + Color: th.Palette.ContrastFg, + Icon: icon, + Size: unit.Dp(24), + Inset: layout.UniformInset(unit.Dp(12)), + Button: button, + } +} + +// Clickable lays out a rectangular clickable widget without further +// decoration. +func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions { + return layout.Stack{}.Layout(gtx, + layout.Expanded(button.Layout), + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops) + for _, c := range button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(w), + ) +} + +func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + return ButtonLayoutStyle{ + Background: b.Background, + CornerRadius: b.CornerRadius, + Button: b.Button, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: b.Color}.Add(gtx.Ops) + return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text) + }) + }) +} + +func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { + min := gtx.Constraints.Min + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + rr := float32(gtx.Px(b.CornerRadius)) + clip.UniformRRect(f32.Rectangle{Max: f32.Point{ + X: float32(gtx.Constraints.Min.X), + Y: float32(gtx.Constraints.Min.Y), + }}, rr).Add(gtx.Ops) + background := b.Background + switch { + case gtx.Queue == nil: + background = f32color.Disabled(b.Background) + case b.Button.Hovered(): + background = f32color.Hovered(b.Background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Min = min + return layout.Center.Layout(gtx, w) + }), + layout.Expanded(b.Button.Layout), + ) +} + +func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y + sizexf, sizeyf := float32(sizex), float32(sizey) + rr := (sizexf + sizeyf) * .25 + clip.UniformRRect(f32.Rectangle{ + Max: f32.Point{X: sizexf, Y: sizeyf}, + }, rr).Add(gtx.Ops) + background := b.Background + switch { + case gtx.Queue == nil: + background = f32color.Disabled(b.Background) + case b.Button.Hovered(): + background = f32color.Hovered(b.Background) + } + paint.Fill(gtx.Ops, background) + for _, c := range b.Button.History() { + drawInk(gtx, c) + } + return layout.Dimensions{Size: gtx.Constraints.Min} + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(b.Size) + if b.Icon != nil { + b.Icon.Color = b.Color + b.Icon.Layout(gtx, unit.Px(float32(size))) + } + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }), + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + return b.Button.Layout(gtx) + }), + ) +} + +func drawInk(gtx layout.Context, c widget.Press) { + // duration is the number of seconds for the + // completed animation: expand while fading in, then + // out. + const ( + expandDuration = float32(0.5) + fadeDuration = float32(0.9) + ) + + now := gtx.Now + + t := float32(now.Sub(c.Start).Seconds()) + + end := c.End + if end.IsZero() { + // If the press hasn't ended, don't fade-out. + end = now + } + + endt := float32(end.Sub(c.Start).Seconds()) + + // Compute the fade-in/out position in [0;1]. + var alphat float32 + { + var haste float32 + if c.Cancelled { + // If the press was cancelled before the inkwell + // was fully faded in, fast forward the animation + // to match the fade-out. + if h := 0.5 - endt/fadeDuration; h > 0 { + haste = h + } + } + // Fade in. + half1 := t/fadeDuration + haste + if half1 > 0.5 { + half1 = 0.5 + } + + // Fade out. + half2 := float32(now.Sub(end).Seconds()) + half2 /= fadeDuration + half2 += haste + if half2 > 0.5 { + // Too old. + return + } + + alphat = half1 + half2 + } + + // Compute the expand position in [0;1]. + sizet := t + if c.Cancelled { + // Freeze expansion of cancelled presses. + sizet = endt + } + sizet /= expandDuration + + // Animate only ended presses, and presses that are fading in. + if !c.End.IsZero() || sizet <= 1.0 { + op.InvalidateOp{}.Add(gtx.Ops) + } + + if sizet > 1.0 { + sizet = 1.0 + } + + if alphat > .5 { + // Start fadeout after half the animation. + alphat = 1.0 - alphat + } + // Twice the speed to attain fully faded in at 0.5. + t2 := alphat * 2 + // Beziér ease-in curve. + alphaBezier := t2 * t2 * (3.0 - 2.0*t2) + sizeBezier := sizet * sizet * (3.0 - 2.0*sizet) + size := float32(gtx.Constraints.Min.X) + if h := float32(gtx.Constraints.Min.Y); h > size { + size = h + } + // Cover the entire constraints min rectangle. + size *= 2 * float32(math.Sqrt(2)) + // Apply curve values to size and color. + size *= sizeBezier + alpha := 0.7 * alphaBezier + const col = 0.8 + ba, bc := byte(alpha*0xff), byte(col*0xff) + defer op.Save(gtx.Ops).Load() + rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba) + ink := paint.ColorOp{Color: rgba} + ink.Add(gtx.Ops) + rr := size * .5 + op.Offset(c.Position.Add(f32.Point{ + X: -rr, + Y: -rr, + })).Add(gtx.Ops) + clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) +} diff --git a/pkg/gel/gio/widget/material/checkable.go b/pkg/gel/gio/widget/material/checkable.go new file mode 100644 index 0000000..9d2640d --- /dev/null +++ b/pkg/gel/gio/widget/material/checkable.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type checkable struct { + Label string + Color color.NRGBA + Font text.Font + TextSize unit.Value + IconColor color.NRGBA + Size unit.Value + shaper text.Shaper + checkedStateIcon *widget.Icon + uncheckedStateIcon *widget.Icon +} + +func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dimensions { + var icon *widget.Icon + if checked { + icon = c.checkedStateIcon + } else { + icon = c.uncheckedStateIcon + } + + dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Stack{Alignment: layout.Center}.Layout(gtx, + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(c.Size) * 4 / 3 + dims := layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + if !hovered { + return dims + } + + background := f32color.MulAlpha(c.IconColor, 70) + + radius := float32(size) / 2 + paint.FillShape(gtx.Ops, background, + clip.Circle{ + Center: f32.Point{X: radius, Y: radius}, + Radius: radius, + }.Op(gtx.Ops)) + + return dims + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + size := gtx.Px(c.Size) + icon.Color = c.IconColor + if gtx.Queue == nil { + icon.Color = f32color.Disabled(icon.Color) + } + icon.Layout(gtx, unit.Px(float32(size))) + return layout.Dimensions{ + Size: image.Point{X: size, Y: size}, + } + }) + }), + ) + }), + + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: c.Color}.Add(gtx.Ops) + return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label) + }) + }), + ) + pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops) + return dims +} diff --git a/pkg/gel/gio/widget/material/checkbox.go b/pkg/gel/gio/widget/material/checkbox.go new file mode 100644 index 0000000..cd41852 --- /dev/null +++ b/pkg/gel/gio/widget/material/checkbox.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type CheckBoxStyle struct { + checkable + CheckBox *widget.Bool +} + +func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle { + return CheckBoxStyle{ + CheckBox: checkBox, + checkable: checkable{ + Label: label, + Color: th.Palette.Fg, + IconColor: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Size: unit.Dp(26), + shaper: th.Shaper, + checkedStateIcon: th.Icon.CheckBoxChecked, + uncheckedStateIcon: th.Icon.CheckBoxUnchecked, + }, + } +} + +// Layout updates the checkBox and displays it. +func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { + dims := c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered()) + gtx.Constraints.Min = dims.Size + c.CheckBox.Layout(gtx) + return dims +} diff --git a/pkg/gel/gio/widget/material/doc.go b/pkg/gel/gio/widget/material/doc.go new file mode 100644 index 0000000..715f5a0 --- /dev/null +++ b/pkg/gel/gio/widget/material/doc.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Package material implements the Material design. +// +// To maximize reusability and visual flexibility, user interface controls are +// split into two parts: the stateful widget and the stateless drawing of it. +// +// For example, widget.Clickable encapsulates the state and event +// handling of all clickable areas, while the Theme is responsible to +// draw a specific area, for example a button. +// +// This snippet defines a button that prints a message when clicked: +// +// var gtx layout.Context +// button := new(widget.Clickable) +// +// for button.Clicked(gtx) { +// fmt.Println("Clicked!") +// } +// +// Use a Theme to draw the button: +// +// theme := material.NewTheme(...) +// +// material.Button(theme, "Click me!").Layout(gtx, button) +// +// Customization +// +// Quite often, a program needs to customize the theme-provided defaults. Several +// options are available, depending on the nature of the change. +// +// Mandatory parameters: Some parameters are not part of the widget state but +// have no obvious default. In the program above, the button text is a +// parameter to the Theme.Button method. +// +// Theme-global parameters: For changing the look of all widgets drawn with a +// particular theme, adjust the `Theme` fields: +// +// theme.Color.Primary = color.NRGBA{...} +// +// Widget-local parameters: For changing the look of a particular widget, +// adjust the widget specific theme object: +// +// btn := material.Button(theme, "Click me!") +// btn.Font.Style = text.Italic +// btn.Layout(gtx, button) +// +// Widget variants: A widget can have several distinct representations even +// though the underlying state is the same. A widget.Clickable can be drawn as a +// round icon button: +// +// icon := material.NewIcon(...) +// +// material.IconButton(theme, icon).Layout(gtx, button) +// +// Specialized widgets: Theme both define a generic Label method +// that takes a text size, and specialized methods for standard text +// sizes such as Theme.H1 and Theme.Body2. +package material diff --git a/pkg/gel/gio/widget/material/editor.go b/pkg/gel/gio/widget/material/editor.go new file mode 100644 index 0000000..8998dc1 --- /dev/null +++ b/pkg/gel/gio/widget/material/editor.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type EditorStyle struct { + Font text.Font + TextSize unit.Value + // Color is the text color. + Color color.NRGBA + // Hint contains the text displayed when the editor is empty. + Hint string + // HintColor is the color of hint text. + HintColor color.NRGBA + // SelectionColor is the color of the background for selected text. + SelectionColor color.NRGBA + Editor *widget.Editor + + shaper text.Shaper +} + +func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { + return EditorStyle{ + Editor: editor, + TextSize: th.TextSize, + Color: th.Palette.Fg, + shaper: th.Shaper, + Hint: hint, + HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb), + SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60), + } +} + +func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { + defer op.Save(gtx.Ops).Load() + macro := op.Record(gtx.Ops) + paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) + var maxlines int + if e.Editor.SingleLine { + maxlines = 1 + } + tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines} + dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint) + call := macro.Stop() + if w := dims.Size.X; gtx.Constraints.Min.X < w { + gtx.Constraints.Min.X = w + } + if h := dims.Size.Y; gtx.Constraints.Min.Y < h { + gtx.Constraints.Min.Y = h + } + dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize) + disabled := gtx.Queue == nil + if e.Editor.Len() > 0 { + paint.ColorOp{Color: blendDisabledColor(disabled, e.SelectionColor)}.Add(gtx.Ops) + e.Editor.PaintSelection(gtx) + paint.ColorOp{Color: blendDisabledColor(disabled, e.Color)}.Add(gtx.Ops) + e.Editor.PaintText(gtx) + } else { + call.Add(gtx.Ops) + } + if !disabled { + paint.ColorOp{Color: e.Color}.Add(gtx.Ops) + e.Editor.PaintCaret(gtx) + } + return dims +} + +func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA { + if disabled { + return f32color.Disabled(c) + } + return c +} diff --git a/pkg/gel/gio/widget/material/label.go b/pkg/gel/gio/widget/material/label.go new file mode 100644 index 0000000..80f73c2 --- /dev/null +++ b/pkg/gel/gio/widget/material/label.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type LabelStyle struct { + // Face defines the text style. + Font text.Font + // Color is the text color. + Color color.NRGBA + // Alignment specify the text alignment. + Alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + MaxLines int + Text string + TextSize unit.Value + + shaper text.Shaper +} + +func H1(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(96.0/16.0), txt) +} + +func H2(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(60.0/16.0), txt) +} + +func H3(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(48.0/16.0), txt) +} + +func H4(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(34.0/16.0), txt) +} + +func H5(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(24.0/16.0), txt) +} + +func H6(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(20.0/16.0), txt) +} + +func Body1(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize, txt) +} + +func Body2(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(14.0/16.0), txt) +} + +func Caption(th *Theme, txt string) LabelStyle { + return Label(th, th.TextSize.Scale(12.0/16.0), txt) +} + +func Label(th *Theme, size unit.Value, txt string) LabelStyle { + return LabelStyle{ + Text: txt, + Color: th.Palette.Fg, + TextSize: size, + shaper: th.Shaper, + } +} + +func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { + paint.ColorOp{Color: l.Color}.Add(gtx.Ops) + tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines} + return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text) +} diff --git a/pkg/gel/gio/widget/material/loader.go b/pkg/gel/gio/widget/material/loader.go new file mode 100644 index 0000000..bd8d72a --- /dev/null +++ b/pkg/gel/gio/widget/material/loader.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + "math" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +type LoaderStyle struct { + Color color.NRGBA +} + +func Loader(th *Theme) LoaderStyle { + return LoaderStyle{ + Color: th.Palette.ContrastBg, + } +} + +func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions { + diam := gtx.Constraints.Min.X + if minY := gtx.Constraints.Min.Y; minY > diam { + diam = minY + } + if diam == 0 { + diam = gtx.Px(unit.Dp(24)) + } + sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) + radius := float64(sz.X) * .5 + defer op.Save(gtx.Ops).Load() + op.Offset(f32.Pt(float32(radius), float32(radius))).Add(gtx.Ops) + + dt := (time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds() + startAngle := dt * math.Pi * 2 + endAngle := startAngle + math.Pi*1.5 + + clipLoader(gtx.Ops, startAngle, endAngle, radius) + paint.ColorOp{ + Color: l.Color, + }.Add(gtx.Ops) + op.Offset(f32.Pt(-float32(radius), -float32(radius))).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + op.InvalidateOp{}.Add(gtx.Ops) + return layout.Dimensions{ + Size: sz, + } +} + +func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) { + const thickness = .25 + + var ( + width = float32(radius * thickness) + delta = float32(endAngle - startAngle) + + vy, vx = math.Sincos(startAngle) + + pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius)) + center = f32.Pt(0, 0).Sub(pen) + + p clip.Path + ) + + p.Begin(ops) + p.Move(pen) + p.Arc(center, center, delta) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: width, + Cap: clip.FlatCap, + }, + }.Op().Add(ops) +} diff --git a/pkg/gel/gio/widget/material/progressbar.go b/pkg/gel/gio/widget/material/progressbar.go new file mode 100644 index 0000000..5116d1b --- /dev/null +++ b/pkg/gel/gio/widget/material/progressbar.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +type ProgressBarStyle struct { + Color color.NRGBA + TrackColor color.NRGBA + Progress float32 +} + +func ProgressBar(th *Theme, progress float32) ProgressBarStyle { + return ProgressBarStyle{ + Progress: progress, + Color: th.Palette.ContrastBg, + TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88), + } +} + +func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions { + shader := func(width float32, color color.NRGBA) layout.Dimensions { + maxHeight := unit.Dp(4) + rr := float32(gtx.Px(unit.Dp(2))) + + d := image.Point{X: int(width), Y: gtx.Px(maxHeight)} + + height := float32(gtx.Px(maxHeight)) + clip.UniformRRect(f32.Rectangle{Max: f32.Pt(width, height)}, rr).Add(gtx.Ops) + paint.ColorOp{Color: color}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return layout.Dimensions{Size: d} + } + + progressBarWidth := float32(gtx.Constraints.Max.X) + return layout.Stack{Alignment: layout.W}.Layout(gtx, + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return shader(progressBarWidth, p.TrackColor) + }), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + fillWidth := progressBarWidth * clamp1(p.Progress) + fillColor := p.Color + if gtx.Queue == nil { + fillColor = f32color.Disabled(fillColor) + } + return shader(fillWidth, fillColor) + }), + ) +} + +// clamp1 limits v to range [0..1]. +func clamp1(v float32) float32 { + if v >= 1 { + return 1 + } else if v <= 0 { + return 0 + } else { + return v + } +} diff --git a/pkg/gel/gio/widget/material/radiobutton.go b/pkg/gel/gio/widget/material/radiobutton.go new file mode 100644 index 0000000..f7ca7ed --- /dev/null +++ b/pkg/gel/gio/widget/material/radiobutton.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type RadioButtonStyle struct { + checkable + Key string + Group *widget.Enum +} + +// RadioButton returns a RadioButton with a label. The key specifies +// the value for the Enum. +func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonStyle { + return RadioButtonStyle{ + Group: group, + checkable: checkable{ + Label: label, + + Color: th.Palette.Fg, + IconColor: th.Palette.ContrastBg, + TextSize: th.TextSize.Scale(14.0 / 16.0), + Size: unit.Dp(26), + shaper: th.Shaper, + checkedStateIcon: th.Icon.RadioChecked, + uncheckedStateIcon: th.Icon.RadioUnchecked, + }, + Key: key, + } +} + +// Layout updates enum and displays the radio button. +func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions { + hovered, hovering := r.Group.Hovered() + dims := r.layout(gtx, r.Group.Value == r.Key, hovering && hovered == r.Key) + gtx.Constraints.Min = dims.Size + r.Group.Layout(gtx, r.Key) + return dims +} diff --git a/pkg/gel/gio/widget/material/slider.go b/pkg/gel/gio/widget/material/slider.go new file mode 100644 index 0000000..352d9f0 --- /dev/null +++ b/pkg/gel/gio/widget/material/slider.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +// Slider is for selecting a value in a range. +func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle { + return SliderStyle{ + Min: min, + Max: max, + Color: th.Palette.ContrastBg, + Float: float, + FingerSize: th.FingerSize, + } +} + +type SliderStyle struct { + Min, Max float32 + Color color.NRGBA + Float *widget.Float + + FingerSize unit.Value +} + +func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions { + thumbRadius := gtx.Px(unit.Dp(6)) + trackWidth := gtx.Px(unit.Dp(2)) + + axis := s.Float.Axis + // Keep a minimum length so that the track is always visible. + minLength := thumbRadius + 3*thumbRadius + thumbRadius + // Try to expand to finger size, but only if the constraints + // allow for it. + touchSizePx := min(gtx.Px(s.FingerSize), axis.Convert(gtx.Constraints.Max).Y) + sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength) + sizeCross := max(2*thumbRadius, touchSizePx) + size := axis.Convert(image.Pt(sizeMain, sizeCross)) + + st := op.Save(gtx.Ops) + o := axis.Convert(image.Pt(thumbRadius, 0)) + op.Offset(layout.FPt(o)).Add(gtx.Ops) + gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*thumbRadius, sizeCross)) + s.Float.Layout(gtx, thumbRadius, s.Min, s.Max) + gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, sizeCross))) + thumbPos := thumbRadius + int(s.Float.Pos()) + st.Load() + + color := s.Color + if gtx.Queue == nil { + color = f32color.Disabled(color) + } + + // Draw track before thumb. + st = op.Save(gtx.Ops) + track := image.Rectangle{ + Min: axis.Convert(image.Pt(thumbRadius, sizeCross/2-trackWidth/2)), + Max: axis.Convert(image.Pt(thumbPos, sizeCross/2+trackWidth/2)), + } + clip.Rect(track).Add(gtx.Ops) + paint.Fill(gtx.Ops, color) + st.Load() + + // Draw track after thumb. + st = op.Save(gtx.Ops) + track = image.Rectangle{ + Min: axis.Convert(image.Pt(thumbPos, axis.Convert(track.Min).Y)), + Max: axis.Convert(image.Pt(sizeMain-thumbRadius, axis.Convert(track.Max).Y)), + } + clip.Rect(track).Add(gtx.Ops) + paint.Fill(gtx.Ops, f32color.MulAlpha(color, 96)) + st.Load() + + // Draw thumb. + pt := axis.Convert(image.Pt(thumbPos, sizeCross/2)) + paint.FillShape(gtx.Ops, color, + clip.Circle{ + Center: f32.Point{X: float32(pt.X), Y: float32(pt.Y)}, + Radius: float32(thumbRadius), + }.Op(gtx.Ops)) + + return layout.Dimensions{Size: size} +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/pkg/gel/gio/widget/material/switch.go b/pkg/gel/gio/widget/material/switch.go new file mode 100644 index 0000000..9089105 --- /dev/null +++ b/pkg/gel/gio/widget/material/switch.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/internal/f32color" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +type SwitchStyle struct { + Color struct { + Enabled color.NRGBA + Disabled color.NRGBA + Track color.NRGBA + } + Switch *widget.Bool +} + +// Switch is for selecting a boolean value. +func Switch(th *Theme, swtch *widget.Bool) SwitchStyle { + sw := SwitchStyle{ + Switch: swtch, + } + sw.Color.Enabled = th.Palette.ContrastBg + sw.Color.Disabled = th.Palette.Bg + sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88) + return sw +} + +// Layout updates the switch and displays it. +func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions { + trackWidth := gtx.Px(unit.Dp(36)) + trackHeight := gtx.Px(unit.Dp(16)) + thumbSize := gtx.Px(unit.Dp(20)) + trackOff := float32(thumbSize-trackHeight) * .5 + + // Draw track. + stack := op.Save(gtx.Ops) + trackCorner := float32(trackHeight) / 2 + trackRect := f32.Rectangle{Max: f32.Point{ + X: float32(trackWidth), + Y: float32(trackHeight), + }} + col := s.Color.Disabled + if s.Switch.Value { + col = s.Color.Enabled + } + if gtx.Queue == nil { + col = f32color.Disabled(col) + } + trackColor := s.Color.Track + op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops) + clip.UniformRRect(trackRect, trackCorner).Add(gtx.Ops) + paint.ColorOp{Color: trackColor}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + + // Draw thumb ink. + stack = op.Save(gtx.Ops) + inkSize := gtx.Px(unit.Dp(44)) + rr := float32(inkSize) * .5 + inkOff := f32.Point{ + X: float32(trackWidth)*.5 - rr, + Y: -rr + float32(trackHeight)*.5 + trackOff, + } + op.Offset(inkOff).Add(gtx.Ops) + gtx.Constraints.Min = image.Pt(inkSize, inkSize) + clip.UniformRRect(f32.Rectangle{Max: layout.FPt(gtx.Constraints.Min)}, rr).Add(gtx.Ops) + for _, p := range s.Switch.History() { + drawInk(gtx, p) + } + stack.Load() + + // Compute thumb offset and color. + stack = op.Save(gtx.Ops) + if s.Switch.Value { + off := trackWidth - thumbSize + op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops) + } + + thumbRadius := float32(thumbSize) / 2 + + // Draw hover. + if s.Switch.Hovered() { + r := 1.7 * thumbRadius + background := f32color.MulAlpha(s.Color.Enabled, 70) + paint.FillShape(gtx.Ops, background, + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius}, + Radius: r, + }.Op(gtx.Ops)) + } + + // Draw thumb shadow, a translucent disc slightly larger than the + // thumb itself. + // Center shadow horizontally and slightly adjust its Y. + paint.FillShape(gtx.Ops, argb(0x55000000), + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius + .25}, + Radius: thumbRadius + 1, + }.Op(gtx.Ops)) + + // Draw thumb. + paint.FillShape(gtx.Ops, col, + clip.Circle{ + Center: f32.Point{X: thumbRadius, Y: thumbRadius}, + Radius: thumbRadius, + }.Op(gtx.Ops)) + + // Set up click area. + stack = op.Save(gtx.Ops) + clickSize := gtx.Px(unit.Dp(40)) + clickOff := f32.Point{ + X: (float32(trackWidth) - float32(clickSize)) * .5, + Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff, + } + op.Offset(clickOff).Add(gtx.Ops) + sz := image.Pt(clickSize, clickSize) + pointer.Ellipse(image.Rectangle{Max: sz}).Add(gtx.Ops) + gtx.Constraints.Min = sz + s.Switch.Layout(gtx) + stack.Load() + + dims := image.Point{X: trackWidth, Y: thumbSize} + return layout.Dimensions{Size: dims} +} diff --git a/pkg/gel/gio/widget/material/theme.go b/pkg/gel/gio/widget/material/theme.go new file mode 100644 index 0000000..13f7c36 --- /dev/null +++ b/pkg/gel/gio/widget/material/theme.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package material + +import ( + "image/color" + + "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/gel/gio/widget" +) + +// Palette contains the minimal set of colors that a widget may need to +// draw itself. +type Palette struct { + // Bg is the background color atop which content is currently being + // drawn. + Bg color.NRGBA + + // Fg is a color suitable for drawing on top of Bg. + Fg color.NRGBA + + // ContrastBg is a color used to draw attention to active, + // important, interactive widgets such as buttons. + ContrastBg color.NRGBA + + // ContrastFg is a color suitable for content drawn on top of + // ContrastBg. + ContrastFg color.NRGBA +} + +type Theme struct { + Shaper text.Shaper + Palette + TextSize unit.Value + Icon struct { + CheckBoxChecked *widget.Icon + CheckBoxUnchecked *widget.Icon + RadioChecked *widget.Icon + RadioUnchecked *widget.Icon + } + + // FingerSize is the minimum touch target size. + FingerSize unit.Value +} + +func NewTheme(fontCollection []text.FontFace) *Theme { + t := &Theme{ + Shaper: text.NewCache(fontCollection), + } + t.Palette = Palette{ + Fg: rgb(0x000000), + Bg: rgb(0xffffff), + ContrastBg: rgb(0x3f51b5), + ContrastFg: rgb(0xffffff), + } + t.TextSize = unit.Sp(16) + + t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox)) + t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) + t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked)) + t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) + + // 38dp is on the lower end of possible finger size. + t.FingerSize = unit.Dp(38) + + return t +} + +func (t Theme) WithPalette(p Palette) Theme { + t.Palette = p + return t +} + +func mustIcon(ic *widget.Icon, err error) *widget.Icon { + if err != nil { + panic(err) + } + return ic +} + +func rgb(c uint32) color.NRGBA { + return argb(0xff000000 | c) +} + +func argb(c uint32) color.NRGBA { + return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} +} diff --git a/pkg/gel/helpers.go b/pkg/gel/helpers.go new file mode 100644 index 0000000..815d662 --- /dev/null +++ b/pkg/gel/helpers.go @@ -0,0 +1,235 @@ +package gel + +import ( + "errors" + "image" + "image/color" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/system" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/text" +) + +// Defining these as types gives flexibility later to create methods that modify them + +type Fonts map[string]text.Typeface +type Icons map[string]*Icon +type Collection []text.FontFace + +func (c Collection) Font(font string) (out text.Font, e error) { + for i := range c { + if c[i].Font.Typeface == text.Typeface(font) { + out = c[i].Font + return + } + } + return out, errors.New("font " + font + " not found") + +} + +const Inf = 1e6 + +// Fill is a general fill function that covers the background of the current context space +func Fill(gtx l.Context, col color.NRGBA) l.Dimensions { + cs := gtx.Constraints + d := cs.Min + // dr := f32.Rectangle{ + // Max: f32.Point{X: float32(d.X), Y: float32(d.Y)}, + // } + // paint.ColorOp{Color: col}.Add(gtx.Ops) + // paint.PaintOp{Rect: dr}.Add(gtx.Ops) + fill(gtx, col, d, 0, 0) + return l.Dimensions{Size: d} +} + +// func (th *Theme) GetFont(font string) *text.Font { +// for i := range th.collection { +// if th.collection[i].Font.Typeface == text.Typeface(font) { +// return &th.collection[i].Font +// } +// } +// return nil +// } + +// func rgb(c uint32) color.RGBA { +// return argb(0xff000000 | c) +// } + +func argb(c uint32) color.RGBA { + return color.RGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} +} + +// FPt converts an point to a f32.Point. +func Fpt(p image.Point) f32.Point { + return f32.Point{ + X: float32(p.X), Y: float32(p.Y), + } +} + +func axisPoint(a l.Axis, main, cross int) image.Point { + if a == l.Horizontal { + return image.Point{X: main, Y: cross} + } else { + return image.Point{X: cross, Y: main} + } +} + +// axisConvert a point in (x, y) coordinates to (main, cross) coordinates, +// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged +// for the horizontal axis, or (y, x) for the vertical axis. +func axisConvert(a l.Axis, pt image.Point) image.Point { + if a == l.Horizontal { + return pt + } + return image.Pt(pt.Y, pt.X) +} + +func axisMain(a l.Axis, sz image.Point) int { + if a == l.Horizontal { + return sz.X + } + return sz.Y +} + +func axisCross(a l.Axis, sz image.Point) int { + if a == l.Horizontal { + return sz.Y + } + return sz.X +} + +func axisMainConstraint(a l.Axis, cs l.Constraints) (int, int) { + if a == l.Horizontal { + return cs.Min.X, cs.Max.X + } + return cs.Min.Y, cs.Max.Y +} + +func axisCrossConstraint(a l.Axis, cs l.Constraints) (int, int) { + if a == l.Horizontal { + return cs.Min.Y, cs.Max.Y + } + return cs.Min.X, cs.Max.X +} + +func axisConstraints(a l.Axis, mainMin, mainMax, crossMin, crossMax int) l.Constraints { + if a == l.Horizontal { + return l.Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)} + } + return l.Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)} +} + +func EmptySpace(x, y int) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{ + Size: image.Point{ + X: x, + Y: y, + }, + } + } +} + +func EmptyFromSize(size image.Point) func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{ + Size: size, + } + } +} + +func EmptyMaxWidth() func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{ + Size: image.Point{X: gtx.Constraints.Max.X, Y: gtx.Constraints.Min.Y}, + Baseline: 0, + } + } +} +func EmptyMaxHeight() func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{Size: image.Point{X: gtx.Constraints.Min.X, Y: gtx.Constraints.Min.Y}} + } +} + +func EmptyMinWidth() func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{ + Size: image.Point{X: gtx.Constraints.Min.X, Y: gtx.Constraints.Min.Y}, + Baseline: 0, + } + } +} +func EmptyMinHeight() func(gtx l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + return l.Dimensions{Size: image.Point{Y: gtx.Constraints.Min.Y}} + } +} + +// CopyContextDimensionsWithMaxAxis copies the dimensions out with the max set by an image.Point along the axis +func CopyContextDimensionsWithMaxAxis(gtx l.Context, axis l.Axis) l.Context { + ip := image.Point{} + if axis == l.Horizontal { + ip.Y = gtx.Constraints.Max.Y + ip.X = gtx.Constraints.Max.X + } else { + ip.Y = gtx.Constraints.Max.Y + ip.X = gtx.Constraints.Max.X + } + var ops op.Ops + gtx1 := l.NewContext( + &ops, system.FrameEvent{ + Now: time.Now(), + Metric: gtx.Metric, + Size: ip, + }, + ) + if axis == l.Horizontal { + gtx1.Constraints.Min.X = 0 + gtx1.Constraints.Min.Y = gtx.Constraints.Min.Y + } else { + gtx1.Constraints.Min.X = gtx.Constraints.Min.X + gtx1.Constraints.Min.Y = 0 + } + return gtx1 +} + +// GetInfContext creates a context with infinite max constraints +func GetInfContext(gtx l.Context) l.Context { + ip := image.Point{} + ip.Y = Inf + ip.X = Inf + var ops op.Ops + gtx1 := l.NewContext( + &ops, system.FrameEvent{ + Now: time.Now(), + Metric: gtx.Metric, + Size: ip, + }, + ) + return gtx1 +} + +func If(value bool, t, f l.Widget) l.Widget { + if value { + return t + } else { + return f + } +} + +func (th *Theme) SliceToWidget(w []l.Widget, axis l.Axis) l.Widget { + var out *Flex + if axis == l.Horizontal { + out = th.Flex().AlignStart() + } else { + out = th.VFlex().AlignStart() + } + for i := range w { + out.Rigid(w[i]) + } + return out.Fn +} diff --git a/pkg/gel/icon.go b/pkg/gel/icon.go new file mode 100644 index 0000000..4954d15 --- /dev/null +++ b/pkg/gel/icon.go @@ -0,0 +1,112 @@ +package gel + +import ( + "image" + "image/color" + "image/draw" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + "golang.org/x/exp/shiny/iconvg" +) + +type Icon struct { + *Window + color string + src *[]byte + size unit.Value + // Cached values. + sz int + op paint.ImageOp + imgSize int + imgColor string +} + +type IconByColor map[color.NRGBA]paint.ImageOp +type IconBySize map[float32]IconByColor +type IconCache map[*[]byte]IconBySize + +// Icon returns a new Icon from iconVG data. +func (w *Window) Icon() *Icon { + return &Icon{Window: w, size: w.TextSize, color: "DocText"} +} + +// Color sets the color of the icon image. It must be called before creating the image +func (i *Icon) Color(color string) *Icon { + i.color = color + return i +} + +// Src sets the icon source to draw from +func (i *Icon) Src(data *[]byte) *Icon { + _, e := iconvg.DecodeMetadata(*data) + if E.Chk(e) { + D.Ln("no image data, crashing") + panic(e) + // return nil + } + i.src = data + return i +} + +// Scale changes the size relative to the base font size +func (i *Icon) Scale(scale float32) *Icon { + i.size = i.Theme.TextSize.Scale(scale) + return i +} + +func (i *Icon) Size(size unit.Value) *Icon { + i.size = size + return i +} + +// Fn renders the icon +func (i *Icon) Fn(gtx l.Context) l.Dimensions { + ico := i.image(gtx.Px(i.size)) + if i.src == nil { + panic("icon is nil") + } + ico.Add(gtx.Ops) + paint.PaintOp{ + // Rect: f32.Rectangle{ + // Max: Fpt(ico.Size()), + // }, + }.Add(gtx.Ops) + return l.Dimensions{Size: ico.Size()} +} + +func (i *Icon) image(sz int) paint.ImageOp { + // if sz == i.imgSize && i.color == i.imgColor { + // // D.Ln("reusing old icon") + // return i.op + // } + if ico, ok := i.Theme.iconCache[i.src]; ok { + if isz, ok := ico[i.size.V]; ok { + if icl, ok := isz[i.Theme.Colors.GetNRGBAFromName(i.color)]; ok { + return icl + } + } + } + m, _ := iconvg.DecodeMetadata(*i.src) + dx, dy := m.ViewBox.AspectRatio() + img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, + Y: int(float32(sz) * dy / dx)}}) + var ico iconvg.Rasterizer + ico.SetDstImage(img, img.Bounds(), draw.Src) + m.Palette[0] = color.RGBA(i.Theme.Colors.GetNRGBAFromName(i.color)) + if e := iconvg.Decode(&ico, *i.src, &iconvg.DecodeOptions{ + Palette: &m.Palette, + }); E.Chk(e) { + } + operation := paint.NewImageOp(img) + // create the maps if they don't exist + if _, ok := i.Theme.iconCache[i.src]; !ok { + i.Theme.iconCache[i.src] = make(IconBySize) + } + if _, ok := i.Theme.iconCache[i.src][i.size.V]; !ok { + i.Theme.iconCache[i.src][i.size.V] = make(IconByColor) + } + i.Theme.iconCache[i.src][i.size.V][i.Theme.Colors.GetNRGBAFromName(i.color)] = operation + return operation +} diff --git a/pkg/gel/iconbutton.go b/pkg/gel/iconbutton.go new file mode 100644 index 0000000..4b83b34 --- /dev/null +++ b/pkg/gel/iconbutton.go @@ -0,0 +1,132 @@ +package gel + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/unit" + "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gel/f32color" +) + +type IconButton struct { + *Window + background string + // Color is the icon color. + color string + icon *Icon + // Size is the icon size. + size unit.Value + inset *Inset + button *Clickable + corners int +} + +// IconButton creates an icon with a circular *optional non-round corners* background and an icon placed in the centre +func (w *Window) IconButton(button *Clickable) *IconButton { + return &IconButton{ + Window: w, + background: "Primary", + color: "DocBg", + size: w.TextSize, + inset: w.Inset(0.33, nil), + button: button, + icon: w.Icon().Src(&icons.AlertError), + } +} + +// Corners sets the corners that will be circular +func (b *IconButton) Corners(corners int) *IconButton { + b.corners = corners + return b +} + +// Background sets the color of the circular background +func (b *IconButton) Background(color string) *IconButton { + b.background = color + return b +} + +// Color sets the color of the icon +func (b *IconButton) Color(color string) *IconButton { + b.color = color + return b +} + +// Icon sets the icon to display +func (b *IconButton) Icon(ic *Icon) *IconButton { + b.icon = ic + return b +} + +// Scale changes the size of the icon as a ratio of the base font size +func (b *IconButton) Scale(scale float32) *IconButton { + b.size = b.Theme.TextSize.Scale(scale * 0.72) + return b +} + +// ButtonInset sets the size of inset that goes in between the button background +// and the icon +func (b *IconButton) ButtonInset(inset float32) *IconButton { + b.inset = b.Inset(inset, b.button.Fn) + return b +} + +// SetClick sets the function to run on click +func (b *IconButton) SetClick(fn func()) *IconButton { + b.button.SetClick(fn) + return b +} + +// SetPress sets the function to run on press +func (b *IconButton) SetPress(fn func()) *IconButton { + b.button.SetPress(fn) + return b +} + +// SetCancel sets the function to run on cancel (click but release outside) +func (b *IconButton) SetCancel(fn func()) *IconButton { + b.button.SetCancel(fn) + return b +} + +// Fn renders the icon button +func (b *IconButton) Fn(gtx l.Context) l.Dimensions { + return b.Stack().Expanded( + func(gtx l.Context) l.Dimensions { + sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y + sizexf, sizeyf := float32(sizex), float32(sizey) + rr := (sizexf + sizeyf) * .25 + clip.RRect{ + Rect: f32.Rectangle{Max: f32.Point{X: sizexf, Y: sizeyf}}, + NE: ifDir(rr, b.corners&NE), + NW: ifDir(rr, b.corners&NW), + SE: ifDir(rr, b.corners&SE), + SW: ifDir(rr, b.corners&SW), + }.Add(gtx.Ops) + background := b.Theme.Colors.GetNRGBAFromName(b.background) + if gtx.Queue == nil { + background = f32color.MulAlpha(background, 150) + } + var dims l.Dimensions + if b.background != "" { + dims = Fill(gtx, background) + } + for _, c := range b.button.History() { + drawInk(gtx, c) + } + return dims + }, + ).Stacked( + b.inset.Embed(b.icon.Fn).Fn, + ).Expanded( + func(gtx l.Context) l.Dimensions { + pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops) + return b.button.Fn(gtx) + }, + ).Fn(gtx) +} diff --git a/pkg/gel/icons/icongen/logmain.go b/pkg/gel/icons/icongen/logmain.go new file mode 100644 index 0000000..58579ab --- /dev/null +++ b/pkg/gel/icons/icongen/logmain.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/pkg/gel/icons/icongen/main.go b/pkg/gel/icons/icongen/main.go new file mode 100644 index 0000000..176adde --- /dev/null +++ b/pkg/gel/icons/icongen/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "go/format" + "io/ioutil" + "net/http" + "strings" + + "github.com/p9c/p9/pkg/apputil" +) + +func main() { + src := getSourceCode("icons", getIcons()) + filename := "../icons/icons.go" + apputil.EnsureDir(filename) + var e error + if e = ioutil.WriteFile(filename, src, 0600); E.Chk(e) { + panic(e) + } +} + +func getIcons() (iconNames []string) { + url := "https://raw.githubusercontent.com/golang/exp/54ebac48fca0f39f9b63e0112b50a168ee5b5c00/shiny/materialdesign/icons/data.go" + var e error + var r *http.Response + if r, e = http.Get(url); E.Chk(e) { + panic(e) + } + var b []byte + if b, e = ioutil.ReadAll(r.Body); E.Chk(e) { + panic(e) + } + if e = r.Body.Close(); E.Chk(e) { + panic(e) + } + s := string(b) + split := strings.Split(s, "var ") + iconNames = make([]string, len(split)) + for i, x := range split[1:] { + split2 := strings.Split(x, " ") + iconNames[i] = split2[0] + } + return iconNames +} + +func getSourceCode(packagename string, iconNames []string) []byte { + o := `// Package icons bundles the entire set of several icon sets into one package as maps to allow iteration + +`+`//go:generate go run ./icongen/. + +package ` + packagename + ` + + +import ( + "golang.org/x/exp/shiny/materialdesign/icons" +) + +var Material = map[string]*[]byte { +` + for i := range iconNames { + if iconNames[i] == "" { + continue + } + o += "\t" + `"` + iconNames[i] + `": &icons.` + iconNames[i] + ",\n" + } + o += "}\n" + // I.Ln(o) + var e error + var out []byte + if out, e = format.Source([]byte(o)); e != nil { + panic(e) + } + return out +} diff --git a/pkg/gel/icons/icons.go b/pkg/gel/icons/icons.go new file mode 100644 index 0000000..3f5e180 --- /dev/null +++ b/pkg/gel/icons/icons.go @@ -0,0 +1,973 @@ +// Package icons bundles the entire set of several icon sets into one package as maps to allow iteration + +//go:generate go run ./icongen/. + +package icons + +import ( + "golang.org/x/exp/shiny/materialdesign/icons" +) + +var Material = map[string]*[]byte{ + "Action3DRotation": &icons.Action3DRotation, + "ActionAccessibility": &icons.ActionAccessibility, + "ActionAccessible": &icons.ActionAccessible, + "ActionAccountBalance": &icons.ActionAccountBalance, + "ActionAccountBalanceWallet": &icons.ActionAccountBalanceWallet, + "ActionAccountBox": &icons.ActionAccountBox, + "ActionAccountCircle": &icons.ActionAccountCircle, + "ActionAddShoppingCart": &icons.ActionAddShoppingCart, + "ActionAlarm": &icons.ActionAlarm, + "ActionAlarmAdd": &icons.ActionAlarmAdd, + "ActionAlarmOff": &icons.ActionAlarmOff, + "ActionAlarmOn": &icons.ActionAlarmOn, + "ActionAllOut": &icons.ActionAllOut, + "ActionAndroid": &icons.ActionAndroid, + "ActionAnnouncement": &icons.ActionAnnouncement, + "ActionAspectRatio": &icons.ActionAspectRatio, + "ActionAssessment": &icons.ActionAssessment, + "ActionAssignment": &icons.ActionAssignment, + "ActionAssignmentInd": &icons.ActionAssignmentInd, + "ActionAssignmentLate": &icons.ActionAssignmentLate, + "ActionAssignmentReturn": &icons.ActionAssignmentReturn, + "ActionAssignmentReturned": &icons.ActionAssignmentReturned, + "ActionAssignmentTurnedIn": &icons.ActionAssignmentTurnedIn, + "ActionAutorenew": &icons.ActionAutorenew, + "ActionBackup": &icons.ActionBackup, + "ActionBook": &icons.ActionBook, + "ActionBookmark": &icons.ActionBookmark, + "ActionBookmarkBorder": &icons.ActionBookmarkBorder, + "ActionBugReport": &icons.ActionBugReport, + "ActionBuild": &icons.ActionBuild, + "ActionCached": &icons.ActionCached, + "ActionCameraEnhance": &icons.ActionCameraEnhance, + "ActionCardGiftcard": &icons.ActionCardGiftcard, + "ActionCardMembership": &icons.ActionCardMembership, + "ActionCardTravel": &icons.ActionCardTravel, + "ActionChangeHistory": &icons.ActionChangeHistory, + "ActionCheckCircle": &icons.ActionCheckCircle, + "ActionChromeReaderMode": &icons.ActionChromeReaderMode, + "ActionClass": &icons.ActionClass, + "ActionCode": &icons.ActionCode, + "ActionCompareArrows": &icons.ActionCompareArrows, + "ActionCopyright": &icons.ActionCopyright, + "ActionCreditCard": &icons.ActionCreditCard, + "ActionDashboard": &icons.ActionDashboard, + "ActionDateRange": &icons.ActionDateRange, + "ActionDelete": &icons.ActionDelete, + "ActionDeleteForever": &icons.ActionDeleteForever, + "ActionDescription": &icons.ActionDescription, + "ActionDNS": &icons.ActionDNS, + "ActionDone": &icons.ActionDone, + "ActionDoneAll": &icons.ActionDoneAll, + "ActionDonutLarge": &icons.ActionDonutLarge, + "ActionDonutSmall": &icons.ActionDonutSmall, + "ActionEject": &icons.ActionEject, + "ActionEuroSymbol": &icons.ActionEuroSymbol, + "ActionEvent": &icons.ActionEvent, + "ActionEventSeat": &icons.ActionEventSeat, + "ActionExitToApp": &icons.ActionExitToApp, + "ActionExplore": &icons.ActionExplore, + "ActionExtension": &icons.ActionExtension, + "ActionFace": &icons.ActionFace, + "ActionFavorite": &icons.ActionFavorite, + "ActionFavoriteBorder": &icons.ActionFavoriteBorder, + "ActionFeedback": &icons.ActionFeedback, + "ActionFindInPage": &icons.ActionFindInPage, + "ActionFindReplace": &icons.ActionFindReplace, + "ActionFingerprint": &icons.ActionFingerprint, + "ActionFlightLand": &icons.ActionFlightLand, + "ActionFlightTakeoff": &icons.ActionFlightTakeoff, + "ActionFlipToBack": &icons.ActionFlipToBack, + "ActionFlipToFront": &icons.ActionFlipToFront, + "ActionGTranslate": &icons.ActionGTranslate, + "ActionGavel": &icons.ActionGavel, + "ActionGetApp": &icons.ActionGetApp, + "ActionGIF": &icons.ActionGIF, + "ActionGrade": &icons.ActionGrade, + "ActionGroupWork": &icons.ActionGroupWork, + "ActionHelp": &icons.ActionHelp, + "ActionHelpOutline": &icons.ActionHelpOutline, + "ActionHighlightOff": &icons.ActionHighlightOff, + "ActionHistory": &icons.ActionHistory, + "ActionHome": &icons.ActionHome, + "ActionHourglassEmpty": &icons.ActionHourglassEmpty, + "ActionHourglassFull": &icons.ActionHourglassFull, + "ActionHTTP": &icons.ActionHTTP, + "ActionHTTPS": &icons.ActionHTTPS, + "ActionImportantDevices": &icons.ActionImportantDevices, + "ActionInfo": &icons.ActionInfo, + "ActionInfoOutline": &icons.ActionInfoOutline, + "ActionInput": &icons.ActionInput, + "ActionInvertColors": &icons.ActionInvertColors, + "ActionLabel": &icons.ActionLabel, + "ActionLabelOutline": &icons.ActionLabelOutline, + "ActionLanguage": &icons.ActionLanguage, + "ActionLaunch": &icons.ActionLaunch, + "ActionLightbulbOutline": &icons.ActionLightbulbOutline, + "ActionLineStyle": &icons.ActionLineStyle, + "ActionLineWeight": &icons.ActionLineWeight, + "ActionList": &icons.ActionList, + "ActionLock": &icons.ActionLock, + "ActionLockOpen": &icons.ActionLockOpen, + "ActionLockOutline": &icons.ActionLockOutline, + "ActionLoyalty": &icons.ActionLoyalty, + "ActionMarkUnreadMailbox": &icons.ActionMarkUnreadMailbox, + "ActionMotorcycle": &icons.ActionMotorcycle, + "ActionNoteAdd": &icons.ActionNoteAdd, + "ActionOfflinePin": &icons.ActionOfflinePin, + "ActionOpacity": &icons.ActionOpacity, + "ActionOpenInBrowser": &icons.ActionOpenInBrowser, + "ActionOpenInNew": &icons.ActionOpenInNew, + "ActionOpenWith": &icons.ActionOpenWith, + "ActionPageview": &icons.ActionPageview, + "ActionPanTool": &icons.ActionPanTool, + "ActionPayment": &icons.ActionPayment, + "ActionPermCameraMic": &icons.ActionPermCameraMic, + "ActionPermContactCalendar": &icons.ActionPermContactCalendar, + "ActionPermDataSetting": &icons.ActionPermDataSetting, + "ActionPermDeviceInformation": &icons.ActionPermDeviceInformation, + "ActionPermIdentity": &icons.ActionPermIdentity, + "ActionPermMedia": &icons.ActionPermMedia, + "ActionPermPhoneMsg": &icons.ActionPermPhoneMsg, + "ActionPermScanWiFi": &icons.ActionPermScanWiFi, + "ActionPets": &icons.ActionPets, + "ActionPictureInPicture": &icons.ActionPictureInPicture, + "ActionPictureInPictureAlt": &icons.ActionPictureInPictureAlt, + "ActionPlayForWork": &icons.ActionPlayForWork, + "ActionPolymer": &icons.ActionPolymer, + "ActionPowerSettingsNew": &icons.ActionPowerSettingsNew, + "ActionPregnantWoman": &icons.ActionPregnantWoman, + "ActionPrint": &icons.ActionPrint, + "ActionQueryBuilder": &icons.ActionQueryBuilder, + "ActionQuestionAnswer": &icons.ActionQuestionAnswer, + "ActionReceipt": &icons.ActionReceipt, + "ActionRecordVoiceOver": &icons.ActionRecordVoiceOver, + "ActionRedeem": &icons.ActionRedeem, + "ActionRemoveShoppingCart": &icons.ActionRemoveShoppingCart, + "ActionReorder": &icons.ActionReorder, + "ActionReportProblem": &icons.ActionReportProblem, + "ActionRestore": &icons.ActionRestore, + "ActionRestorePage": &icons.ActionRestorePage, + "ActionRoom": &icons.ActionRoom, + "ActionRoundedCorner": &icons.ActionRoundedCorner, + "ActionRowing": &icons.ActionRowing, + "ActionSchedule": &icons.ActionSchedule, + "ActionSearch": &icons.ActionSearch, + "ActionSettings": &icons.ActionSettings, + "ActionSettingsApplications": &icons.ActionSettingsApplications, + "ActionSettingsBackupRestore": &icons.ActionSettingsBackupRestore, + "ActionSettingsBluetooth": &icons.ActionSettingsBluetooth, + "ActionSettingsBrightness": &icons.ActionSettingsBrightness, + "ActionSettingsCell": &icons.ActionSettingsCell, + "ActionSettingsEthernet": &icons.ActionSettingsEthernet, + "ActionSettingsInputAntenna": &icons.ActionSettingsInputAntenna, + "ActionSettingsInputComponent": &icons.ActionSettingsInputComponent, + "ActionSettingsInputComposite": &icons.ActionSettingsInputComposite, + "ActionSettingsInputHDMI": &icons.ActionSettingsInputHDMI, + "ActionSettingsInputSVideo": &icons.ActionSettingsInputSVideo, + "ActionSettingsOverscan": &icons.ActionSettingsOverscan, + "ActionSettingsPhone": &icons.ActionSettingsPhone, + "ActionSettingsPower": &icons.ActionSettingsPower, + "ActionSettingsRemote": &icons.ActionSettingsRemote, + "ActionSettingsVoice": &icons.ActionSettingsVoice, + "ActionShop": &icons.ActionShop, + "ActionShopTwo": &icons.ActionShopTwo, + "ActionShoppingBasket": &icons.ActionShoppingBasket, + "ActionShoppingCart": &icons.ActionShoppingCart, + "ActionSpeakerNotes": &icons.ActionSpeakerNotes, + "ActionSpeakerNotesOff": &icons.ActionSpeakerNotesOff, + "ActionSpellcheck": &icons.ActionSpellcheck, + "ActionStarRate": &icons.ActionStarRate, + "ActionStars": &icons.ActionStars, + "ActionStore": &icons.ActionStore, + "ActionSubject": &icons.ActionSubject, + "ActionSupervisorAccount": &icons.ActionSupervisorAccount, + "ActionSwapHoriz": &icons.ActionSwapHoriz, + "ActionSwapVert": &icons.ActionSwapVert, + "ActionSwapVerticalCircle": &icons.ActionSwapVerticalCircle, + "ActionSystemUpdateAlt": &icons.ActionSystemUpdateAlt, + "ActionTab": &icons.ActionTab, + "ActionTabUnselected": &icons.ActionTabUnselected, + "ActionTheaters": &icons.ActionTheaters, + "ActionThumbDown": &icons.ActionThumbDown, + "ActionThumbUp": &icons.ActionThumbUp, + "ActionThumbsUpDown": &icons.ActionThumbsUpDown, + "ActionTimeline": &icons.ActionTimeline, + "ActionTOC": &icons.ActionTOC, + "ActionToday": &icons.ActionToday, + "ActionToll": &icons.ActionToll, + "ActionTouchApp": &icons.ActionTouchApp, + "ActionTrackChanges": &icons.ActionTrackChanges, + "ActionTranslate": &icons.ActionTranslate, + "ActionTrendingDown": &icons.ActionTrendingDown, + "ActionTrendingFlat": &icons.ActionTrendingFlat, + "ActionTrendingUp": &icons.ActionTrendingUp, + "ActionTurnedIn": &icons.ActionTurnedIn, + "ActionTurnedInNot": &icons.ActionTurnedInNot, + "ActionUpdate": &icons.ActionUpdate, + "ActionVerifiedUser": &icons.ActionVerifiedUser, + "ActionViewAgenda": &icons.ActionViewAgenda, + "ActionViewArray": &icons.ActionViewArray, + "ActionViewCarousel": &icons.ActionViewCarousel, + "ActionViewColumn": &icons.ActionViewColumn, + "ActionViewDay": &icons.ActionViewDay, + "ActionViewHeadline": &icons.ActionViewHeadline, + "ActionViewList": &icons.ActionViewList, + "ActionViewModule": &icons.ActionViewModule, + "ActionViewQuilt": &icons.ActionViewQuilt, + "ActionViewStream": &icons.ActionViewStream, + "ActionViewWeek": &icons.ActionViewWeek, + "ActionVisibility": &icons.ActionVisibility, + "ActionVisibilityOff": &icons.ActionVisibilityOff, + "ActionWatchLater": &icons.ActionWatchLater, + "ActionWork": &icons.ActionWork, + "ActionYoutubeSearchedFor": &icons.ActionYoutubeSearchedFor, + "ActionZoomIn": &icons.ActionZoomIn, + "ActionZoomOut": &icons.ActionZoomOut, + "AlertAddAlert": &icons.AlertAddAlert, + "AlertError": &icons.AlertError, + "AlertErrorOutline": &icons.AlertErrorOutline, + "AlertWarning": &icons.AlertWarning, + "AVAddToQueue": &icons.AVAddToQueue, + "AVAirplay": &icons.AVAirplay, + "AVAlbum": &icons.AVAlbum, + "AVArtTrack": &icons.AVArtTrack, + "AVAVTimer": &icons.AVAVTimer, + "AVBrandingWatermark": &icons.AVBrandingWatermark, + "AVCallToAction": &icons.AVCallToAction, + "AVClosedCaption": &icons.AVClosedCaption, + "AVEqualizer": &icons.AVEqualizer, + "AVExplicit": &icons.AVExplicit, + "AVFastForward": &icons.AVFastForward, + "AVFastRewind": &icons.AVFastRewind, + "AVFeaturedPlayList": &icons.AVFeaturedPlayList, + "AVFeaturedVideo": &icons.AVFeaturedVideo, + "AVFiberDVR": &icons.AVFiberDVR, + "AVFiberManualRecord": &icons.AVFiberManualRecord, + "AVFiberNew": &icons.AVFiberNew, + "AVFiberPin": &icons.AVFiberPin, + "AVFiberSmartRecord": &icons.AVFiberSmartRecord, + "AVForward10": &icons.AVForward10, + "AVForward30": &icons.AVForward30, + "AVForward5": &icons.AVForward5, + "AVGames": &icons.AVGames, + "AVHD": &icons.AVHD, + "AVHearing": &icons.AVHearing, + "AVHighQuality": &icons.AVHighQuality, + "AVLibraryAdd": &icons.AVLibraryAdd, + "AVLibraryBooks": &icons.AVLibraryBooks, + "AVLibraryMusic": &icons.AVLibraryMusic, + "AVLoop": &icons.AVLoop, + "AVMic": &icons.AVMic, + "AVMicNone": &icons.AVMicNone, + "AVMicOff": &icons.AVMicOff, + "AVMovie": &icons.AVMovie, + "AVMusicVideo": &icons.AVMusicVideo, + "AVNewReleases": &icons.AVNewReleases, + "AVNotInterested": &icons.AVNotInterested, + "AVNote": &icons.AVNote, + "AVPause": &icons.AVPause, + "AVPauseCircleFilled": &icons.AVPauseCircleFilled, + "AVPauseCircleOutline": &icons.AVPauseCircleOutline, + "AVPlayArrow": &icons.AVPlayArrow, + "AVPlayCircleFilled": &icons.AVPlayCircleFilled, + "AVPlayCircleOutline": &icons.AVPlayCircleOutline, + "AVPlaylistAdd": &icons.AVPlaylistAdd, + "AVPlaylistAddCheck": &icons.AVPlaylistAddCheck, + "AVPlaylistPlay": &icons.AVPlaylistPlay, + "AVQueue": &icons.AVQueue, + "AVQueueMusic": &icons.AVQueueMusic, + "AVQueuePlayNext": &icons.AVQueuePlayNext, + "AVRadio": &icons.AVRadio, + "AVRecentActors": &icons.AVRecentActors, + "AVRemoveFromQueue": &icons.AVRemoveFromQueue, + "AVRepeat": &icons.AVRepeat, + "AVRepeatOne": &icons.AVRepeatOne, + "AVReplay": &icons.AVReplay, + "AVReplay10": &icons.AVReplay10, + "AVReplay30": &icons.AVReplay30, + "AVReplay5": &icons.AVReplay5, + "AVShuffle": &icons.AVShuffle, + "AVSkipNext": &icons.AVSkipNext, + "AVSkipPrevious": &icons.AVSkipPrevious, + "AVSlowMotionVideo": &icons.AVSlowMotionVideo, + "AVSnooze": &icons.AVSnooze, + "AVSortByAlpha": &icons.AVSortByAlpha, + "AVStop": &icons.AVStop, + "AVSubscriptions": &icons.AVSubscriptions, + "AVSubtitles": &icons.AVSubtitles, + "AVSurroundSound": &icons.AVSurroundSound, + "AVVideoCall": &icons.AVVideoCall, + "AVVideoLabel": &icons.AVVideoLabel, + "AVVideoLibrary": &icons.AVVideoLibrary, + "AVVideocam": &icons.AVVideocam, + "AVVideocamOff": &icons.AVVideocamOff, + "AVVolumeDown": &icons.AVVolumeDown, + "AVVolumeMute": &icons.AVVolumeMute, + "AVVolumeOff": &icons.AVVolumeOff, + "AVVolumeUp": &icons.AVVolumeUp, + "AVWeb": &icons.AVWeb, + "AVWebAsset": &icons.AVWebAsset, + "CommunicationBusiness": &icons.CommunicationBusiness, + "CommunicationCall": &icons.CommunicationCall, + "CommunicationCallEnd": &icons.CommunicationCallEnd, + "CommunicationCallMade": &icons.CommunicationCallMade, + "CommunicationCallMerge": &icons.CommunicationCallMerge, + "CommunicationCallMissed": &icons.CommunicationCallMissed, + "CommunicationCallMissedOutgoing": &icons.CommunicationCallMissedOutgoing, + "CommunicationCallReceived": &icons.CommunicationCallReceived, + "CommunicationCallSplit": &icons.CommunicationCallSplit, + "CommunicationChat": &icons.CommunicationChat, + "CommunicationChatBubble": &icons.CommunicationChatBubble, + "CommunicationChatBubbleOutline": &icons.CommunicationChatBubbleOutline, + "CommunicationClearAll": &icons.CommunicationClearAll, + "CommunicationComment": &icons.CommunicationComment, + "CommunicationContactMail": &icons.CommunicationContactMail, + "CommunicationContactPhone": &icons.CommunicationContactPhone, + "CommunicationContacts": &icons.CommunicationContacts, + "CommunicationDialerSIP": &icons.CommunicationDialerSIP, + "CommunicationDialpad": &icons.CommunicationDialpad, + "CommunicationEmail": &icons.CommunicationEmail, + "CommunicationForum": &icons.CommunicationForum, + "CommunicationImportContacts": &icons.CommunicationImportContacts, + "CommunicationImportExport": &icons.CommunicationImportExport, + "CommunicationInvertColorsOff": &icons.CommunicationInvertColorsOff, + "CommunicationLiveHelp": &icons.CommunicationLiveHelp, + "CommunicationLocationOff": &icons.CommunicationLocationOff, + "CommunicationLocationOn": &icons.CommunicationLocationOn, + "CommunicationMailOutline": &icons.CommunicationMailOutline, + "CommunicationMessage": &icons.CommunicationMessage, + "CommunicationNoSIM": &icons.CommunicationNoSIM, + "CommunicationPhone": &icons.CommunicationPhone, + "CommunicationPhoneLinkErase": &icons.CommunicationPhoneLinkErase, + "CommunicationPhoneLinkLock": &icons.CommunicationPhoneLinkLock, + "CommunicationPhoneLinkRing": &icons.CommunicationPhoneLinkRing, + "CommunicationPhoneLinkSetup": &icons.CommunicationPhoneLinkSetup, + "CommunicationPortableWiFiOff": &icons.CommunicationPortableWiFiOff, + "CommunicationPresentToAll": &icons.CommunicationPresentToAll, + "CommunicationRingVolume": &icons.CommunicationRingVolume, + "CommunicationRSSFeed": &icons.CommunicationRSSFeed, + "CommunicationScreenShare": &icons.CommunicationScreenShare, + "CommunicationSpeakerPhone": &icons.CommunicationSpeakerPhone, + "CommunicationStayCurrentLandscape": &icons.CommunicationStayCurrentLandscape, + "CommunicationStayCurrentPortrait": &icons.CommunicationStayCurrentPortrait, + "CommunicationStayPrimaryLandscape": &icons.CommunicationStayPrimaryLandscape, + "CommunicationStayPrimaryPortrait": &icons.CommunicationStayPrimaryPortrait, + "CommunicationStopScreenShare": &icons.CommunicationStopScreenShare, + "CommunicationSwapCalls": &icons.CommunicationSwapCalls, + "CommunicationTextSMS": &icons.CommunicationTextSMS, + "CommunicationVoicemail": &icons.CommunicationVoicemail, + "CommunicationVPNKey": &icons.CommunicationVPNKey, + "ContentAdd": &icons.ContentAdd, + "ContentAddBox": &icons.ContentAddBox, + "ContentAddCircle": &icons.ContentAddCircle, + "ContentAddCircleOutline": &icons.ContentAddCircleOutline, + "ContentArchive": &icons.ContentArchive, + "ContentBackspace": &icons.ContentBackspace, + "ContentBlock": &icons.ContentBlock, + "ContentClear": &icons.ContentClear, + "ContentContentCopy": &icons.ContentContentCopy, + "ContentContentCut": &icons.ContentContentCut, + "ContentContentPaste": &icons.ContentContentPaste, + "ContentCreate": &icons.ContentCreate, + "ContentDeleteSweep": &icons.ContentDeleteSweep, + "ContentDrafts": &icons.ContentDrafts, + "ContentFilterList": &icons.ContentFilterList, + "ContentFlag": &icons.ContentFlag, + "ContentFontDownload": &icons.ContentFontDownload, + "ContentForward": &icons.ContentForward, + "ContentGesture": &icons.ContentGesture, + "ContentInbox": &icons.ContentInbox, + "ContentLink": &icons.ContentLink, + "ContentLowPriority": &icons.ContentLowPriority, + "ContentMail": &icons.ContentMail, + "ContentMarkUnread": &icons.ContentMarkUnread, + "ContentMoveToInbox": &icons.ContentMoveToInbox, + "ContentNextWeek": &icons.ContentNextWeek, + "ContentRedo": &icons.ContentRedo, + "ContentRemove": &icons.ContentRemove, + "ContentRemoveCircle": &icons.ContentRemoveCircle, + "ContentRemoveCircleOutline": &icons.ContentRemoveCircleOutline, + "ContentReply": &icons.ContentReply, + "ContentReplyAll": &icons.ContentReplyAll, + "ContentReport": &icons.ContentReport, + "ContentSave": &icons.ContentSave, + "ContentSelectAll": &icons.ContentSelectAll, + "ContentSend": &icons.ContentSend, + "ContentSort": &icons.ContentSort, + "ContentTextFormat": &icons.ContentTextFormat, + "ContentUnarchive": &icons.ContentUnarchive, + "ContentUndo": &icons.ContentUndo, + "ContentWeekend": &icons.ContentWeekend, + "DeviceAccessAlarm": &icons.DeviceAccessAlarm, + "DeviceAccessAlarms": &icons.DeviceAccessAlarms, + "DeviceAccessTime": &icons.DeviceAccessTime, + "DeviceAddAlarm": &icons.DeviceAddAlarm, + "DeviceAirplaneModeActive": &icons.DeviceAirplaneModeActive, + "DeviceAirplaneModeInactive": &icons.DeviceAirplaneModeInactive, + "DeviceBattery20": &icons.DeviceBattery20, + "DeviceBattery30": &icons.DeviceBattery30, + "DeviceBattery50": &icons.DeviceBattery50, + "DeviceBattery60": &icons.DeviceBattery60, + "DeviceBattery80": &icons.DeviceBattery80, + "DeviceBattery90": &icons.DeviceBattery90, + "DeviceBatteryAlert": &icons.DeviceBatteryAlert, + "DeviceBatteryCharging20": &icons.DeviceBatteryCharging20, + "DeviceBatteryCharging30": &icons.DeviceBatteryCharging30, + "DeviceBatteryCharging50": &icons.DeviceBatteryCharging50, + "DeviceBatteryCharging60": &icons.DeviceBatteryCharging60, + "DeviceBatteryCharging80": &icons.DeviceBatteryCharging80, + "DeviceBatteryCharging90": &icons.DeviceBatteryCharging90, + "DeviceBatteryChargingFull": &icons.DeviceBatteryChargingFull, + "DeviceBatteryFull": &icons.DeviceBatteryFull, + "DeviceBatteryStd": &icons.DeviceBatteryStd, + "DeviceBatteryUnknown": &icons.DeviceBatteryUnknown, + "DeviceBluetooth": &icons.DeviceBluetooth, + "DeviceBluetoothConnected": &icons.DeviceBluetoothConnected, + "DeviceBluetoothDisabled": &icons.DeviceBluetoothDisabled, + "DeviceBluetoothSearching": &icons.DeviceBluetoothSearching, + "DeviceBrightnessAuto": &icons.DeviceBrightnessAuto, + "DeviceBrightnessHigh": &icons.DeviceBrightnessHigh, + "DeviceBrightnessLow": &icons.DeviceBrightnessLow, + "DeviceBrightnessMedium": &icons.DeviceBrightnessMedium, + "DeviceDataUsage": &icons.DeviceDataUsage, + "DeviceDeveloperMode": &icons.DeviceDeveloperMode, + "DeviceDevices": &icons.DeviceDevices, + "DeviceDVR": &icons.DeviceDVR, + "DeviceGPSFixed": &icons.DeviceGPSFixed, + "DeviceGPSNotFixed": &icons.DeviceGPSNotFixed, + "DeviceGPSOff": &icons.DeviceGPSOff, + "DeviceGraphicEq": &icons.DeviceGraphicEq, + "DeviceLocationDisabled": &icons.DeviceLocationDisabled, + "DeviceLocationSearching": &icons.DeviceLocationSearching, + "DeviceNetworkCell": &icons.DeviceNetworkCell, + "DeviceNetworkWiFi": &icons.DeviceNetworkWiFi, + "DeviceNFC": &icons.DeviceNFC, + "DeviceScreenLockLandscape": &icons.DeviceScreenLockLandscape, + "DeviceScreenLockPortrait": &icons.DeviceScreenLockPortrait, + "DeviceScreenLockRotation": &icons.DeviceScreenLockRotation, + "DeviceScreenRotation": &icons.DeviceScreenRotation, + "DeviceSDStorage": &icons.DeviceSDStorage, + "DeviceSettingsSystemDaydream": &icons.DeviceSettingsSystemDaydream, + "DeviceSignalCellular0Bar": &icons.DeviceSignalCellular0Bar, + "DeviceSignalCellular1Bar": &icons.DeviceSignalCellular1Bar, + "DeviceSignalCellular2Bar": &icons.DeviceSignalCellular2Bar, + "DeviceSignalCellular3Bar": &icons.DeviceSignalCellular3Bar, + "DeviceSignalCellular4Bar": &icons.DeviceSignalCellular4Bar, + "DeviceSignalCellularConnectedNoInternet0Bar": &icons.DeviceSignalCellularConnectedNoInternet0Bar, + "DeviceSignalCellularConnectedNoInternet1Bar": &icons.DeviceSignalCellularConnectedNoInternet1Bar, + "DeviceSignalCellularConnectedNoInternet2Bar": &icons.DeviceSignalCellularConnectedNoInternet2Bar, + "DeviceSignalCellularConnectedNoInternet3Bar": &icons.DeviceSignalCellularConnectedNoInternet3Bar, + "DeviceSignalCellularConnectedNoInternet4Bar": &icons.DeviceSignalCellularConnectedNoInternet4Bar, + "DeviceSignalCellularNoSIM": &icons.DeviceSignalCellularNoSIM, + "DeviceSignalCellularNull": &icons.DeviceSignalCellularNull, + "DeviceSignalCellularOff": &icons.DeviceSignalCellularOff, + "DeviceSignalWiFi0Bar": &icons.DeviceSignalWiFi0Bar, + "DeviceSignalWiFi1Bar": &icons.DeviceSignalWiFi1Bar, + "DeviceSignalWiFi1BarLock": &icons.DeviceSignalWiFi1BarLock, + "DeviceSignalWiFi2Bar": &icons.DeviceSignalWiFi2Bar, + "DeviceSignalWiFi2BarLock": &icons.DeviceSignalWiFi2BarLock, + "DeviceSignalWiFi3Bar": &icons.DeviceSignalWiFi3Bar, + "DeviceSignalWiFi3BarLock": &icons.DeviceSignalWiFi3BarLock, + "DeviceSignalWiFi4Bar": &icons.DeviceSignalWiFi4Bar, + "DeviceSignalWiFi4BarLock": &icons.DeviceSignalWiFi4BarLock, + "DeviceSignalWiFiOff": &icons.DeviceSignalWiFiOff, + "DeviceStorage": &icons.DeviceStorage, + "DeviceUSB": &icons.DeviceUSB, + "DeviceWallpaper": &icons.DeviceWallpaper, + "DeviceWidgets": &icons.DeviceWidgets, + "DeviceWiFiLock": &icons.DeviceWiFiLock, + "DeviceWiFiTethering": &icons.DeviceWiFiTethering, + "EditorAttachFile": &icons.EditorAttachFile, + "EditorAttachMoney": &icons.EditorAttachMoney, + "EditorBorderAll": &icons.EditorBorderAll, + "EditorBorderBottom": &icons.EditorBorderBottom, + "EditorBorderClear": &icons.EditorBorderClear, + "EditorBorderColor": &icons.EditorBorderColor, + "EditorBorderHorizontal": &icons.EditorBorderHorizontal, + "EditorBorderInner": &icons.EditorBorderInner, + "EditorBorderLeft": &icons.EditorBorderLeft, + "EditorBorderOuter": &icons.EditorBorderOuter, + "EditorBorderRight": &icons.EditorBorderRight, + "EditorBorderStyle": &icons.EditorBorderStyle, + "EditorBorderTop": &icons.EditorBorderTop, + "EditorBorderVertical": &icons.EditorBorderVertical, + "EditorBubbleChart": &icons.EditorBubbleChart, + "EditorDragHandle": &icons.EditorDragHandle, + "EditorFormatAlignCenter": &icons.EditorFormatAlignCenter, + "EditorFormatAlignJustify": &icons.EditorFormatAlignJustify, + "EditorFormatAlignLeft": &icons.EditorFormatAlignLeft, + "EditorFormatAlignRight": &icons.EditorFormatAlignRight, + "EditorFormatBold": &icons.EditorFormatBold, + "EditorFormatClear": &icons.EditorFormatClear, + "EditorFormatColorFill": &icons.EditorFormatColorFill, + "EditorFormatColorReset": &icons.EditorFormatColorReset, + "EditorFormatColorText": &icons.EditorFormatColorText, + "EditorFormatIndentDecrease": &icons.EditorFormatIndentDecrease, + "EditorFormatIndentIncrease": &icons.EditorFormatIndentIncrease, + "EditorFormatItalic": &icons.EditorFormatItalic, + "EditorFormatLineSpacing": &icons.EditorFormatLineSpacing, + "EditorFormatListBulleted": &icons.EditorFormatListBulleted, + "EditorFormatListNumbered": &icons.EditorFormatListNumbered, + "EditorFormatPaint": &icons.EditorFormatPaint, + "EditorFormatQuote": &icons.EditorFormatQuote, + "EditorFormatShapes": &icons.EditorFormatShapes, + "EditorFormatSize": &icons.EditorFormatSize, + "EditorFormatStrikethrough": &icons.EditorFormatStrikethrough, + "EditorFormatTextDirectionLToR": &icons.EditorFormatTextDirectionLToR, + "EditorFormatTextDirectionRToL": &icons.EditorFormatTextDirectionRToL, + "EditorFormatUnderlined": &icons.EditorFormatUnderlined, + "EditorFunctions": &icons.EditorFunctions, + "EditorHighlight": &icons.EditorHighlight, + "EditorInsertChart": &icons.EditorInsertChart, + "EditorInsertComment": &icons.EditorInsertComment, + "EditorInsertDriveFile": &icons.EditorInsertDriveFile, + "EditorInsertEmoticon": &icons.EditorInsertEmoticon, + "EditorInsertInvitation": &icons.EditorInsertInvitation, + "EditorInsertLink": &icons.EditorInsertLink, + "EditorInsertPhoto": &icons.EditorInsertPhoto, + "EditorLinearScale": &icons.EditorLinearScale, + "EditorMergeType": &icons.EditorMergeType, + "EditorModeComment": &icons.EditorModeComment, + "EditorModeEdit": &icons.EditorModeEdit, + "EditorMonetizationOn": &icons.EditorMonetizationOn, + "EditorMoneyOff": &icons.EditorMoneyOff, + "EditorMultilineChart": &icons.EditorMultilineChart, + "EditorPieChart": &icons.EditorPieChart, + "EditorPieChartOutlined": &icons.EditorPieChartOutlined, + "EditorPublish": &icons.EditorPublish, + "EditorShortText": &icons.EditorShortText, + "EditorShowChart": &icons.EditorShowChart, + "EditorSpaceBar": &icons.EditorSpaceBar, + "EditorStrikethroughS": &icons.EditorStrikethroughS, + "EditorTextFields": &icons.EditorTextFields, + "EditorTitle": &icons.EditorTitle, + "EditorVerticalAlignBottom": &icons.EditorVerticalAlignBottom, + "EditorVerticalAlignCenter": &icons.EditorVerticalAlignCenter, + "EditorVerticalAlignTop": &icons.EditorVerticalAlignTop, + "EditorWrapText": &icons.EditorWrapText, + "FileAttachment": &icons.FileAttachment, + "FileCloud": &icons.FileCloud, + "FileCloudCircle": &icons.FileCloudCircle, + "FileCloudDone": &icons.FileCloudDone, + "FileCloudDownload": &icons.FileCloudDownload, + "FileCloudOff": &icons.FileCloudOff, + "FileCloudQueue": &icons.FileCloudQueue, + "FileCloudUpload": &icons.FileCloudUpload, + "FileCreateNewFolder": &icons.FileCreateNewFolder, + "FileFileDownload": &icons.FileFileDownload, + "FileFileUpload": &icons.FileFileUpload, + "FileFolder": &icons.FileFolder, + "FileFolderOpen": &icons.FileFolderOpen, + "FileFolderShared": &icons.FileFolderShared, + "HardwareCast": &icons.HardwareCast, + "HardwareCastConnected": &icons.HardwareCastConnected, + "HardwareComputer": &icons.HardwareComputer, + "HardwareDesktopMac": &icons.HardwareDesktopMac, + "HardwareDesktopWindows": &icons.HardwareDesktopWindows, + "HardwareDeveloperBoard": &icons.HardwareDeveloperBoard, + "HardwareDeviceHub": &icons.HardwareDeviceHub, + "HardwareDevicesOther": &icons.HardwareDevicesOther, + "HardwareDock": &icons.HardwareDock, + "HardwareGamepad": &icons.HardwareGamepad, + "HardwareHeadset": &icons.HardwareHeadset, + "HardwareHeadsetMic": &icons.HardwareHeadsetMic, + "HardwareKeyboard": &icons.HardwareKeyboard, + "HardwareKeyboardArrowDown": &icons.HardwareKeyboardArrowDown, + "HardwareKeyboardArrowLeft": &icons.HardwareKeyboardArrowLeft, + "HardwareKeyboardArrowRight": &icons.HardwareKeyboardArrowRight, + "HardwareKeyboardArrowUp": &icons.HardwareKeyboardArrowUp, + "HardwareKeyboardBackspace": &icons.HardwareKeyboardBackspace, + "HardwareKeyboardCapslock": &icons.HardwareKeyboardCapslock, + "HardwareKeyboardHide": &icons.HardwareKeyboardHide, + "HardwareKeyboardReturn": &icons.HardwareKeyboardReturn, + "HardwareKeyboardTab": &icons.HardwareKeyboardTab, + "HardwareKeyboardVoice": &icons.HardwareKeyboardVoice, + "HardwareLaptop": &icons.HardwareLaptop, + "HardwareLaptopChromebook": &icons.HardwareLaptopChromebook, + "HardwareLaptopMac": &icons.HardwareLaptopMac, + "HardwareLaptopWindows": &icons.HardwareLaptopWindows, + "HardwareMemory": &icons.HardwareMemory, + "HardwareMouse": &icons.HardwareMouse, + "HardwarePhoneAndroid": &icons.HardwarePhoneAndroid, + "HardwarePhoneIPhone": &icons.HardwarePhoneIPhone, + "HardwarePhoneLink": &icons.HardwarePhoneLink, + "HardwarePhoneLinkOff": &icons.HardwarePhoneLinkOff, + "HardwarePowerInput": &icons.HardwarePowerInput, + "HardwareRouter": &icons.HardwareRouter, + "HardwareScanner": &icons.HardwareScanner, + "HardwareSecurity": &icons.HardwareSecurity, + "HardwareSIMCard": &icons.HardwareSIMCard, + "HardwareSmartphone": &icons.HardwareSmartphone, + "HardwareSpeaker": &icons.HardwareSpeaker, + "HardwareSpeakerGroup": &icons.HardwareSpeakerGroup, + "HardwareTablet": &icons.HardwareTablet, + "HardwareTabletAndroid": &icons.HardwareTabletAndroid, + "HardwareTabletMac": &icons.HardwareTabletMac, + "HardwareToys": &icons.HardwareToys, + "HardwareTV": &icons.HardwareTV, + "HardwareVideogameAsset": &icons.HardwareVideogameAsset, + "HardwareWatch": &icons.HardwareWatch, + "ImageAddAPhoto": &icons.ImageAddAPhoto, + "ImageAddToPhotos": &icons.ImageAddToPhotos, + "ImageAdjust": &icons.ImageAdjust, + "ImageAssistant": &icons.ImageAssistant, + "ImageAssistantPhoto": &icons.ImageAssistantPhoto, + "ImageAudiotrack": &icons.ImageAudiotrack, + "ImageBlurCircular": &icons.ImageBlurCircular, + "ImageBlurLinear": &icons.ImageBlurLinear, + "ImageBlurOff": &icons.ImageBlurOff, + "ImageBlurOn": &icons.ImageBlurOn, + "ImageBrightness1": &icons.ImageBrightness1, + "ImageBrightness2": &icons.ImageBrightness2, + "ImageBrightness3": &icons.ImageBrightness3, + "ImageBrightness4": &icons.ImageBrightness4, + "ImageBrightness5": &icons.ImageBrightness5, + "ImageBrightness6": &icons.ImageBrightness6, + "ImageBrightness7": &icons.ImageBrightness7, + "ImageBrokenImage": &icons.ImageBrokenImage, + "ImageBrush": &icons.ImageBrush, + "ImageBurstMode": &icons.ImageBurstMode, + "ImageCamera": &icons.ImageCamera, + "ImageCameraAlt": &icons.ImageCameraAlt, + "ImageCameraFront": &icons.ImageCameraFront, + "ImageCameraRear": &icons.ImageCameraRear, + "ImageCameraRoll": &icons.ImageCameraRoll, + "ImageCenterFocusStrong": &icons.ImageCenterFocusStrong, + "ImageCenterFocusWeak": &icons.ImageCenterFocusWeak, + "ImageCollections": &icons.ImageCollections, + "ImageCollectionsBookmark": &icons.ImageCollectionsBookmark, + "ImageColorLens": &icons.ImageColorLens, + "ImageColorize": &icons.ImageColorize, + "ImageCompare": &icons.ImageCompare, + "ImageControlPoint": &icons.ImageControlPoint, + "ImageControlPointDuplicate": &icons.ImageControlPointDuplicate, + "ImageCrop": &icons.ImageCrop, + "ImageCrop169": &icons.ImageCrop169, + "ImageCrop32": &icons.ImageCrop32, + "ImageCrop54": &icons.ImageCrop54, + "ImageCrop75": &icons.ImageCrop75, + "ImageCropDIN": &icons.ImageCropDIN, + "ImageCropFree": &icons.ImageCropFree, + "ImageCropLandscape": &icons.ImageCropLandscape, + "ImageCropOriginal": &icons.ImageCropOriginal, + "ImageCropPortrait": &icons.ImageCropPortrait, + "ImageCropRotate": &icons.ImageCropRotate, + "ImageCropSquare": &icons.ImageCropSquare, + "ImageDehaze": &icons.ImageDehaze, + "ImageDetails": &icons.ImageDetails, + "ImageEdit": &icons.ImageEdit, + "ImageExposure": &icons.ImageExposure, + "ImageExposureNeg1": &icons.ImageExposureNeg1, + "ImageExposureNeg2": &icons.ImageExposureNeg2, + "ImageExposurePlus1": &icons.ImageExposurePlus1, + "ImageExposurePlus2": &icons.ImageExposurePlus2, + "ImageExposureZero": &icons.ImageExposureZero, + "ImageFilter": &icons.ImageFilter, + "ImageFilter1": &icons.ImageFilter1, + "ImageFilter2": &icons.ImageFilter2, + "ImageFilter3": &icons.ImageFilter3, + "ImageFilter4": &icons.ImageFilter4, + "ImageFilter5": &icons.ImageFilter5, + "ImageFilter6": &icons.ImageFilter6, + "ImageFilter7": &icons.ImageFilter7, + "ImageFilter8": &icons.ImageFilter8, + "ImageFilter9": &icons.ImageFilter9, + "ImageFilter9Plus": &icons.ImageFilter9Plus, + "ImageFilterBAndW": &icons.ImageFilterBAndW, + "ImageFilterCenterFocus": &icons.ImageFilterCenterFocus, + "ImageFilterDrama": &icons.ImageFilterDrama, + "ImageFilterFrames": &icons.ImageFilterFrames, + "ImageFilterHDR": &icons.ImageFilterHDR, + "ImageFilterNone": &icons.ImageFilterNone, + "ImageFilterTiltShift": &icons.ImageFilterTiltShift, + "ImageFilterVintage": &icons.ImageFilterVintage, + "ImageFlare": &icons.ImageFlare, + "ImageFlashAuto": &icons.ImageFlashAuto, + "ImageFlashOff": &icons.ImageFlashOff, + "ImageFlashOn": &icons.ImageFlashOn, + "ImageFlip": &icons.ImageFlip, + "ImageGradient": &icons.ImageGradient, + "ImageGrain": &icons.ImageGrain, + "ImageGridOff": &icons.ImageGridOff, + "ImageGridOn": &icons.ImageGridOn, + "ImageHDROff": &icons.ImageHDROff, + "ImageHDROn": &icons.ImageHDROn, + "ImageHDRStrong": &icons.ImageHDRStrong, + "ImageHDRWeak": &icons.ImageHDRWeak, + "ImageHealing": &icons.ImageHealing, + "ImageImage": &icons.ImageImage, + "ImageImageAspectRatio": &icons.ImageImageAspectRatio, + "ImageISO": &icons.ImageISO, + "ImageLandscape": &icons.ImageLandscape, + "ImageLeakAdd": &icons.ImageLeakAdd, + "ImageLeakRemove": &icons.ImageLeakRemove, + "ImageLens": &icons.ImageLens, + "ImageLinkedCamera": &icons.ImageLinkedCamera, + "ImageLooks": &icons.ImageLooks, + "ImageLooks3": &icons.ImageLooks3, + "ImageLooks4": &icons.ImageLooks4, + "ImageLooks5": &icons.ImageLooks5, + "ImageLooks6": &icons.ImageLooks6, + "ImageLooksOne": &icons.ImageLooksOne, + "ImageLooksTwo": &icons.ImageLooksTwo, + "ImageLoupe": &icons.ImageLoupe, + "ImageMonochromePhotos": &icons.ImageMonochromePhotos, + "ImageMovieCreation": &icons.ImageMovieCreation, + "ImageMovieFilter": &icons.ImageMovieFilter, + "ImageMusicNote": &icons.ImageMusicNote, + "ImageNature": &icons.ImageNature, + "ImageNaturePeople": &icons.ImageNaturePeople, + "ImageNavigateBefore": &icons.ImageNavigateBefore, + "ImageNavigateNext": &icons.ImageNavigateNext, + "ImagePalette": &icons.ImagePalette, + "ImagePanorama": &icons.ImagePanorama, + "ImagePanoramaFishEye": &icons.ImagePanoramaFishEye, + "ImagePanoramaHorizontal": &icons.ImagePanoramaHorizontal, + "ImagePanoramaVertical": &icons.ImagePanoramaVertical, + "ImagePanoramaWideAngle": &icons.ImagePanoramaWideAngle, + "ImagePhoto": &icons.ImagePhoto, + "ImagePhotoAlbum": &icons.ImagePhotoAlbum, + "ImagePhotoCamera": &icons.ImagePhotoCamera, + "ImagePhotoFilter": &icons.ImagePhotoFilter, + "ImagePhotoLibrary": &icons.ImagePhotoLibrary, + "ImagePhotoSizeSelectActual": &icons.ImagePhotoSizeSelectActual, + "ImagePhotoSizeSelectLarge": &icons.ImagePhotoSizeSelectLarge, + "ImagePhotoSizeSelectSmall": &icons.ImagePhotoSizeSelectSmall, + "ImagePictureAsPDF": &icons.ImagePictureAsPDF, + "ImagePortrait": &icons.ImagePortrait, + "ImageRemoveRedEye": &icons.ImageRemoveRedEye, + "ImageRotate90DegreesCCW": &icons.ImageRotate90DegreesCCW, + "ImageRotateLeft": &icons.ImageRotateLeft, + "ImageRotateRight": &icons.ImageRotateRight, + "ImageSlideshow": &icons.ImageSlideshow, + "ImageStraighten": &icons.ImageStraighten, + "ImageStyle": &icons.ImageStyle, + "ImageSwitchCamera": &icons.ImageSwitchCamera, + "ImageSwitchVideo": &icons.ImageSwitchVideo, + "ImageTagFaces": &icons.ImageTagFaces, + "ImageTexture": &icons.ImageTexture, + "ImageTimeLapse": &icons.ImageTimeLapse, + "ImageTimer": &icons.ImageTimer, + "ImageTimer10": &icons.ImageTimer10, + "ImageTimer3": &icons.ImageTimer3, + "ImageTimerOff": &icons.ImageTimerOff, + "ImageTonality": &icons.ImageTonality, + "ImageTransform": &icons.ImageTransform, + "ImageTune": &icons.ImageTune, + "ImageViewComfy": &icons.ImageViewComfy, + "ImageViewCompact": &icons.ImageViewCompact, + "ImageVignette": &icons.ImageVignette, + "ImageWBAuto": &icons.ImageWBAuto, + "ImageWBCloudy": &icons.ImageWBCloudy, + "ImageWBIncandescent": &icons.ImageWBIncandescent, + "ImageWBIridescent": &icons.ImageWBIridescent, + "ImageWBSunny": &icons.ImageWBSunny, + "MapsAddLocation": &icons.MapsAddLocation, + "MapsBeenhere": &icons.MapsBeenhere, + "MapsDirections": &icons.MapsDirections, + "MapsDirectionsBike": &icons.MapsDirectionsBike, + "MapsDirectionsBoat": &icons.MapsDirectionsBoat, + "MapsDirectionsBus": &icons.MapsDirectionsBus, + "MapsDirectionsCar": &icons.MapsDirectionsCar, + "MapsDirectionsRailway": &icons.MapsDirectionsRailway, + "MapsDirectionsRun": &icons.MapsDirectionsRun, + "MapsDirectionsSubway": &icons.MapsDirectionsSubway, + "MapsDirectionsTransit": &icons.MapsDirectionsTransit, + "MapsDirectionsWalk": &icons.MapsDirectionsWalk, + "MapsEditLocation": &icons.MapsEditLocation, + "MapsEVStation": &icons.MapsEVStation, + "MapsFlight": &icons.MapsFlight, + "MapsHotel": &icons.MapsHotel, + "MapsLayers": &icons.MapsLayers, + "MapsLayersClear": &icons.MapsLayersClear, + "MapsLocalActivity": &icons.MapsLocalActivity, + "MapsLocalAirport": &icons.MapsLocalAirport, + "MapsLocalATM": &icons.MapsLocalATM, + "MapsLocalBar": &icons.MapsLocalBar, + "MapsLocalCafe": &icons.MapsLocalCafe, + "MapsLocalCarWash": &icons.MapsLocalCarWash, + "MapsLocalConvenienceStore": &icons.MapsLocalConvenienceStore, + "MapsLocalDining": &icons.MapsLocalDining, + "MapsLocalDrink": &icons.MapsLocalDrink, + "MapsLocalFlorist": &icons.MapsLocalFlorist, + "MapsLocalGasStation": &icons.MapsLocalGasStation, + "MapsLocalGroceryStore": &icons.MapsLocalGroceryStore, + "MapsLocalHospital": &icons.MapsLocalHospital, + "MapsLocalHotel": &icons.MapsLocalHotel, + "MapsLocalLaundryService": &icons.MapsLocalLaundryService, + "MapsLocalLibrary": &icons.MapsLocalLibrary, + "MapsLocalMall": &icons.MapsLocalMall, + "MapsLocalMovies": &icons.MapsLocalMovies, + "MapsLocalOffer": &icons.MapsLocalOffer, + "MapsLocalParking": &icons.MapsLocalParking, + "MapsLocalPharmacy": &icons.MapsLocalPharmacy, + "MapsLocalPhone": &icons.MapsLocalPhone, + "MapsLocalPizza": &icons.MapsLocalPizza, + "MapsLocalPlay": &icons.MapsLocalPlay, + "MapsLocalPostOffice": &icons.MapsLocalPostOffice, + "MapsLocalPrintshop": &icons.MapsLocalPrintshop, + "MapsLocalSee": &icons.MapsLocalSee, + "MapsLocalShipping": &icons.MapsLocalShipping, + "MapsLocalTaxi": &icons.MapsLocalTaxi, + "MapsMap": &icons.MapsMap, + "MapsMyLocation": &icons.MapsMyLocation, + "MapsNavigation": &icons.MapsNavigation, + "MapsNearMe": &icons.MapsNearMe, + "MapsPersonPin": &icons.MapsPersonPin, + "MapsPersonPinCircle": &icons.MapsPersonPinCircle, + "MapsPinDrop": &icons.MapsPinDrop, + "MapsPlace": &icons.MapsPlace, + "MapsRateReview": &icons.MapsRateReview, + "MapsRestaurant": &icons.MapsRestaurant, + "MapsRestaurantMenu": &icons.MapsRestaurantMenu, + "MapsSatellite": &icons.MapsSatellite, + "MapsStoreMallDirectory": &icons.MapsStoreMallDirectory, + "MapsStreetView": &icons.MapsStreetView, + "MapsSubway": &icons.MapsSubway, + "MapsTerrain": &icons.MapsTerrain, + "MapsTraffic": &icons.MapsTraffic, + "MapsTrain": &icons.MapsTrain, + "MapsTram": &icons.MapsTram, + "MapsTransferWithinAStation": &icons.MapsTransferWithinAStation, + "MapsZoomOutMap": &icons.MapsZoomOutMap, + "NavigationApps": &icons.NavigationApps, + "NavigationArrowBack": &icons.NavigationArrowBack, + "NavigationArrowDownward": &icons.NavigationArrowDownward, + "NavigationArrowDropDown": &icons.NavigationArrowDropDown, + "NavigationArrowDropDownCircle": &icons.NavigationArrowDropDownCircle, + "NavigationArrowDropUp": &icons.NavigationArrowDropUp, + "NavigationArrowForward": &icons.NavigationArrowForward, + "NavigationArrowUpward": &icons.NavigationArrowUpward, + "NavigationCancel": &icons.NavigationCancel, + "NavigationCheck": &icons.NavigationCheck, + "NavigationChevronLeft": &icons.NavigationChevronLeft, + "NavigationChevronRight": &icons.NavigationChevronRight, + "NavigationClose": &icons.NavigationClose, + "NavigationExpandLess": &icons.NavigationExpandLess, + "NavigationExpandMore": &icons.NavigationExpandMore, + "NavigationFirstPage": &icons.NavigationFirstPage, + "NavigationFullscreen": &icons.NavigationFullscreen, + "NavigationFullscreenExit": &icons.NavigationFullscreenExit, + "NavigationLastPage": &icons.NavigationLastPage, + "NavigationMenu": &icons.NavigationMenu, + "NavigationMoreHoriz": &icons.NavigationMoreHoriz, + "NavigationMoreVert": &icons.NavigationMoreVert, + "NavigationRefresh": &icons.NavigationRefresh, + "NavigationSubdirectoryArrowLeft": &icons.NavigationSubdirectoryArrowLeft, + "NavigationSubdirectoryArrowRight": &icons.NavigationSubdirectoryArrowRight, + "NavigationUnfoldLess": &icons.NavigationUnfoldLess, + "NavigationUnfoldMore": &icons.NavigationUnfoldMore, + "NotificationADB": &icons.NotificationADB, + "NotificationAirlineSeatFlat": &icons.NotificationAirlineSeatFlat, + "NotificationAirlineSeatFlatAngled": &icons.NotificationAirlineSeatFlatAngled, + "NotificationAirlineSeatIndividualSuite": &icons.NotificationAirlineSeatIndividualSuite, + "NotificationAirlineSeatLegroomExtra": &icons.NotificationAirlineSeatLegroomExtra, + "NotificationAirlineSeatLegroomNormal": &icons.NotificationAirlineSeatLegroomNormal, + "NotificationAirlineSeatLegroomReduced": &icons.NotificationAirlineSeatLegroomReduced, + "NotificationAirlineSeatReclineExtra": &icons.NotificationAirlineSeatReclineExtra, + "NotificationAirlineSeatReclineNormal": &icons.NotificationAirlineSeatReclineNormal, + "NotificationBluetoothAudio": &icons.NotificationBluetoothAudio, + "NotificationConfirmationNumber": &icons.NotificationConfirmationNumber, + "NotificationDiscFull": &icons.NotificationDiscFull, + "NotificationDoNotDisturb": &icons.NotificationDoNotDisturb, + "NotificationDoNotDisturbAlt": &icons.NotificationDoNotDisturbAlt, + "NotificationDoNotDisturbOff": &icons.NotificationDoNotDisturbOff, + "NotificationDoNotDisturbOn": &icons.NotificationDoNotDisturbOn, + "NotificationDriveETA": &icons.NotificationDriveETA, + "NotificationEnhancedEncryption": &icons.NotificationEnhancedEncryption, + "NotificationEventAvailable": &icons.NotificationEventAvailable, + "NotificationEventBusy": &icons.NotificationEventBusy, + "NotificationEventNote": &icons.NotificationEventNote, + "NotificationFolderSpecial": &icons.NotificationFolderSpecial, + "NotificationLiveTV": &icons.NotificationLiveTV, + "NotificationMMS": &icons.NotificationMMS, + "NotificationMore": &icons.NotificationMore, + "NotificationNetworkCheck": &icons.NotificationNetworkCheck, + "NotificationNetworkLocked": &icons.NotificationNetworkLocked, + "NotificationNoEncryption": &icons.NotificationNoEncryption, + "NotificationOnDemandVideo": &icons.NotificationOnDemandVideo, + "NotificationPersonalVideo": &icons.NotificationPersonalVideo, + "NotificationPhoneBluetoothSpeaker": &icons.NotificationPhoneBluetoothSpeaker, + "NotificationPhoneForwarded": &icons.NotificationPhoneForwarded, + "NotificationPhoneInTalk": &icons.NotificationPhoneInTalk, + "NotificationPhoneLocked": &icons.NotificationPhoneLocked, + "NotificationPhoneMissed": &icons.NotificationPhoneMissed, + "NotificationPhonePaused": &icons.NotificationPhonePaused, + "NotificationPower": &icons.NotificationPower, + "NotificationPriorityHigh": &icons.NotificationPriorityHigh, + "NotificationRVHookup": &icons.NotificationRVHookup, + "NotificationSDCard": &icons.NotificationSDCard, + "NotificationSIMCardAlert": &icons.NotificationSIMCardAlert, + "NotificationSMS": &icons.NotificationSMS, + "NotificationSMSFailed": &icons.NotificationSMSFailed, + "NotificationSync": &icons.NotificationSync, + "NotificationSyncDisabled": &icons.NotificationSyncDisabled, + "NotificationSyncProblem": &icons.NotificationSyncProblem, + "NotificationSystemUpdate": &icons.NotificationSystemUpdate, + "NotificationTapAndPlay": &icons.NotificationTapAndPlay, + "NotificationTimeToLeave": &icons.NotificationTimeToLeave, + "NotificationVibration": &icons.NotificationVibration, + "NotificationVoiceChat": &icons.NotificationVoiceChat, + "NotificationVPNLock": &icons.NotificationVPNLock, + "NotificationWC": &icons.NotificationWC, + "NotificationWiFi": &icons.NotificationWiFi, + "PlacesACUnit": &icons.PlacesACUnit, + "PlacesAirportShuttle": &icons.PlacesAirportShuttle, + "PlacesAllInclusive": &icons.PlacesAllInclusive, + "PlacesBeachAccess": &icons.PlacesBeachAccess, + "PlacesBusinessCenter": &icons.PlacesBusinessCenter, + "PlacesCasino": &icons.PlacesCasino, + "PlacesChildCare": &icons.PlacesChildCare, + "PlacesChildFriendly": &icons.PlacesChildFriendly, + "PlacesFitnessCenter": &icons.PlacesFitnessCenter, + "PlacesFreeBreakfast": &icons.PlacesFreeBreakfast, + "PlacesGolfCourse": &icons.PlacesGolfCourse, + "PlacesHotTub": &icons.PlacesHotTub, + "PlacesKitchen": &icons.PlacesKitchen, + "PlacesPool": &icons.PlacesPool, + "PlacesRoomService": &icons.PlacesRoomService, + "PlacesRVHookup": &icons.PlacesRVHookup, + "PlacesSmokeFree": &icons.PlacesSmokeFree, + "PlacesSmokingRooms": &icons.PlacesSmokingRooms, + "PlacesSpa": &icons.PlacesSpa, + "SocialCake": &icons.SocialCake, + "SocialDomain": &icons.SocialDomain, + "SocialGroup": &icons.SocialGroup, + "SocialGroupAdd": &icons.SocialGroupAdd, + "SocialLocationCity": &icons.SocialLocationCity, + "SocialMood": &icons.SocialMood, + "SocialMoodBad": &icons.SocialMoodBad, + "SocialNotifications": &icons.SocialNotifications, + "SocialNotificationsActive": &icons.SocialNotificationsActive, + "SocialNotificationsNone": &icons.SocialNotificationsNone, + "SocialNotificationsOff": &icons.SocialNotificationsOff, + "SocialNotificationsPaused": &icons.SocialNotificationsPaused, + "SocialPages": &icons.SocialPages, + "SocialPartyMode": &icons.SocialPartyMode, + "SocialPeople": &icons.SocialPeople, + "SocialPeopleOutline": &icons.SocialPeopleOutline, + "SocialPerson": &icons.SocialPerson, + "SocialPersonAdd": &icons.SocialPersonAdd, + "SocialPersonOutline": &icons.SocialPersonOutline, + "SocialPlusOne": &icons.SocialPlusOne, + "SocialPoll": &icons.SocialPoll, + "SocialPublic": &icons.SocialPublic, + "SocialSchool": &icons.SocialSchool, + "SocialSentimentDissatisfied": &icons.SocialSentimentDissatisfied, + "SocialSentimentNeutral": &icons.SocialSentimentNeutral, + "SocialSentimentSatisfied": &icons.SocialSentimentSatisfied, + "SocialSentimentVeryDissatisfied": &icons.SocialSentimentVeryDissatisfied, + "SocialSentimentVerySatisfied": &icons.SocialSentimentVerySatisfied, + "SocialShare": &icons.SocialShare, + "SocialWhatsHot": &icons.SocialWhatsHot, + "ToggleCheckBox": &icons.ToggleCheckBox, + "ToggleCheckBoxOutlineBlank": &icons.ToggleCheckBoxOutlineBlank, + "ToggleIndeterminateCheckBox": &icons.ToggleIndeterminateCheckBox, + "ToggleRadioButtonChecked": &icons.ToggleRadioButtonChecked, + "ToggleRadioButtonUnchecked": &icons.ToggleRadioButtonUnchecked, + "ToggleStar": &icons.ToggleStar, + "ToggleStarBorder": &icons.ToggleStarBorder, + "ToggleStarHalf": &icons.ToggleStarHalf, +} diff --git a/pkg/gel/image.go b/pkg/gel/image.go new file mode 100644 index 0000000..b57d109 --- /dev/null +++ b/pkg/gel/image.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gel + +import ( + "image" + + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +// Image is a widget that displays an image. +type Image struct { + // src is the image to display. + src paint.ImageOp + // scale is the ratio of image pixels to dps. If scale is zero Image falls back to a scale that match a standard 72 + // DPI. + scale float32 +} + +func (th *Theme) Image() *Image { + return &Image{} +} + +func (i *Image) Src(img paint.ImageOp) *Image { + i.src = img + return i +} + +func (i *Image) Scale(scale float32) *Image { + i.scale = scale + return i +} + +func (i Image) Fn(gtx layout.Context) layout.Dimensions { + scale := i.scale + if scale == 0 { + scale = 160.0 / 72.0 + } + size := i.src.Size() + wf, hf := float32(size.X), float32(size.Y) + w, h := gtx.Px(unit.Dp(wf*scale)), gtx.Px(unit.Dp(hf*scale)) + cs := gtx.Constraints + d := cs.Constrain(image.Pt(w, h)) + stack := op.Save(gtx.Ops) + clip.Rect(image.Rectangle{Max: d}).Add(gtx.Ops) + i.src.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + return layout.Dimensions{Size: size} +} diff --git a/pkg/gel/incdec.go b/pkg/gel/incdec.go new file mode 100644 index 0000000..bade9f8 --- /dev/null +++ b/pkg/gel/incdec.go @@ -0,0 +1,147 @@ +package gel + +import ( + "fmt" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +type IncDec struct { + *Window + nDigits int + min, max int + amount int + current int + changeHook func(n int) + inc, dec *Clickable + color, background string + inactive string + scale float32 +} + +// IncDec is a simple increment/decrement for a number setting +func (w *Window) IncDec() (out *IncDec) { + out = &IncDec{ + Window: w, + // nDigits: nDigits, + // min: min, + // max: max, + // current: current, + // changeHook: changeHook, + inc: w.Clickable(), + dec: w.Clickable(), + color: "DocText", + background: "Transparent", + inactive: "Transparent", + amount: 1, + scale: 1, + } + return +} + +func (in *IncDec) Scale(n float32) *IncDec { + in.scale = n + return in +} + +func (in *IncDec) Amount(n int) *IncDec { + in.amount = n + return in +} + +func (in *IncDec) ChangeHook(fn func(n int)) *IncDec { + in.changeHook = fn + return in +} + +func (in *IncDec) SetCurrent(current int) *IncDec { + in.current = current + return in +} + +func (in *IncDec) GetCurrent() int { + return in.current +} + +func (in *IncDec) Max(max int) *IncDec { + in.max = max + return in +} + +func (in *IncDec) Min(min int) *IncDec { + in.min = min + return in +} + +func (in *IncDec) NDigits(nDigits int) *IncDec { + in.nDigits = nDigits + return in +} + +func (in *IncDec) Color(color string) *IncDec { + in.color = color + return in +} + +func (in *IncDec) Background(color string) *IncDec { + in.background = color + return in +} +func (in *IncDec) Inactive(color string) *IncDec { + in.inactive = color + return in +} + +func (in *IncDec) Fn(gtx l.Context) l.Dimensions { + out := in.Theme.Flex().AlignMiddle() + incColor, decColor := in.color, in.color + if in.current != in.min { + out.Rigid( + in.Inset( + 0.25, + in.ButtonLayout( + in.inc.SetClick( + func() { + ic := in.current + ic -= in.amount + if ic < in.min { + ic = in.min + } + in.current = ic + in.changeHook(ic) + }, + ), + ).Background(in.background).Embed( + in.Icon().Color(decColor).Scale(in.scale).Src(&icons.ContentRemove).Fn, + ).Fn, + ).Fn, + ) + } + cur := fmt.Sprintf("%"+fmt.Sprint(in.nDigits)+"d", in.current) + out.Rigid(in.Caption(cur).Color(in.color).TextScale(in.scale).Font("go regular").Fn) + if in.current != in.max { + out.Rigid( + in.Inset( + 0.25, + in.ButtonLayout( + in.dec.SetClick( + func() { + ic := in.current + ic += in.amount + if in.current > in.max { + in.current = in.max + } else { + in.current = ic + in.changeHook(in.current) + } + }, + ), + ).Background(in.background).Embed( + in.Icon().Color(incColor).Scale(in.scale).Src(&icons.ContentAdd).Fn, + ).Fn, + ).Fn, + ) + } + return out.Fn(gtx) +} diff --git a/pkg/gel/indefinite.go b/pkg/gel/indefinite.go new file mode 100644 index 0000000..9bf3d92 --- /dev/null +++ b/pkg/gel/indefinite.go @@ -0,0 +1,95 @@ +package gel + +import ( + "image" + "image/color" + "math" + "time" + + "github.com/p9c/p9/pkg/gel/gio/f32" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" +) + +type Indefinite struct { + *Window + color color.NRGBA + scale float32 +} + +// Indefinite creates an indefinite loading animation icon +func (w *Window) Indefinite() *Indefinite { + return &Indefinite{ + Window: w, + color: w.Colors.GetNRGBAFromName("Primary"), + } +} + +// Scale sets the size of the spinner +func (lo *Indefinite) Scale(scale float32) *Indefinite { + lo.scale = scale + return lo +} + +// Color sets the color of the spinner +func (lo *Indefinite) Color(color string) *Indefinite { + lo.color = lo.Theme.Colors.GetNRGBAFromName(color) + return lo +} + +// Fn renders the loader +func (lo *Indefinite) Fn(gtx l.Context) l.Dimensions { + diam := gtx.Constraints.Min.X + if minY := gtx.Constraints.Min.Y; minY > diam { + diam = minY + } + if diam == 0 { + diam = gtx.Px(lo.Theme.TextSize.Scale(lo.scale)) + } + sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) + radius := float64(sz.X) * .5 + defer op.Save(gtx.Ops).Load() + op.Offset(f32.Pt(float32(radius), float32(radius))).Add(gtx.Ops) + dt := (time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds() + startAngle := dt * math.Pi * 2 + endAngle := startAngle + math.Pi*1.5 + clipLoader(gtx.Ops, startAngle, endAngle, radius) + paint.ColorOp{ + Color: lo.color, + }.Add(gtx.Ops) + op.Offset(f32.Pt(-float32(radius), -float32(radius))).Add(gtx.Ops) + paint.PaintOp{ + // Rect: f32.Rectangle{Max: l.FPt(sz)}, + }.Add(gtx.Ops) + op.InvalidateOp{}.Add(gtx.Ops) + return l.Dimensions{ + Size: sz, + } +} + +func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) { + const thickness = .25 + var ( + width = float32(radius * thickness) + delta = float32(endAngle - startAngle) + + vy, vx = math.Sincos(startAngle) + + pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius)) + center = f32.Pt(0, 0).Sub(pen) + + p clip.Path + ) + p.Begin(ops) + p.Move(pen) + p.Arc(center, center, delta) + clip.Stroke{ + Path: p.End(), + Style: clip.StrokeStyle{ + Width: width, + Cap: clip.FlatCap, + }, + }.Op().Add(ops) +} diff --git a/pkg/gel/input.go b/pkg/gel/input.go new file mode 100644 index 0000000..7572954 --- /dev/null +++ b/pkg/gel/input.go @@ -0,0 +1,164 @@ +package gel + +import ( + "regexp" + + "golang.org/x/exp/shiny/materialdesign/icons" + + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +type Input struct { + *Window + editor *Editor + input *TextInput + clearClickable *Clickable + clearButton *IconButton + copyClickable *Clickable + copyButton *IconButton + pasteClickable *Clickable + pasteButton *IconButton + GetText func() string + SetText func(string) + SetPasteFunc func() bool + borderColor string + borderColorUnfocused string + borderColorFocused string + backgroundColor string + focused bool +} + +var findSpaceRegexp = regexp.MustCompile(`\s+`) + +func (w *Window) Input( + txt, hint, borderColorFocused, borderColorUnfocused, backgroundColor string, + submit, change func(txt string), +) *Input { + editor := w.Editor().SingleLine().Submit(false) + input := w.TextInput(editor, hint).TextScale(1) + p := &Input{ + Window: w, + clearClickable: w.Clickable(), + copyClickable: w.Clickable(), + pasteClickable: w.Clickable(), + editor: editor, + input: input, + borderColorUnfocused: borderColorUnfocused, + borderColorFocused: borderColorFocused, + backgroundColor: backgroundColor, + } + p.GetText = func() string { + return p.editor.Text() + } + p.SetText = func(s string) { + p.editor.SetText(s) + } + p.clearButton = w.IconButton(p.clearClickable) + p.copyButton = w.IconButton(p.copyClickable) + p.pasteButton = w.IconButton(p.pasteClickable) + clearClickableFn := func() { + p.editor.SetText("") + p.editor.changeHook("") + p.editor.Focus() + } + copyClickableFn := func() { + p.ClipboardWriteReqs <- p.editor.Text() + p.editor.Focus() + } + pasteClickableFn := func() { + p.ClipboardReadReqs <- func(cs string) { + cs = findSpaceRegexp.ReplaceAllString(cs, " ") + p.editor.Insert(cs) + p.editor.changeHook(cs) + p.editor.Focus() + } + } + p.clearButton. + Icon( + w.Icon(). + Color("DocText"). + Src(&icons.ContentBackspace), + ) + p.copyButton. + Icon( + w.Icon(). + Color("DocText"). + Src(&icons.ContentContentCopy), + ) + p.pasteButton. + Icon( + w.Icon(). + Color("DocText"). + Src(&icons.ContentContentPaste), + ) + p.input.Color("DocText") + p.clearClickable.SetClick(clearClickableFn) + p.copyClickable.SetClick(copyClickableFn) + p.pasteClickable.SetClick(pasteClickableFn) + p.editor.SetText(txt).SetSubmit( + func(txt string) { + go func() { + submit(txt) + }() + }, + ).SetChange( + change, + ) + p.editor.SetFocus( + func(is bool) { + if is { + p.borderColor = p.borderColorFocused + } else { + p.borderColor = p.borderColorUnfocused + } + }, + ) + return p +} + +// Fn renders the input widget +func (in *Input) Fn(gtx l.Context) l.Dimensions { + // gtx.Constraints.Max.X = int(in.TextSize.Scale(float32(in.size)).True) + // gtx.Constraints.Min.X = 0 + // width := int(in.Theme.TextSize.Scale(in.size).True) + // gtx.Constraints.Max.X, gtx.Constraints.Min.X = width, width + return in.Border(). + Width(0.125). + CornerRadius(0.0). + Color(in.borderColor). + Embed( + in.Fill( + in.backgroundColor, l.Center, in.TextSize.V, 0, + in.Inset( + 0.25, + in.Flex(). + Flexed( + 1, + in.Inset(0.125, in.input.Color("DocText").Fn).Fn, + ). + Rigid( + in.copyButton. + Background(""). + Icon(in.Icon().Color(in.borderColor).Scale(Scales["H6"]).Src(&icons.ContentContentCopy)). + ButtonInset(0.25). + Fn, + ). + Rigid( + in.pasteButton. + Background(""). + Icon(in.Icon().Color(in.borderColor).Scale(Scales["H6"]).Src(&icons.ContentContentPaste)). + ButtonInset(0.25). + Fn, + ). + Rigid( + in.clearButton. + Background(""). + Icon(in.Icon().Color(in.borderColor).Scale(Scales["H6"]).Src(&icons.ContentBackspace)). + ButtonInset(0.25). + Fn, + ). + Fn, + ).Fn, + ).Fn, + ).Fn(gtx) +} diff --git a/pkg/gel/inset.go b/pkg/gel/inset.go new file mode 100644 index 0000000..5894725 --- /dev/null +++ b/pkg/gel/inset.go @@ -0,0 +1,32 @@ +package gel + +import ( + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +type Inset struct { + *Window + in l.Inset + w l.Widget +} + +// Inset creates a padded empty space around a widget +func (w *Window) Inset(pad float32, embed l.Widget) (out *Inset) { + out = &Inset{ + Window: w, + in: l.UniformInset(w.TextSize.Scale(pad)), + w: embed, + } + return +} + +// Embed sets the widget that will be inside the inset +func (in *Inset) Embed(w l.Widget) *Inset { + in.w = w + return in +} + +// Fn lays out the given widget with the configured context and padding +func (in *Inset) Fn(c l.Context) l.Dimensions { + return in.in.Layout(c, in.w) +} diff --git a/pkg/gel/intslider.go b/pkg/gel/intslider.go new file mode 100644 index 0000000..c763907 --- /dev/null +++ b/pkg/gel/intslider.go @@ -0,0 +1,91 @@ +package gel + +import ( + "fmt" + + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +type IntSlider struct { + *Window + min, max *Clickable + floater *Float + hook func(int) + value int + minV, maxV float32 +} + +func (w *Window) IntSlider() *IntSlider { + return &IntSlider{ + Window: w, + min: w.Clickable(), + max: w.Clickable(), + floater: w.Float(), + hook: func(int) {}, + } +} + +func (i *IntSlider) Min(min float32) *IntSlider { + i.minV = min + return i +} + +func (i *IntSlider) Max(max float32) *IntSlider { + i.maxV = max + return i +} + +func (i *IntSlider) Hook(fn func(v int)) *IntSlider { + i.hook = fn + return i +} + +func (i *IntSlider) Value(value int) *IntSlider { + i.value = value + i.floater.SetValue(float32(value)) + return i +} + +func (i *IntSlider) GetValue() int { + return int(i.floater.Value() + 0.5) +} + +func (i *IntSlider) Fn(gtx l.Context) l.Dimensions { + return i.Flex().Rigid( + i.Button( + i.min.SetClick(func() { + i.floater.SetValue(i.minV) + i.hook(int(i.minV)) + })). + Inset(0.25). + Color("Primary"). + Background("Transparent"). + Font("bariol regular"). + Text("0"). + Fn, + ).Flexed(1, + i.Inset(0.25, + i.Slider(). + Float(i.floater.SetHook(func(fl float32) { + iFl := int(fl + 0.5) + i.value = iFl + i.floater.SetValue(float32(iFl)) + i.hook(iFl) + })). + Min(i.minV).Max(i.maxV). + Fn, + ).Fn, + ).Rigid( + i.Button( + i.max.SetClick(func() { + i.floater.SetValue(i.maxV) + i.hook(int(i.maxV)) + })). + Inset(0.25). + Color("Primary"). + Background("Transparent"). + Font("bariol regular"). + Text(fmt.Sprint(int(i.maxV))). + Fn, + ).Fn(gtx) +} diff --git a/pkg/gel/label.go b/pkg/gel/label.go new file mode 100644 index 0000000..5e619f1 --- /dev/null +++ b/pkg/gel/label.go @@ -0,0 +1,384 @@ +package gel + +import ( + "fmt" + "image" + "image/color" + "unicode/utf8" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "golang.org/x/image/math/fixed" +) + +// Label is text drawn inside an empty box +type Label struct { + *Window + // Face defines the text style. + font text.Font + // Color is the text color. + color color.NRGBA + // Alignment specify the text alignment. + alignment text.Alignment + // MaxLines limits the number of lines. Zero means no limit. + maxLines int + text string + textSize unit.Value + shaper text.Shaper +} + +// screenPos describes a character position (in text line and column numbers, +// not pixels): Y = line number, X = rune column. +type screenPos image.Point + +type segmentIterator struct { + Lines []text.Line + Clip image.Rectangle + Alignment text.Alignment + Width int + Offset image.Point + startSel screenPos + endSel screenPos + + pos screenPos // current position + line text.Line // current line + layout text.Layout // current line's Layout + + // pixel positions + off fixed.Point26_6 + y, prevDesc fixed.Int26_6 +} + +func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int, image.Point, bool) { + for l.pos.Y < len(l.Lines) { + if l.pos.X == 0 { + l.line = l.Lines[l.pos.Y] + + // Calculate X & Y pixel coordinates of left edge of line. We need y + // for the next line, so it's in l, but we only need x here, so it's + // not. + x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X) + l.y += l.prevDesc + l.line.Ascent + l.prevDesc = l.line.Descent + // Align baseline and line start to the pixel grid. + l.off = fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())} + l.y = l.off.Y + l.off.Y += fixed.I(l.Offset.Y) + if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { + break + } + + if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { + // This line is outside/before the clip area; go on to the next line. + l.pos.Y++ + continue + } + + // Copy the line's Layout, since we slice it up later. + l.layout = l.line.Layout + + // Find the left edge of the text visible in the l.Clip clipping + // area. + for len(l.layout.Advances) > 0 { + _, n := utf8.DecodeRuneInString(l.layout.Text) + adv := l.layout.Advances[0] + if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X { + break + } + l.off.X += adv + l.layout.Text = l.layout.Text[n:] + l.layout.Advances = l.layout.Advances[1:] + l.pos.X++ + } + } + + selected := l.inSelection() + endx := l.off.X + rune := 0 + nextLine := true + retLayout := l.layout + for n := range l.layout.Text { + selChanged := selected != l.inSelection() + beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X + if selChanged || beyondClipEdge { + retLayout.Advances = l.layout.Advances[:rune] + retLayout.Text = l.layout.Text[:n] + if selChanged { + // Save the rest of the line + l.layout.Advances = l.layout.Advances[rune:] + l.layout.Text = l.layout.Text[n:] + nextLine = false + } + break + } + endx += l.layout.Advances[rune] + rune++ + l.pos.X++ + } + offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()} + + // Calculate the width & height if the returned text. + // + // If there's a better way to do this, I'm all ears. + var d fixed.Int26_6 + for _, adv := range retLayout.Advances { + d += adv + } + size := image.Point{ + X: d.Ceil(), + Y: (l.line.Ascent + l.line.Descent).Ceil(), + } + + if nextLine { + l.pos.Y++ + l.pos.X = 0 + } else { + l.off.X = endx + } + + return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true + } + return text.Layout{}, image.Point{}, false, 0, image.Point{}, false +} + +func (l *segmentIterator) inSelection() bool { + return l.startSel.LessOrEqual(l.pos) && + l.pos.Less(l.endSel) +} + +func (p1 screenPos) LessOrEqual(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X) +} + +func (p1 screenPos) Less(p2 screenPos) bool { + return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X) +} + +// Fn renders the label as specified +func (l *Label) Fn(gtx l.Context) l.Dimensions { + cs := gtx.Constraints + textSize := fixed.I(gtx.Px(l.textSize)) + lines := l.shaper.LayoutString(l.font, textSize, cs.Max.X, l.text) + if max := l.maxLines; max > 0 && len(lines) > max { + lines = lines[:max] + } + dims := linesDimens(lines) + dims.Size = cs.Constrain(dims.Size) + cl := textPadding(lines) + cl.Max = cl.Max.Add(dims.Size) + it := segmentIterator{ + Lines: lines, + Clip: cl, + Alignment: l.alignment, + Width: dims.Size.X, + } + for { + lb, off, _, _, _, ok := it.Next() + if !ok { + break + } + stack := op.Save(gtx.Ops) + op.Offset(Fpt(off)).Add(gtx.Ops) + l.shaper.Shape(l.font, textSize, lb).Add(gtx.Ops) + clip.Rect(cl.Sub(off)).Add(gtx.Ops) + paint.ColorOp{Color: l.color}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } + return dims +} + +func textPadding(lines []text.Line) (padding image.Rectangle) { + if len(lines) == 0 { + return + } + first := lines[0] + if d := first.Ascent + first.Bounds.Min.Y; d < 0 { + padding.Min.Y = d.Ceil() + } + last := lines[len(lines)-1] + if d := last.Bounds.Max.Y - last.Descent; d > 0 { + padding.Max.Y = d.Ceil() + } + if d := first.Bounds.Min.X; d < 0 { + padding.Min.X = d.Ceil() + } + if d := first.Bounds.Max.X - first.Width; d > 0 { + padding.Max.X = d.Ceil() + } + return +} + +func linesDimens(lines []text.Line) l.Dimensions { + var width fixed.Int26_6 + var h int + var baseline int + if len(lines) > 0 { + baseline = lines[0].Ascent.Ceil() + var prevDesc fixed.Int26_6 + for _, l := range lines { + h += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if l.Width > width { + width = l.Width + } + } + h += lines[len(lines)-1].Descent.Ceil() + } + w := width.Ceil() + return l.Dimensions{ + Size: image.Point{ + X: w, + Y: h, + }, + Baseline: h - baseline, + } +} + +func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { + mw := fixed.I(maxWidth) + switch align { + case text.Middle: + return fixed.I(((mw - width) / 2).Floor()) + case text.End: + return fixed.I((mw - width).Floor()) + case text.Start: + return 0 + default: + panic(fmt.Errorf("unknown alignment %v", align)) + } +} + +// ScaleType is a map of the set of label txsizes +type ScaleType map[string]float32 + +// Scales is the ratios against +// +// TODO: shouldn't that 16.0 be the text size in the theme? +var Scales = ScaleType{ + "H1": 96.0 / 16.0, + "H2": 60.0 / 16.0, + "H3": 48.0 / 16.0, + "H4": 34.0 / 16.0, + "H5": 24.0 / 16.0, + "H6": 20.0 / 16.0, + "Body1": 1, + "Body2": 14.0 / 16.0, + "Caption": 12.0 / 16.0, +} + +// Label creates a label that prints a block of text +func (w *Window) Label() (l *Label) { + var f text.Font + var e error + var fon text.Font + if fon, e = w.Theme.collection.Font("plan9"); !E.Chk(e) { + f = fon + } + return &Label{ + Window: w, + text: "", + font: f, + color: w.Colors.GetNRGBAFromName("DocText"), + textSize: unit.Sp(1), + shaper: w.shaper, + } +} + +// Text sets the text to render in the label +func (l *Label) Text(text string) *Label { + l.text = text + return l +} + +// TextScale sets the size of the text relative to the base font size +func (l *Label) TextScale(scale float32) *Label { + l.textSize = l.Theme.TextSize.Scale(scale) + return l +} + +// MaxLines sets the maximum number of lines to render +func (l *Label) MaxLines(maxLines int) *Label { + l.maxLines = maxLines + return l +} + +// Alignment sets the text alignment, left, right or centered +func (l *Label) Alignment(alignment text.Alignment) *Label { + l.alignment = alignment + return l +} + +// Color sets the color of the label font +func (l *Label) Color(color string) *Label { + l.color = l.Theme.Colors.GetNRGBAFromName(color) + return l +} + +// Font sets the font out of the available font collection +func (l *Label) Font(font string) *Label { + var e error + var fon text.Font + if fon, e = l.Theme.collection.Font(font); !E.Chk(e) { + l.font = fon + } + return l +} + +// H1 header 1 +func (w *Window) H1(txt string) (l *Label) { + l = w.Label().TextScale(Scales["H1"]).Font("plan9").Text(txt) + return +} + +// H2 header 2 +func (w *Window) H2(txt string) (l *Label) { + l = w.Label().TextScale(Scales["H2"]).Font("plan9").Text(txt) + return +} + +// H3 header 3 +func (w *Window) H3(txt string) (l *Label) { + l = w.Label().TextScale(Scales["H3"]).Font("plan9").Text(txt) + return +} + +// H4 header 4 +func (w *Window) H4(txt string) (l *Label) { + l = w.Label().TextScale(Scales["H4"]).Font("plan9").Text(txt) + return +} + +// H5 header 5 +func (w *Window) H5(txt string) (l *Label) { + l = w.Label().TextScale(Scales["H5"]).Font("plan9").Text(txt) + return +} + +// H6 header 6 +func (w *Window) H6(txt string) (l *Label) { + l = w.Label().TextScale(Scales["H6"]).Font("plan9").Text(txt) + return +} + +// Body1 normal body text 1 +func (w *Window) Body1(txt string) (l *Label) { + l = w.Label().TextScale(Scales["Body1"]).Font("bariol regular").Text(txt) + return +} + +// Body2 normal body text 2 +func (w *Window) Body2(txt string) (l *Label) { + l = w.Label().TextScale(Scales["Body2"]).Font("bariol regular").Text(txt) + return +} + +// Caption caption text +func (w *Window) Caption(txt string) (l *Label) { + l = w.Label().TextScale(Scales["Caption"]).Font("bariol regular").Text(txt) + return +} diff --git a/pkg/gel/list.go b/pkg/gel/list.go new file mode 100644 index 0000000..c5c0ac9 --- /dev/null +++ b/pkg/gel/list.go @@ -0,0 +1,724 @@ +package gel + +import ( + "image" + "time" + + "github.com/p9c/p9/pkg/gel/gio/gesture" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" +) + +type scrollChild struct { + size image.Point + call op.CallOp +} + +// List displays a subsection of a potentially infinitely large underlying list. List accepts user input to scroll the +// subsection. +type List struct { + axis l.Axis + // ScrollToEnd instructs the list to stay scrolled to the far end position once reached. A List with ScrollToEnd == + // true and Position.BeforeEnd == false draws its content with the last item at the bottom of the list area. + scrollToEnd bool + // Alignment is the cross axis alignment of list elements. + alignment l.Alignment + scroll gesture.Scroll + scrollDelta int + // position is updated during Layout. To save the list scroll position, just save Position after Layout finishes. To + // scroll the list programmatically, update Position (e.g. restore it from a saved value) before calling Layout. + // nextUp, nextDown Position + position Position + Len int + // maxSize is the total size of visible children. + maxSize int + children []scrollChild + dir iterationDir + + // all below are additional fields to implement the scrollbar + *Window + // we store the constraints here instead of in the `cs` field + ctx l.Context + sideScroll gesture.Scroll + disableScroll bool + drag gesture.Drag + recentPageClick time.Time + color string + active string + background string + currentColor string + scrollWidth int + setScrollWidth int + length int + prevLength int + w ListElement + pageUp, pageDown *Clickable + dims DimensionList + cross int + view, total, before int + top, middle, bottom int + lastWidth int + recalculateTime time.Time + recalculate bool + notFirst bool + leftSide bool +} + +// List returns a new scrollable List widget +func (w *Window) List() (li *List) { + li = &List{ + Window: w, + pageUp: w.WidgetPool.GetClickable(), + pageDown: w.WidgetPool.GetClickable(), + color: "DocText", + background: "Transparent", + active: "Primary", + scrollWidth: int(w.TextSize.Scale(0.75).V), + setScrollWidth: int(w.TextSize.Scale(0.75).V), + recalculateTime: time.Now().Add(-time.Second), + recalculate: true, + } + li.currentColor = li.color + return +} + +// ListElement is a function that computes the dimensions of a list element. +type ListElement func(gtx l.Context, index int) l.Dimensions + +type iterationDir uint8 + +// Position is a List scroll offset represented as an offset from the top edge of a child element. +type Position struct { + // BeforeEnd tracks whether the List position is before the very end. We use "before end" instead of "at end" so + // that the zero value of a Position struct is useful. + // + // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, then First and Offset are ignored, and the + // list is drawn with the last item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored. + BeforeEnd bool + // First is the index of the first visible child. + First int + // Offset is the distance in pixels from the top edge to the child at index First. + Offset int + // OffsetLast is the signed distance in pixels from the bottom edge to the + // bottom edge of the child at index First+Count. + OffsetLast int + // Count is the number of visible children. + Count int +} + +const ( + iterateNone iterationDir = iota + iterateForward + iterateBackward +) + +// init prepares the list for iterating through its children with next. +func (li *List) init(gtx l.Context, length int) { + if li.more() { + panic("unfinished child") + } + li.ctx = gtx + li.maxSize = 0 + li.children = li.children[:0] + li.Len = length + li.update() + if li.canScrollToEnd() || li.position.First > length { + li.position.Offset = 0 + li.position.First = length + } +} + +// Layout the List. +func (li *List) Layout(gtx l.Context, len int, w ListElement) l.Dimensions { + li.init(gtx, len) + crossMin, crossMax := axisCrossConstraint(li.axis, gtx.Constraints) + gtx.Constraints = axisConstraints(li.axis, 0, Inf, crossMin, crossMax) + macro := op.Record(gtx.Ops) + for li.next(); li.more(); li.next() { + child := op.Record(gtx.Ops) + dims := w(gtx, li.index()) + call := child.Stop() + li.end(dims, call) + } + return li.layout(macro) +} + +// canScrollToEnd returns true if there is room to scroll further towards the end +func (li *List) canScrollToEnd() bool { + return li.scrollToEnd && !li.position.BeforeEnd +} + +// Dragging reports whether the List is being dragged. +func (li *List) Dragging() bool { + return li.scroll.State() == gesture.StateDragging || + li.sideScroll.State() == gesture.StateDragging +} + +// update the scrolling +func (li *List) update() { + d := li.scroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, gesture.Axis(li.axis)) + d += li.sideScroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, gesture.Axis(li.axis)) + li.scrollDelta = d + li.position.Offset += d +} + +// next advances to the next child. +func (li *List) next() { + li.dir = li.nextDir() + // The user scroll offset is applied after scrolling to list end. + if li.canScrollToEnd() && !li.more() && li.scrollDelta < 0 { + li.position.BeforeEnd = true + li.position.Offset += li.scrollDelta + li.dir = li.nextDir() + } +} + +// index is current child's position in the underlying list. +func (li *List) index() int { + switch li.dir { + case iterateBackward: + return li.position.First - 1 + case iterateForward: + return li.position.First + len(li.children) + default: + panic("Index called before Next") + } +} + +// more reports whether more children are needed. +func (li *List) more() bool { + return li.dir != iterateNone +} + +func (li *List) nextDir() iterationDir { + _, vSize := axisMainConstraint(li.axis, li.ctx.Constraints) + last := li.position.First + len(li.children) + // Clamp offset. + if li.maxSize-li.position.Offset < vSize && last == li.Len { + li.position.Offset = li.maxSize - vSize + } + if li.position.Offset < 0 && li.position.First == 0 { + li.position.Offset = 0 + } + switch { + case len(li.children) == li.Len: + return iterateNone + case li.maxSize-li.position.Offset < vSize: + return iterateForward + case li.position.Offset < 0: + return iterateBackward + } + return iterateNone +} + +// End the current child by specifying its dimensions. +func (li *List) end(dims l.Dimensions, call op.CallOp) { + child := scrollChild{dims.Size, call} + mainSize := axisConvert(li.axis, child.size).X + li.maxSize += mainSize + switch li.dir { + case iterateForward: + li.children = append(li.children, child) + case iterateBackward: + li.children = append(li.children, scrollChild{}) + copy(li.children[1:], li.children) + li.children[0] = child + li.position.First-- + li.position.Offset += mainSize + default: + panic("call Next before End") + } + li.dir = iterateNone +} + +// layout the List and return its dimensions. +func (li *List) layout(macro op.MacroOp) l.Dimensions { + if li.more() { + panic("unfinished child") + } + mainMin, mainMax := axisMainConstraint(li.axis, li.ctx.Constraints) + children := li.children + // Skip invisible children + for len(children) > 0 { + sz := children[0].size + mainSize := axisConvert(li.axis, sz).X + if li.position.Offset < mainSize { + // First child is partially visible. + break + } + li.position.First++ + li.position.Offset -= mainSize + children = children[1:] + } + size := -li.position.Offset + var maxCross int + for i, child := range children { + sz := axisConvert(li.axis, child.size) + if c := sz.Y; c > maxCross { + maxCross = c + } + size += sz.X + if size >= mainMax { + children = children[:i+1] + break + } + } + li.position.Count = len(children) + li.position.OffsetLast = mainMax - size + ops := li.ctx.Ops + pos := -li.position.Offset + // ScrollToEnd lists are end aligned. + if space := li.position.OffsetLast; li.scrollToEnd && space > 0 { + pos += space + } + for _, child := range children { + sz := axisConvert(li.axis, child.size) + var cross int + switch li.alignment { + case l.End: + cross = maxCross - sz.Y + case l.Middle: + cross = (maxCross - sz.Y) / 2 + } + childSize := sz.X + max := childSize + pos + if max > mainMax { + max = mainMax + } + min := pos + if min < 0 { + min = 0 + } + r := image.Rectangle{ + Min: axisConvert(li.axis, image.Pt(min, -Inf)), + Max: axisConvert(li.axis, image.Pt(max, Inf)), + } + stack := op.Save(ops) + clip.Rect(r).Add(ops) + pt := axisConvert(li.axis, image.Pt(pos, cross)) + op.Offset(Fpt(pt)).Add(ops) + child.call.Add(ops) + stack.Load() + pos += childSize + } + atStart := li.position.First == 0 && li.position.Offset <= 0 + atEnd := li.position.First+len(children) == li.Len && mainMax >= pos + if atStart && li.scrollDelta < 0 || atEnd && li.scrollDelta > 0 { + li.scroll.Stop() + li.sideScroll.Stop() + } + li.position.BeforeEnd = !atEnd + if pos < mainMin { + pos = mainMin + } + if pos > mainMax { + pos = mainMax + } + dims := axisConvert(li.axis, image.Pt(pos, maxCross)) + call := macro.Stop() + defer op.Save(ops).Load() + bounds := image.Rectangle{Max: dims} + pointer.Rect(bounds).Add(ops) + // li.sideScroll.Add(ops, bounds) + // li.scroll.Add(ops, bounds) + + var min, max int + if o := li.position.Offset; o > 0 { + // Use the size of the invisible part as scroll boundary. + min = -o + } else if li.position.First > 0 { + min = -Inf + } + if o := li.position.OffsetLast; o < 0 { + max = -o + } else if li.position.First+li.position.Count < li.Len { + max = Inf + } + scrollRange := image.Rectangle{ + Min: axisConvert(li.axis, image.Pt(min, 0)), + Max: axisConvert(li.axis, image.Pt(max, 0)), + } + li.scroll.Add(ops, scrollRange) + li.sideScroll.Add(ops, scrollRange) + + call.Add(ops) + return l.Dimensions{Size: dims} +} + +// Everything below is extensions on the original from github.com/p9c/p9/pkg/gel/gio/layout + +// Position returns the current position of the scroller +func (li *List) Position() Position { + return li.position +} + +// SetPosition sets the position of the scroller +func (li *List) SetPosition(position Position) { + li.position = position +} + +// JumpToStart moves the position to the start +func (li *List) JumpToStart() { + li.position = Position{} +} + +// JumpToEnd moves the position to the end +func (li *List) JumpToEnd() { + li.position = Position{ + BeforeEnd: false, + First: len(li.dims), + Offset: axisMain(li.axis, li.dims[len(li.dims)-1].Size), + } +} + +// Vertical sets the axis to vertical (default implicit is horizontal) +func (li *List) Vertical() (out *List) { + li.axis = l.Vertical + return li +} + +// Start sets the alignment to start +func (li *List) Start() *List { + li.alignment = l.Start + return li +} + +// End sets the alignment to end +func (li *List) End() *List { + li.alignment = l.End + return li +} + +// Middle sets the alignment to middle +func (li *List) Middle() *List { + li.alignment = l.Middle + return li +} + +// Baseline sets the alignment to baseline +func (li *List) Baseline() *List { + li.alignment = l.Baseline + return li +} + +// ScrollToEnd sets the List to add new items to the end and push older ones up/left and initial render has scroll +// to the end (or bottom) of the List +func (li *List) ScrollToEnd() (out *List) { + li.scrollToEnd = true + return li +} + +// LeftSide sets the scroller to be on the opposite side from usual +func (li *List) LeftSide(b bool) (out *List) { + li.leftSide = b + return li +} + +// Length sets the new length for the list +func (li *List) Length(length int) *List { + li.prevLength = li.length + li.length = length + return li +} + +// DisableScroll turns off the scrollbar +func (li *List) DisableScroll(disable bool) *List { + li.disableScroll = disable + if disable { + li.scrollWidth = 0 + } else { + li.scrollWidth = li.setScrollWidth + } + return li +} + +// ListElement defines the function that returns list elements +func (li *List) ListElement(w ListElement) *List { + li.w = w + return li +} + +// ScrollWidth sets the width of the scrollbar +func (li *List) ScrollWidth(width int) *List { + li.scrollWidth = width + li.setScrollWidth = width + return li +} + +// Color sets the primary color of the scrollbar grabber +func (li *List) Color(color string) *List { + li.color = color + li.currentColor = li.color + return li +} + +// Background sets the background color of the scrollbar +func (li *List) Background(color string) *List { + li.background = color + return li +} + +// Active sets the color of the scrollbar grabber when it is being operated +func (li *List) Active(color string) *List { + li.active = color + return li +} + +func (li *List) Slice(gtx l.Context, widgets ...l.Widget) l.Widget { + return li.Length(len(widgets)).Vertical().ListElement(func(gtx l.Context, index int) l.Dimensions { + return widgets[index](gtx) + }, + ).Fn +} + +// Fn runs the layout in the configured context. The ListElement function returns the widget at the given index +func (li *List) Fn(gtx l.Context) l.Dimensions { + if li.length == 0 { + // if there is no children just return a big empty box + return EmptyFromSize(gtx.Constraints.Max)(gtx) + } + if li.disableScroll { + return li.embedWidget(0)(gtx) + } + if li.length != li.prevLength { + li.recalculate = true + li.recalculateTime = time.Now().Add(time.Millisecond * 100) + } else if li.lastWidth != gtx.Constraints.Max.X && li.notFirst { + li.recalculateTime = time.Now().Add(time.Millisecond * 100) + li.recalculate = true + } + if !li.notFirst { + li.recalculateTime = time.Now().Add(-time.Millisecond * 100) + li.notFirst = true + } + li.lastWidth = gtx.Constraints.Max.X + if li.recalculateTime.Sub(time.Now()) < 0 && li.recalculate { + li.scrollBarSize = li.scrollWidth // + li.scrollBarPad + gtx1 := CopyContextDimensionsWithMaxAxis(gtx, li.axis) + // generate the dimensions for all the list elements + li.dims = GetDimensionList(gtx1, li.length, li.w) + li.recalculateTime = time.Time{} + li.recalculate = false + } + _, li.view = axisMainConstraint(li.axis, gtx.Constraints) + _, li.cross = axisCrossConstraint(li.axis, gtx.Constraints) + li.total, li.before = li.dims.GetSizes(li.position, li.axis) + if li.total == 0 { + // if there is no children just return a big empty box + return EmptyFromSize(gtx.Constraints.Max)(gtx) + } + if li.total < li.view { + // if the contents fit the view, don't show the scrollbar + li.top, li.middle, li.bottom = 0, 0, 0 + li.scrollWidth = 0 + } else { + li.scrollWidth = li.setScrollWidth + li.top = li.before * (li.view - li.scrollWidth) / li.total + li.middle = li.view * (li.view - li.scrollWidth) / li.total + li.bottom = (li.total - li.before - li.view) * (li.view - li.scrollWidth) / li.total + if li.view < li.scrollWidth { + li.middle = li.view + li.top, li.bottom = 0, 0 + } else { + li.middle += li.scrollWidth + } + } + // now lay it all out and draw the list and scrollbar + var container l.Widget + if li.axis == l.Horizontal { + containerFlex := li.Theme.VFlex() + if !li.leftSide { + containerFlex.Rigid(li.embedWidget(li.scrollWidth /* + int(li.TextSize.True)/4)*/)) + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, int(li.TextSize.V)/4)) + } + containerFlex.Rigid( + li.VFlex(). + Rigid( + func(gtx l.Context) l.Dimensions { + pointer.Rect(image.Rectangle{Max: image.Point{X: gtx.Constraints.Max.X, + Y: gtx.Constraints.Max.Y, + }, + }, + ).Add(gtx.Ops) + li.drag.Add(gtx.Ops) + return li.Theme.Flex(). + Rigid(li.pageUpDown(li.dims, li.view, li.total, + // li.scrollBarPad+ + li.scrollWidth, li.top, false, + ), + ). + Rigid(li.grabber(li.dims, li.scrollWidth, li.middle, + li.view, gtx.Constraints.Max.X, + ), + ). + Rigid(li.pageUpDown(li.dims, li.view, li.total, + // li.scrollBarPad+ + li.scrollWidth, li.bottom, true, + ), + ). + Fn(gtx) + }, + ). + Fn, + ) + if li.leftSide { + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, int(li.TextSize.V)/4)) + containerFlex.Rigid(li.embedWidget(li.scrollWidth)) // li.scrollWidth)) // + li.scrollBarPad)) + } + container = containerFlex.Fn + } else { + containerFlex := li.Theme.Flex() + if !li.leftSide { + containerFlex.Rigid(li.embedWidget(li.scrollWidth + int(li.TextSize.V)/2)) // + li.scrollBarPad)) + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, int(li.TextSize.V)/2)) + } + containerFlex.Rigid( + li.Fill(li.background, l.Center, li.TextSize.V/4, 0, li.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + pointer.Rect(image.Rectangle{Max: image.Point{X: gtx.Constraints.Max.X, + Y: gtx.Constraints.Max.Y, + }, + }, + ).Add(gtx.Ops) + li.drag.Add(gtx.Ops) + return li.Theme.Flex().Vertical(). + Rigid(li.pageUpDown(li.dims, li.view, li.total, + li.scrollWidth, li.top, false, + ), + ). + Rigid(li.grabber(li.dims, + li.scrollWidth, li.middle, + li.view, gtx.Constraints.Max.X, + ), + ). + Rigid(li.pageUpDown(li.dims, li.view, li.total, + li.scrollWidth, li.bottom, true, + ), + ). + Fn(gtx) + }, + ). + Fn, + ).Fn, + ) + if li.leftSide { + containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, int(li.TextSize.V)/2)) + containerFlex.Rigid(li.embedWidget(li.scrollWidth + int(li.TextSize.V)/2)) + } + container = li.Fill(li.background, l.Center, li.TextSize.V/4, 0, containerFlex.Fn).Fn + } + return container(gtx) +} + +// EmbedWidget places the scrollable content +func (li *List) embedWidget(scrollWidth int) func(l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + if li.axis == l.Horizontal { + gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - scrollWidth + gtx.Constraints.Max.Y = gtx.Constraints.Min.Y + } else { + gtx.Constraints.Min.X = gtx.Constraints.Max.X - scrollWidth + gtx.Constraints.Max.X = gtx.Constraints.Min.X + } + return li.Layout(gtx, li.length, li.w) + } +} + +// pageUpDown creates the clickable areas either side of the grabber that trigger a page up/page down action +func (li *List) pageUpDown(dims DimensionList, view, total, x, y int, down bool) func(l.Context) l.Dimensions { + button := li.pageUp + if down { + button = li.pageDown + } + return func(gtx l.Context) l.Dimensions { + bounds := image.Rectangle{Max: gtx.Constraints.Max} + pointer.Rect(bounds).Add(gtx.Ops) + li.sideScroll.Add(gtx.Ops, bounds) + return li.ButtonLayout(button.SetClick(func() { + current := dims.PositionToCoordinate(li.position, li.axis) + var newPos int + if down { + if current+view > total { + newPos = total - view + } else { + newPos = current + view + } + } else { + newPos = current - view + if newPos < 0 { + newPos = 0 + } + } + li.position = dims.CoordinateToPosition(newPos, li.axis) + }, + ). + SetPress(func() { li.recentPageClick = time.Now() }), + ).Embed( + li.Flex(). + Rigid(EmptySpace(x/4, y)). + Rigid( + li.Fill("scrim", l.Center, li.TextSize.V/4, 0, EmptySpace(x/2, y)).Fn, + ). + Rigid(EmptySpace(x/4, y)). + Fn, + ).Background("Transparent").CornerRadius(0).Fn(gtx) + } +} + +// grabber renders the grabber +func (li *List) grabber(dims DimensionList, x, y, viewAxis, viewCross int) func(l.Context) l.Dimensions { + return func(gtx l.Context) l.Dimensions { + ax := gesture.Vertical + if li.axis == l.Horizontal { + ax = gesture.Horizontal + } + var de *pointer.Event + for _, ev := range li.drag.Events(gtx.Metric, gtx, ax) { + if ev.Type == pointer.Press || + ev.Type == pointer.Release || + ev.Type == pointer.Drag { + de = &ev + } + } + if de != nil { + if de.Type == pointer.Press { // || de.Type == pointer.Drag { + } + if de.Type == pointer.Release { + } + if de.Type == pointer.Drag { + // D.Ln("drag position", de.Position) + if time.Now().Sub(li.recentPageClick) > time.Second/2 { + total := dims.GetTotal(li.axis) + var d int + if li.axis == l.Horizontal { + deltaX := int(de.Position.X) + if deltaX > 8 || deltaX < -8 { + d = deltaX * (total / viewAxis) + li.SetPosition(dims.CoordinateToPosition(d, li.axis)) + } + } else { + deltaY := int(de.Position.Y) + if deltaY > 8 || deltaY < -8 { + d = deltaY * (total / viewAxis) + li.SetPosition(dims.CoordinateToPosition(d, li.axis)) + } + } + } + li.Window.Invalidate() + } + } + defer op.Save(gtx.Ops).Load() + bounds := image.Rectangle{Max: image.Point{X: x, Y: y}} + pointer.Rect(bounds).Add(gtx.Ops) + li.sideScroll.Add(gtx.Ops, bounds) + return li.Flex(). + Rigid( + li.Fill(li.currentColor, l.Center, 0, 0, EmptySpace(x, y)). + Fn, + ). + Fn(gtx) + } +} diff --git a/pkg/gel/log.go b/pkg/gel/log.go new file mode 100644 index 0000000..1e21ce2 --- /dev/null +++ b/pkg/gel/log.go @@ -0,0 +1,9 @@ +package gel + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/pkg/gel/modal.go b/pkg/gel/modal.go new file mode 100644 index 0000000..361cd9f --- /dev/null +++ b/pkg/gel/modal.go @@ -0,0 +1 @@ +package gel diff --git a/pkg/gel/multi.go b/pkg/gel/multi.go new file mode 100644 index 0000000..ec8bdc5 --- /dev/null +++ b/pkg/gel/multi.go @@ -0,0 +1,443 @@ +package gel + +import ( + l "github.com/p9c/p9/pkg/gel/gio/layout" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +type Multi struct { + *Window + lines *[]string + clickables []*Clickable + buttons []*ButtonLayout + input *Input + inputLocation int + addClickable *Clickable + removeClickables []*Clickable + removeButtons []*IconButton + handle func(txt []string) +} + +func (w *Window) Multiline( + txt *[]string, + borderColorFocused, borderColorUnfocused, backgroundColor string, + size float32, + handle func(txt []string), +) (m *Multi) { + if handle == nil { + handle = func(txt []string) { + D.Ln(txt) + } + } + addClickable := w.Clickable() + m = &Multi{ + Window: w, + lines: txt, + inputLocation: -1, + addClickable: addClickable, + handle: handle, + } + handleChange := func(txt string) { + D.Ln("handleChange", m.inputLocation) + (*m.lines)[m.inputLocation] = txt + // after submit clear the editor + m.inputLocation = -1 + m.handle(*m.lines) + } + m.input = w.Input("", "", borderColorFocused, borderColorUnfocused, backgroundColor, handleChange, nil) + m.clickables = append(m.clickables, (*Clickable)(nil)) + // m.buttons = append(m.buttons, (*ButtonLayout)(nil)) + m.removeClickables = append(m.removeClickables, (*Clickable)(nil)) + m.removeButtons = append(m.removeButtons, (*IconButton)(nil)) + for i := range *m.lines { + // D.Ln("making clickables") + x := i + clickable := m.Clickable().SetClick( + func() { + m.inputLocation = x + D.Ln("button clicked", x, m.inputLocation) + }) + if len(*m.lines) > len(m.clickables) { + m.clickables = append(m.clickables, clickable) + } else { + m.clickables[i] = clickable + } + // D.Ln("making button") + btn := m.ButtonLayout(clickable).CornerRadius(0).Background( + backgroundColor). + Embed( + m.Theme.Flex().AlignStart(). + Flexed(1, + m.Fill("Primary", l.Center, m.TextSize.V, 0, m.Inset(0.25, + m.Body2((*m.lines)[i]).Color("DocText").Fn, + ).Fn).Fn, + ).Fn, + ) + if len(*m.lines) > len(m.buttons) { + m.buttons = append(m.buttons, btn) + } else { + m.buttons[i] = btn + } + // D.Ln("making clickables") + removeClickable := m.Clickable() + if len(*m.lines) > len(m.removeClickables) { + m.removeClickables = append(m.removeClickables, removeClickable) + } else { + m.removeClickables[i] = removeClickable + } + // D.Ln("making remove button") + y := i + removeBtn := m.IconButton(removeClickable). + Icon( + m.Icon().Scale(1.5).Color("DocText").Src(&icons.ActionDelete), + ). + Background(""). + SetClick(func() { + D.Ln("remove button", y, "clicked", len(*m.lines)) + m.inputLocation = -1 + if len(*m.lines)-1 == y { + *m.lines = (*m.lines)[:len(*m.lines)-1] + } else if len(*m.lines)-2 == y { + *m.lines = (*m.lines)[:len(*m.lines)-2] + } else { + *m.lines = append((*m.lines)[:y+1], (*m.lines)[y+2:]...) + } + m.handle(*m.lines) + // D.Ln("remove button", i, "clicked") + // m.inputLocation = -1 + // ll := len(*m.lines)-1 + // if i == ll { + // *m.lines = (*m.lines)[:len(*m.lines)-1] + // m.clickables = m.clickables[:len(m.clickables)-1] + // m.buttons = m.buttons[:len(m.buttons)-1] + // m.removeClickables = m.removeClickables[:len(m.removeClickables)-1] + // m.removeButtons = m.removeButtons[:len(m.removeButtons)-1] + // } else { + // if len(*m.lines)-1 < i { + // return + // } + // *m.lines = append((*m.lines)[:i], (*m.lines)[i+1:]...) + // m.clickables = append(m.clickables[:i], m.clickables[i+1:]...) + // m.buttons = append(m.buttons[:i], m.buttons[i+1:]...) + // m.removeClickables = append(m.removeClickables[:i], m.removeClickables[i+1:]...) + // m.removeButtons = append(m.removeButtons[:i], m.removeButtons[i+1:]...) + // } + }) + if len(*m.lines) > len(m.removeButtons) { + m.removeButtons = append(m.removeButtons, removeBtn) + } else { + m.removeButtons[x] = removeBtn + } + } + return m +} + +func (m *Multi) UpdateWidgets() *Multi { + if len(m.clickables) < len(*m.lines) { + D.Ln("allocating new clickables") + m.clickables = append(m.clickables, (*Clickable)(nil)) + } + if len(m.buttons) < len(*m.lines) { + D.Ln("allocating new buttons") + m.buttons = append(m.buttons, (*ButtonLayout)(nil)) + } + if len(m.removeClickables) < len(*m.lines) { + D.Ln("allocating new removeClickables") + m.removeClickables = append(m.clickables, (*Clickable)(nil)) + } + if len(m.removeButtons) < len(*m.lines) { + D.Ln("allocating new removeButtons") + m.removeButtons = append(m.removeButtons, (*IconButton)(nil)) + } + return m +} + +func (m *Multi) PopulateWidgets() *Multi { + added := false + for i := range *m.lines { + if m.clickables[i] == nil { + added = true + D.Ln("making clickables", i) + x := i + m.clickables[i] = m.Clickable().SetClick( + func() { + D.Ln("clicked", x, m.inputLocation) + m.inputLocation = x + m.input.editor.SetText((*m.lines)[x]) + m.input.editor.Focus() + // m.input.editor.SetFocus(func(is bool) { + // if !is { + // m.inputLocation = -1 + // } + // }) + }) + } + // m.clickables[i] + if m.buttons[i] == nil { + added = true + btn := m.ButtonLayout(m.clickables[i]).CornerRadius(0).Background("Transparent") + m.buttons[i] = btn + } + m.buttons[i].Embed( + m.Theme.Flex(). + Rigid( + m.Inset(0.25, + m.Body2((*m.lines)[i]).Color("DocText").Fn, + ).Fn, + ).Fn, + ) + if m.removeClickables[i] == nil { + added = true + removeClickable := m.Clickable() + m.removeClickables[i] = removeClickable + } + if m.removeButtons[i] == nil { + added = true + D.Ln("making remove button", i) + x := i + m.removeButtons[i] = m.IconButton(m.removeClickables[i]. + SetClick(func() { + D.Ln("remove button", x, "clicked", len(*m.lines)) + m.inputLocation = -1 + if len(*m.lines)-1 == i { + *m.lines = (*m.lines)[:len(*m.lines)-1] + } else { + *m.lines = append((*m.lines)[:x], (*m.lines)[x+1:]...) + } + m.handle(*m.lines) + })). + Icon( + m.Icon().Scale(1.5).Color("DocText").Src(&icons.ActionDelete), + ). + Background("") + } + } + if added { + D.Ln("clearing editor") + m.input.editor.SetText("") + m.input.editor.Focus() + } + return m +} + +func (m *Multi) Fn(gtx l.Context) l.Dimensions { + m.UpdateWidgets() + m.PopulateWidgets() + addButton := m.IconButton(m.addClickable).Icon( + m.Icon().Scale(1.5).Color("Primary").Src(&icons.ContentAdd), + ) + var widgets []l.Widget + if m.inputLocation > 0 && m.inputLocation < len(*m.lines) { + m.input.Editor().SetText((*m.lines)[m.inputLocation]) + } + for i := range *m.lines { + if m.buttons[i] == nil { + x := i + btn := m.ButtonLayout(m.clickables[i].SetClick( + func() { + D.Ln("button pressed", (*m.lines)[x], x, m.inputLocation) + m.inputLocation = x + m.input.editor.SetText((*m.lines)[x]) + m.input.editor.Focus() + })).CornerRadius(0).Background("Transparent"). + Embed( + m.Theme.Flex(). + Rigid( + m.Inset(0.25, + m.Body2((*m.lines)[x]).Color("DocText").Fn, + ).Fn, + ).Fn, + ) + m.buttons[i] = btn + } + if i == m.inputLocation { + m.input.Editor().SetText((*m.lines)[i]) + input := m.Flex(). + Rigid( + m.removeButtons[i].Fn, + ). + Flexed(1, + m.input.Fn, + ). + Fn + widgets = append(widgets, input) + } else { + x := i + m.clickables[i].SetClick( + func() { + D.Ln("setting", x, m.inputLocation) + m.inputLocation = x + m.input.editor.SetText((*m.lines)[x]) + m.input.editor.Focus() + }) + button := m.Flex().AlignStart(). + Rigid( + m.removeButtons[i].Fn, + ). + Flexed(1, + m.buttons[i].Fn, + ). + Fn + widgets = append(widgets, button) + } + } + widgets = append(widgets, addButton.SetClick(func() { + D.Ln("clicked add") + *m.lines = append(*m.lines, "") + m.inputLocation = len(*m.lines) - 1 + D.S([]string(*m.lines)) + m.UpdateWidgets() + m.PopulateWidgets() + m.input.editor.SetText("") + m.input.editor.Focus() + }).Background("").Fn) + // m.UpdateWidgets() + // m.PopulateWidgets() + // D.Ln(m.inputLocation) + // if m.inputLocation > 0 { + // m.input.Editor().Focus() + // } + out := m.Theme.VFlex() + for i := range widgets { + out.Rigid(widgets[i]) + } + return out.Fn(gtx) +} + +func (m *Multi) Widgets() (widgets []l.Widget) { + m.UpdateWidgets() + m.PopulateWidgets() + if m.inputLocation > 0 && m.inputLocation < len(*m.lines) { + m.input.Editor().SetText((*m.lines)[m.inputLocation]) + } + focusFunc := func(is bool) { + mi := m.inputLocation + D.Ln("editor", "is focused", is) + // debug.PrintStack() + if !is { + m.input.borderColor = m.input.borderColorUnfocused + // submit the current edit if any + txt := m.input.editor.Text() + cur := (*m.lines)[m.inputLocation] + if txt != cur { + D.Ln("changed text") + // run submit hook + m.input.editor.submitHook(txt) + } else { + D.Ln("text not changed") + // When a new item is added this unfocus event occurs and this makes it behave correctly + // Normally the editor would not be rendered if not focused so setting it to focus does no harm in the + // case of switching to another + } + // m.inputLocation = -1 + } else { + m.input.borderColor = m.input.borderColorFocused + m.inputLocation = mi + // m.input.editor.Focus() + } + } + m.input.editor.SetFocus(focusFunc) + for ii := range *m.lines { + i := ii + // D.Ln("iterating lines", i, len(*m.lines)) + if m.buttons[i] == nil { + D.Ln("making new button layout") + btn := m.ButtonLayout(m.clickables[i].SetClick( + func() { + D.Ln("button pressed", (*m.lines)[i], i, m.inputLocation) + m.UpdateWidgets() + m.PopulateWidgets() + m.inputLocation = i + m.input.editor.SetText((*m.lines)[i]) + m.input.editor.Focus() + })).CornerRadius(0).Background(""). + Embed( + func(gtx l.Context) l.Dimensions { + return m.Theme.Flex(). + Flexed(1, + m.Inset(0.25, + m.Body2((*m.lines)[i]).Color("DocText").Fn, + ).Fn, + ).Fn(gtx) + }, + ) + m.buttons[i] = btn + } + if i == m.inputLocation { + // x := i + // D.Ln("rendering editor", x) + + input := func(gtx l.Context) l.Dimensions { + return m.Inset(0.25, + m.Flex(). + Rigid( + m.removeButtons[i].Fn, + ). + Flexed(1, + m.input.Fn, + ). + Fn, + ). + Fn(gtx) + } + widgets = append(widgets, input) + } else { + // D.Ln("rendering button", i) + m.clickables[i].SetClick( + func() { + m.UpdateWidgets() + m.PopulateWidgets() + m.inputLocation = i + m.input.editor.SetText((*m.lines)[i]) + m.input.editor.Focus() + D.Ln("setting", i, m.inputLocation) + }) + button := func(gtx l.Context) l.Dimensions { + return m.Inset(0.25, + m.Flex().AlignStart(). + Rigid( + m.removeButtons[i].Fn, + ). + Rigid( + m.buttons[i].Fn, + ). + Flexed(1, EmptyMaxWidth()). + Fn, + ).Fn(gtx) + } + widgets = append(widgets, button) + } + } + // D.Ln("widgets", widgets) + addButton := func(gtx l.Context) l.Dimensions { + addb := + m.Inset(0.25, + m.Theme.Flex().AlignStart(). + Rigid( + m.IconButton( + m.addClickable). + Icon( + m.Icon().Scale(1.5).Color("Primary").Src(&icons.ContentAdd), + ). + SetClick(func() { + D.Ln("clicked add") + m.inputLocation = len(*m.lines) + *m.lines = append(*m.lines, "") + m.input.editor.SetText("") + D.S([]string(*m.lines)) + m.UpdateWidgets() + m.PopulateWidgets() + m.input.editor.Focus() + }). + Background("Transparent"). + Fn, + ). + Flexed(1, EmptyMaxWidth()). + Fn, + ).Fn + widgets = append(widgets, addb) + return addb(gtx) + } + widgets = append(widgets, addButton) + return +} diff --git a/pkg/gel/password.go b/pkg/gel/password.go new file mode 100644 index 0000000..876be54 --- /dev/null +++ b/pkg/gel/password.go @@ -0,0 +1,192 @@ +package gel + +import ( + "github.com/p9c/p9/pkg/opts/text" + icons2 "golang.org/x/exp/shiny/materialdesign/icons" + + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +type Password struct { + *Window + pass *Editor + passInput *TextInput + unhideClickable *Clickable + unhideButton *IconButton + copyClickable *Clickable + copyButton *IconButton + pasteClickable *Clickable + pasteButton *IconButton + hide bool + borderColor string + borderColorUnfocused string + borderColorFocused string + backgroundColor string + focused bool + showClickableFn func(col string) + password *text.Opt + handle func(pass string) +} + +func (w *Window) Password( + hint string, password *text.Opt, borderColorFocused, + borderColorUnfocused, backgroundColor string, handle func(pass string), +) *Password { + pass := w.Editor().Mask('•').SingleLine().Submit(true) + passInput := w.TextInput(pass, hint).Color(borderColorUnfocused) + p := &Password{ + Window: w, + unhideClickable: w.Clickable(), + copyClickable: w.Clickable(), + pasteClickable: w.Clickable(), + pass: pass, + passInput: passInput, + borderColorUnfocused: borderColorUnfocused, + borderColorFocused: borderColorFocused, + borderColor: borderColorUnfocused, + backgroundColor: backgroundColor, + handle: handle, + password: password, + } + p.copyButton = w.IconButton(p.copyClickable) + p.pasteButton = w.IconButton(p.pasteClickable) + p.unhideButton = w.IconButton(p.unhideClickable).Background(""). + Icon(w.Icon().Color(p.borderColor).Src(&icons2.ActionVisibility)) + p.showClickableFn = func(col string) { + D.Ln("show clickable clicked") + p.hide = !p.hide + } + copyClickableFn := func() { + p.ClipboardWriteReqs <- p.pass.Text() + p.pass.Focus() + } + pasteClickableFn := func() { + p.ClipboardReadReqs <- func(cs string) { + cs = findSpaceRegexp.ReplaceAllString(cs, " ") + p.pass.Insert(cs) + p.pass.changeHook(cs) + p.pass.Focus() + } + } + p.copyClickable.SetClick(copyClickableFn) + p.pasteClickable.SetClick(pasteClickableFn) + p.unhideButton. + // Color("Primary"). + Icon( + w.Icon(). + Color(p.borderColor). + Src(&icons2.ActionVisibility), + ) + p.pass.Mask('•') + p.pass.SetFocus( + func(is bool) { + if is { + p.borderColor = p.borderColorFocused + } else { + p.borderColor = p.borderColorUnfocused + p.hide = true + } + }, + ) + p.passInput.editor.Mask('•') + p.hide = true + p.passInput.Color(p.borderColor) + p.pass.SetText(p.password.V()).Mask('•').SetSubmit( + func(txt string) { + // if !p.hide { + // p.showClickableFn(p.borderColor) + // } + // p.showClickableFn(p.borderColor) + go func() { + p.handle(txt) + }() + }, + ).SetChange( + func(txt string) { + // send keystrokes to the NSA + }, + ) + return p +} + +func (p *Password) Fn(gtx l.Context) l.Dimensions { + // gtx.Constraints.Max.X = int(p.TextSize.Scale(float32(p.size)).True) + // gtx.Constraints.Min.X = 0 + // cs := gtx.Constraints + // width := int(p.Theme.TextSize.Scale(p.size).True) + // gtx.Constraints.Max.X, gtx.Constraints.Min.X = width, width + return func(gtx l.Context) l.Dimensions { + p.passInput.Color(p.borderColor).Font("go regular") + p.unhideButton.Color(p.borderColor) + p.unhideClickable.SetClick(func() { p.showClickableFn(p.borderColor) }) + visIcon := &icons2.ActionVisibility + if p.hide { + p.pass.Mask('•') + } else { + visIcon = &icons2.ActionVisibilityOff + p.pass.Mask(0) + } + + return p.Border(). + Width(0.125). + CornerRadius(0.0). + Color(p.borderColor).Embed( + p.Fill( + p.backgroundColor, l.Center, 0, 0, + p.Inset( + 0.25, + p.Flex(). + Flexed( + 1, + p.Inset(0.25, p.passInput.Color(p.borderColor).HintColor(p.borderColorUnfocused).Fn).Fn, + ). + Rigid( + p.copyButton. + Background(""). + Icon(p.Icon().Color(p.borderColor).Scale(Scales["H6"]).Src(&icons2.ContentContentCopy)). + ButtonInset(0.25). + Fn, + ). + Rigid( + p.pasteButton. + Background(""). + Icon(p.Icon().Color(p.borderColor).Scale(Scales["H6"]).Src(&icons2.ContentContentPaste)). + ButtonInset(0.25). + Fn, + ). + Rigid( + p.unhideButton. + Background(""). + Icon(p.Icon().Color(p.borderColor).Src(visIcon)).Fn, + ). + Fn, + ).Fn, + ).Fn, + ).Fn(gtx) + }(gtx) +} + +func (p *Password) GetPassword() string { + return p.passInput.editor.Text() +} + +func (p *Password) Wipe() { + p.passInput.editor.editBuffer.Zero() + p.passInput.editor.SetText("") +} + +func (p *Password) Focus() { + p.passInput.editor.Focus() +} + +func (p *Password) Blur() { + p.passInput.editor.focused = false +} + +func (p *Password) Hide() { + p.passInput.editor.Mask('*') +} + +func (p *Password) Show() { + p.passInput.editor.Mask(0) +} diff --git a/pkg/gel/poolgen/log.go b/pkg/gel/poolgen/log.go new file mode 100644 index 0000000..58579ab --- /dev/null +++ b/pkg/gel/poolgen/log.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/pkg/gel/poolgen/poolgen.go b/pkg/gel/poolgen/poolgen.go new file mode 100644 index 0000000..7895b4a --- /dev/null +++ b/pkg/gel/poolgen/poolgen.go @@ -0,0 +1,69 @@ +package main + +import ( + "os" +) + +type poolType struct { + name, sliceName, constructor string +} + +var types = []poolType{ + {"Bool", "bools", "Bool(false)"}, + {"List", "lists", "List()"}, + {"Checkable", "checkables", "Checkable()"}, + {"Clickable", "clickables", "Clickable()"}, + {"Editor", "editors", "Editor()"}, + {"IncDec", "incDecs", "IncDec()"}, + // {"Stack", "stacks", "Stack()"}, +} + +func main() { + var out string + out += `// generated by go run github.com/p9c/p9/pkg/gel/poolgen/poolgen.go; DO NOT EDIT +// +`+`//go:generate go run ./poolgen/. + +package gel +` + for i := range types { + out += ` +func (p *Pool) Get` + types[i].name + `() (out *` + types[i].name + `) { + if len(p.` + types[i].sliceName + `) >= p.` + types[i].sliceName + `InUse { + for i := 0; i < 10; i++ { + p.` + types[i].sliceName + ` = append(p.` + types[i].sliceName + `, p.` + types[i].constructor + `) + } + } + out = p.` + types[i].sliceName + `[p.` + types[i].sliceName + `InUse] + p.` + types[i].sliceName + `InUse++ + return +} + +func (p *Pool) Free` + types[i].name + `(b *` + types[i].name + `) { + for i := 0; i < p.` + types[i].sliceName + `InUse; i++ { + if p.` + types[i].sliceName + `[i] == b { + if i != p.` + types[i].sliceName + `InUse-1 { + // move the item to the end. the next allocation will be then at index p.` + types[i].sliceName + `InUse + tmp := p.` + types[i].sliceName + `[i] + p.` + types[i].sliceName + ` = append(p.` + types[i].sliceName + `[:i], p.` + types[i].sliceName + `[i+1:]...) + p.` + types[i].sliceName + ` = append(p.` + types[i].sliceName + `, tmp) + p.` + types[i].sliceName + `InUse-- + break + } + } + } +} + +` + } + if fd, e := os.Create("pooltypes.go"); E.Chk(e) { + } else { + defer func() { + if e = fd.Close(); E.Chk(e) { + } + }() + + if _, e = fd.Write([]byte(out)); E.Chk(e) { + } + } +} diff --git a/pkg/gel/pools.go b/pkg/gel/pools.go new file mode 100644 index 0000000..c6bfe57 --- /dev/null +++ b/pkg/gel/pools.go @@ -0,0 +1,30 @@ +package gel + +type Pool struct { + *Window + bools []*Bool + boolsInUse int + lists []*List + listsInUse int + checkables []*Checkable + checkablesInUse int + clickables []*Clickable + clickablesInUse int + editors []*Editor + editorsInUse int + incDecs []*IncDec + incDecsInUse int +} + +func (w *Window) NewPool() *Pool { + return &Pool{Window: w} +} + +func (p *Pool) Reset() { + p.boolsInUse = 0 + p.listsInUse = 0 + p.checkablesInUse = 0 + p.clickablesInUse = 0 + p.editorsInUse = 0 + p.incDecsInUse = 0 +} diff --git a/pkg/gel/pooltypes.go b/pkg/gel/pooltypes.go new file mode 100644 index 0000000..ad9c91a --- /dev/null +++ b/pkg/gel/pooltypes.go @@ -0,0 +1,167 @@ +// generated by go run github.com/p9c/p9/pkg/gel/poolgen/poolgen.go; DO NOT EDIT +// +//go:generate go run ./poolgen/. + +package gel + +func (p *Pool) GetBool() (out *Bool) { + if len(p.bools) >= p.boolsInUse { + for i := 0; i < 10; i++ { + p.bools = append(p.bools, p.Bool(false)) + } + } + out = p.bools[p.boolsInUse] + p.boolsInUse++ + return +} + +func (p *Pool) FreeBool(b *Bool) { + for i := 0; i < p.boolsInUse; i++ { + if p.bools[i] == b { + if i != p.boolsInUse-1 { + // move the item to the end. the next allocation will be then at index p.boolsInUse + tmp := p.bools[i] + p.bools = append(p.bools[:i], p.bools[i+1:]...) + p.bools = append(p.bools, tmp) + p.boolsInUse-- + break + } + } + } +} + + +func (p *Pool) GetList() (out *List) { + if len(p.lists) >= p.listsInUse { + for i := 0; i < 10; i++ { + p.lists = append(p.lists, p.List()) + } + } + out = p.lists[p.listsInUse] + p.listsInUse++ + return +} + +func (p *Pool) FreeList(b *List) { + for i := 0; i < p.listsInUse; i++ { + if p.lists[i] == b { + if i != p.listsInUse-1 { + // move the item to the end. the next allocation will be then at index p.listsInUse + tmp := p.lists[i] + p.lists = append(p.lists[:i], p.lists[i+1:]...) + p.lists = append(p.lists, tmp) + p.listsInUse-- + break + } + } + } +} + + +func (p *Pool) GetCheckable() (out *Checkable) { + if len(p.checkables) >= p.checkablesInUse { + for i := 0; i < 10; i++ { + p.checkables = append(p.checkables, p.Checkable()) + } + } + out = p.checkables[p.checkablesInUse] + p.checkablesInUse++ + return +} + +func (p *Pool) FreeCheckable(b *Checkable) { + for i := 0; i < p.checkablesInUse; i++ { + if p.checkables[i] == b { + if i != p.checkablesInUse-1 { + // move the item to the end. the next allocation will be then at index p.checkablesInUse + tmp := p.checkables[i] + p.checkables = append(p.checkables[:i], p.checkables[i+1:]...) + p.checkables = append(p.checkables, tmp) + p.checkablesInUse-- + break + } + } + } +} + + +func (p *Pool) GetClickable() (out *Clickable) { + if len(p.clickables) >= p.clickablesInUse { + for i := 0; i < 10; i++ { + p.clickables = append(p.clickables, p.Clickable()) + } + } + out = p.clickables[p.clickablesInUse] + p.clickablesInUse++ + return +} + +func (p *Pool) FreeClickable(b *Clickable) { + for i := 0; i < p.clickablesInUse; i++ { + if p.clickables[i] == b { + if i != p.clickablesInUse-1 { + // move the item to the end. the next allocation will be then at index p.clickablesInUse + tmp := p.clickables[i] + p.clickables = append(p.clickables[:i], p.clickables[i+1:]...) + p.clickables = append(p.clickables, tmp) + p.clickablesInUse-- + break + } + } + } +} + + +func (p *Pool) GetEditor() (out *Editor) { + if len(p.editors) >= p.editorsInUse { + for i := 0; i < 10; i++ { + p.editors = append(p.editors, p.Editor()) + } + } + out = p.editors[p.editorsInUse] + p.editorsInUse++ + return +} + +func (p *Pool) FreeEditor(b *Editor) { + for i := 0; i < p.editorsInUse; i++ { + if p.editors[i] == b { + if i != p.editorsInUse-1 { + // move the item to the end. the next allocation will be then at index p.editorsInUse + tmp := p.editors[i] + p.editors = append(p.editors[:i], p.editors[i+1:]...) + p.editors = append(p.editors, tmp) + p.editorsInUse-- + break + } + } + } +} + + +func (p *Pool) GetIncDec() (out *IncDec) { + if len(p.incDecs) >= p.incDecsInUse { + for i := 0; i < 10; i++ { + p.incDecs = append(p.incDecs, p.IncDec()) + } + } + out = p.incDecs[p.incDecsInUse] + p.incDecsInUse++ + return +} + +func (p *Pool) FreeIncDec(b *IncDec) { + for i := 0; i < p.incDecsInUse; i++ { + if p.incDecs[i] == b { + if i != p.incDecsInUse-1 { + // move the item to the end. the next allocation will be then at index p.incDecsInUse + tmp := p.incDecs[i] + p.incDecs = append(p.incDecs[:i], p.incDecs[i+1:]...) + p.incDecs = append(p.incDecs, tmp) + p.incDecsInUse-- + break + } + } + } +} + diff --git a/pkg/gel/progressbar.go b/pkg/gel/progressbar.go new file mode 100644 index 0000000..374e487 --- /dev/null +++ b/pkg/gel/progressbar.go @@ -0,0 +1,87 @@ +package gel + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "github.com/p9c/p9/pkg/gel/f32color" +) + +type ProgressBar struct { + *Window + color color.NRGBA + progress int +} + +// ProgressBar renders a horizontal bar with an indication of completion of a process +func (w *Window) ProgressBar() *ProgressBar { + return &ProgressBar{ + Window: w, + progress: 0, + color: w.Colors.GetNRGBAFromName("Primary"), + } +} + +// SetProgress sets the progress of the progress bar +func (p *ProgressBar) SetProgress(progress int) *ProgressBar { + p.progress = progress + return p +} + +// Color sets the color to render the bar in +func (p *ProgressBar) Color(c string) *ProgressBar { + p.color = p.Theme.Colors.GetNRGBAFromName(c) + return p +} + +// Fn renders the progress bar as it is currently configured +func (p *ProgressBar) Fn(gtx l.Context) l.Dimensions { + shader := func(width float32, color color.NRGBA) l.Dimensions { + maxHeight := unit.Dp(4) + rr := float32(gtx.Px(unit.Dp(2))) + + d := image.Point{X: int(width), Y: gtx.Px(maxHeight)} + + clip.RRect{ + Rect: f32.Rectangle{Max: f32.Point{X: width, Y: float32(gtx.Px(maxHeight))}}, + NE: rr, NW: rr, SE: rr, SW: rr, + }.Add(gtx.Ops) + + paint.ColorOp{Color: color}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + + return l.Dimensions{Size: d} + } + + progress := p.progress + if progress > 100 { + progress = 100 + } else if progress < 0 { + progress = 0 + } + + progressBarWidth := float32(gtx.Constraints.Max.X) + + return l.Stack{Alignment: l.W}.Layout(gtx, + l.Stacked(func(gtx l.Context) l.Dimensions { + // Use a transparent equivalent of progress color. + bgCol := f32color.MulAlpha(p.color, 150) + + return shader(progressBarWidth, bgCol) + }), + l.Stacked(func(gtx l.Context) l.Dimensions { + fillWidth := (progressBarWidth / 100) * float32(progress) + fillColor := p.color + if gtx.Queue == nil { + fillColor = f32color.MulAlpha(fillColor, 200) + } + return shader(fillWidth, fillColor) + }), + ) +} diff --git a/pkg/gel/radiobutton.go b/pkg/gel/radiobutton.go new file mode 100644 index 0000000..e9148ac --- /dev/null +++ b/pkg/gel/radiobutton.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gel + +import ( + "golang.org/x/exp/shiny/materialdesign/icons" + + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +type RadioButton struct { + *Checkable + *Window + key string + group *Enum +} + +// RadioButton returns a RadioButton with a label. The key specifies the value for the Enum. +func (w *Window) RadioButton(checkable *Checkable, group *Enum, key, + label string) *RadioButton { + // if checkable == nil { + // debug.PrintStack() + // os.Exit(0) + // } + return &RadioButton{ + group: group, + Window: w, + Checkable: checkable. + CheckedStateIcon(&icons.ToggleRadioButtonChecked). // Color("Primary"). + UncheckedStateIcon(&icons.ToggleRadioButtonUnchecked). // Color("PanelBg"). + Label(label), // .Color("DocText").IconColor("PanelBg"), + key: key, + } +} + +// Key sets the key initially active on the radiobutton +func (r *RadioButton) Key(key string) *RadioButton { + r.key = key + return r +} + +// Group sets the enum group of the radio button +func (r *RadioButton) Group(group *Enum) *RadioButton { + r.group = group + return r +} + +// Fn updates enum and displays the radio button. +func (r RadioButton) Fn(gtx l.Context) l.Dimensions { + dims := r.Checkable.Fn(gtx, r.group.Value() == r.key) + gtx.Constraints.Min = dims.Size + r.group.Fn(gtx, r.key) + return dims +} diff --git a/pkg/gel/responsive.go b/pkg/gel/responsive.go new file mode 100644 index 0000000..2fc9dba --- /dev/null +++ b/pkg/gel/responsive.go @@ -0,0 +1,56 @@ +package gel + +import ( + "sort" + + l "github.com/p9c/p9/pkg/gel/gio/layout" +) + +// WidgetSize is a widget with a specification of the minimum size to select it for viewing. +// Note that the widgets you put in here should be wrapped in func(l.Context) l.Dimensions otherwise +// any parameters retrieved from the controlling state variable will be from initialization and not +// at execution of the widget in the render process +type WidgetSize struct { + Size float32 + Widget l.Widget +} + +type Widgets []WidgetSize + +func (w Widgets) Len() int { + return len(w) +} + +func (w Widgets) Less(i, j int) bool { + // we want largest first so this uses greater than + return w[i].Size > w[j].Size +} + +func (w Widgets) Swap(i, j int) { + w[i], w[j] = w[j], w[i] +} + +type Responsive struct { + *Theme + Widgets + size int32 +} + +func (th *Theme) Responsive(size int32, widgets Widgets) *Responsive { + return &Responsive{Theme: th, size: size, Widgets: widgets} +} + +func (r *Responsive) Fn(gtx l.Context) l.Dimensions { + out := func(l.Context) l.Dimensions { + return l.Dimensions{} + } + sort.Sort(r.Widgets) + for i := range r.Widgets { + if float32(r.size)/r.TextSize.V >= r.Widgets[i].Size { + out = r.Widgets[i].Widget + // D.Ln("selected widget for responsive with scale", r.size, "width", r.Widgets[i].Size) + break + } + } + return out(gtx) +} diff --git a/pkg/gel/slider.go b/pkg/gel/slider.go new file mode 100644 index 0000000..fe5c975 --- /dev/null +++ b/pkg/gel/slider.go @@ -0,0 +1,132 @@ +package gel + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "github.com/p9c/p9/pkg/gel/f32color" +) + +type Slider struct { + *Window + min, max float32 + color color.NRGBA + float *Float +} + +// Slider is for selecting a value in a range. +func (w *Window) Slider() *Slider { + return &Slider{ + Window: w, + color: w.Colors.GetNRGBAFromName("Primary"), + } +} + +// Min sets the value at the left hand side +func (s *Slider) Min(min float32) *Slider { + s.min = min + return s +} + +// Max sets the value at the right hand side +func (s *Slider) Max(max float32) *Slider { + s.max = max + return s +} + +// Color sets the color to draw the slider in +func (s *Slider) Color(color string) *Slider { + s.color = s.Theme.Colors.GetNRGBAFromName(color) + return s +} + +// Float sets the initial value +func (s *Slider) Float(f *Float) *Slider { + s.float = f + return s +} + +// Fn renders the slider +func (s *Slider) Fn(gtx l.Context) l.Dimensions { + thumbRadiusInt := gtx.Px(unit.Dp(6)) + trackWidth := float32(gtx.Px(unit.Dp(2))) + thumbRadius := float32(thumbRadiusInt) + halfWidthInt := 2 * thumbRadiusInt + halfWidth := float32(halfWidthInt) + + size := gtx.Constraints.Min + // Keep a minimum length so that the track is always visible. + minLength := halfWidthInt + 3*thumbRadiusInt + halfWidthInt + if size.X < minLength { + size.X = minLength + } + size.Y = 2 * halfWidthInt + + st := op.Save(gtx.Ops) + op.Offset(f32.Pt(halfWidth, 0)).Add(gtx.Ops) + gtx.Constraints.Min = image.Pt(size.X-2*halfWidthInt, size.Y) + s.float.Fn(gtx, halfWidthInt, s.min, s.max) + thumbPos := halfWidth + s.float.Pos() + st.Load() + + col := s.color + if gtx.Queue == nil { + col = f32color.MulAlpha(col, 150) + } + + // Draw track before thumb. + st = op.Save(gtx.Ops) + track := f32.Rectangle{ + Min: f32.Point{ + X: halfWidth, + Y: halfWidth - trackWidth/2, + }, + Max: f32.Point{ + X: thumbPos, + Y: halfWidth + trackWidth/2, + }, + } + clip.RRect{Rect: track}.Add(gtx.Ops) + paint.ColorOp{Color: col}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Load() + + // Draw track after thumb. + st = op.Save(gtx.Ops) + track.Min.X = thumbPos + track.Max.X = float32(size.X) - halfWidth + clip.RRect{Rect: track}.Add(gtx.Ops) + paint.ColorOp{Color: f32color.MulAlpha(col, 96)}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Load() + + // Draw thumb. + st = op.Save(gtx.Ops) + thumb := f32.Rectangle{ + Min: f32.Point{ + X: thumbPos - thumbRadius, + Y: halfWidth - thumbRadius, + }, + Max: f32.Point{ + X: thumbPos + thumbRadius, + Y: halfWidth + thumbRadius, + }, + } + rr := thumbRadius + clip.RRect{ + Rect: thumb, + NE: rr, NW: rr, SE: rr, SW: rr, + }.Add(gtx.Ops) + paint.ColorOp{Color: col}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + st.Load() + + return l.Dimensions{Size: size} +} diff --git a/pkg/gel/stack.go b/pkg/gel/stack.go new file mode 100644 index 0000000..0c26abf --- /dev/null +++ b/pkg/gel/stack.go @@ -0,0 +1,41 @@ +package gel + +import l "github.com/p9c/p9/pkg/gel/gio/layout" + +type Stack struct { + *l.Stack + children []l.StackChild +} + +// Stack starts a chain of widgets to compose into a stack +func (w *Window) Stack() (out *Stack) { + out = &Stack{ + Stack: &l.Stack{}, + } + return +} + +func (s *Stack) Alignment(alignment l.Direction) *Stack { + s.Stack.Alignment = alignment + return s +} + +// functions to chain widgets to stack (first is lowest last highest) + +// Stacked appends a widget to the stack, the stack's dimensions will be +// computed from the largest widget in the stack +func (s *Stack) Stacked(w l.Widget) (out *Stack) { + s.children = append(s.children, l.Stacked(w)) + return s +} + +// Expanded lays out a widget with the same max constraints as the stack +func (s *Stack) Expanded(w l.Widget) (out *Stack) { + s.children = append(s.children, l.Expanded(w)) + return s +} + +// Fn runs the ops queue configured in the stack +func (s *Stack) Fn(c l.Context) l.Dimensions { + return s.Stack.Layout(c, s.children...) +} diff --git a/pkg/gel/switch.go b/pkg/gel/switch.go new file mode 100644 index 0000000..2d895a3 --- /dev/null +++ b/pkg/gel/switch.go @@ -0,0 +1,157 @@ +package gel + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + "github.com/p9c/p9/pkg/gel/gio/io/pointer" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "github.com/p9c/p9/pkg/gel/f32color" +) + +type Switch struct { + *Window + color struct { + enabled color.NRGBA + disabled color.NRGBA + } + swtch *Bool +} + +// Switch creates a boolean switch widget (basically a checkbox but looks like a switch) +func (w *Window) Switch(swtch *Bool) *Switch { + sw := &Switch{ + Window: w, + swtch: swtch, + } + sw.color.enabled = w.Colors.GetNRGBAFromName("Primary") + sw.color.disabled = w.Colors.GetNRGBAFromName("scrim") + return sw +} + +// EnabledColor sets the color to draw for the enabled state +func (s *Switch) EnabledColor(color string) *Switch { + s.color.enabled = s.Theme.Colors.GetNRGBAFromName(color) + return s +} + +// DisabledColor sets the color to draw for the disabled state +func (s *Switch) DisabledColor(color string) *Switch { + s.color.disabled = s.Theme.Colors.GetNRGBAFromName(color) + return s +} + +func (s *Switch) SetHook(fn func(b bool)) *Switch { + s.swtch.SetOnChange(fn) + return s +} + +// Fn updates the switch and displays it. +func (s *Switch) Fn(gtx l.Context) l.Dimensions { + return s.Inset(0.25, func(gtx l.Context) l.Dimensions { + trackWidth := gtx.Px(s.Theme.TextSize.Scale(2.5)) + trackHeight := gtx.Px(unit.Dp(16)) + thumbSize := gtx.Px(unit.Dp(20)) + trackOff := float32(thumbSize-trackHeight) * .5 + + // Draw track. + stack := op.Save(gtx.Ops) + trackCorner := float32(trackHeight) / 2 + trackRect := f32.Rectangle{Max: f32.Point{ + X: float32(trackWidth), + Y: float32(trackHeight), + }} + col := s.color.disabled + if s.swtch.value { + col = s.color.enabled + } + if gtx.Queue == nil { + col = f32color.MulAlpha(col, 150) + } + trackColor := f32color.MulAlpha(col, 200) + op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops) + clip.RRect{ + Rect: trackRect, + NE: trackCorner, NW: trackCorner, SE: trackCorner, SW: trackCorner, + }.Add(gtx.Ops) + paint.ColorOp{Color: trackColor}.Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + + // Draw thumb ink. + stack = op.Save(gtx.Ops) + inkSize := gtx.Px(unit.Dp(44)) + rr := float32(inkSize) * .5 + inkOff := f32.Point{ + X: float32(trackWidth)*.5 - rr, + Y: -rr + float32(trackHeight)*.5 + trackOff, + } + op.Offset(inkOff).Add(gtx.Ops) + gtx.Constraints.Min = image.Pt(inkSize, inkSize) + clip.RRect{ + Rect: f32.Rectangle{ + Max: l.FPt(gtx.Constraints.Min), + }, + NE: rr, NW: rr, SE: rr, SW: rr, + }.Add(gtx.Ops) + for _, p := range s.swtch.History() { + drawInk(gtx, p) + } + stack.Load() + + // Compute thumb offset and color. + stack = op.Save(gtx.Ops) + if s.swtch.value { + off := trackWidth - thumbSize + op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops) + } + + // Draw thumb shadow, a translucent disc slightly larger than the + // thumb itself. + shadowStack := op.Save(gtx.Ops) + shadowSize := float32(2) + // Center shadow horizontally and slightly adjust its Y. + op.Offset(f32.Point{X: -shadowSize / 2, Y: -.75}).Add(gtx.Ops) + drawDisc(gtx.Ops, float32(thumbSize)+shadowSize, color.NRGBA(argb(0x55000000))) + shadowStack.Load() + + // Draw thumb. + drawDisc(gtx.Ops, float32(thumbSize), col) + stack.Load() + + // Set up click area. + stack = op.Save(gtx.Ops) + clickSize := gtx.Px(unit.Dp(40)) + clickOff := f32.Point{ + X: (float32(trackWidth) - float32(clickSize)) * .5, + Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff, + } + op.Offset(clickOff).Add(gtx.Ops) + sz := image.Pt(clickSize, clickSize) + pointer.Ellipse(image.Rectangle{Max: sz}).Add(gtx.Ops) + gtx.Constraints.Min = sz + s.swtch.Fn(gtx) + stack.Load() + + dims := image.Point{X: trackWidth, Y: thumbSize} + return l.Dimensions{Size: dims} + }).Fn(gtx) +} + +func drawDisc(ops *op.Ops, sz float32, col color.NRGBA) { + defer op.Save(ops).Load() + rr := sz / 2 + r := f32.Rectangle{Max: f32.Point{X: sz, Y: sz}} + clip.RRect{ + Rect: r, + NE: rr, NW: rr, SE: rr, SW: rr, + }.Add(ops) + paint.ColorOp{Color: col}.Add(ops) + paint.PaintOp{}.Add(ops) +} diff --git a/pkg/gel/table.go b/pkg/gel/table.go new file mode 100644 index 0000000..9a93f49 --- /dev/null +++ b/pkg/gel/table.go @@ -0,0 +1,316 @@ +package gel + +import ( + "image" + "sort" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" +) + +type Cell struct { + l.Widget + dims l.Dimensions + computed bool + // priority only has meaning for the header row in defining an order of eliminating elements to fit a width. + // When trimming size to fit width add from highest to lowest priority and stop when dimensions exceed the target. + Priority int +} + +func (c *Cell) getWidgetDimensions(gtx l.Context) { + if c.Widget == nil { + // this happens when new items are added if a frame reads the cell, it just - can't - be rendered! + return + } + if c.computed { + return + } + // gather the dimensions of the list elements + gtx.Ops.Reset() + child := op.Record(gtx.Ops) + c.dims = c.Widget(gtx) + c.computed = true + _ = child.Stop() + return +} + +type CellRow []Cell + +func (c CellRow) GetPriority() (out CellPriorities) { + for i := range c { + var cp CellPriority + cp.Priority = c[i].Priority + cp.Column = i + out = append(out, cp) + } + sort.Sort(out) + return +} + +type CellPriority struct { + Column int + Priority int +} + +type CellPriorities []CellPriority + +// Len sorts a cell row by priority +func (c CellPriorities) Len() int { + return len(c) +} +func (c CellPriorities) Less(i, j int) bool { + return c[i].Priority < c[j].Priority +} +func (c CellPriorities) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +type CellGrid []CellRow + +// Table is a super simple table widget that finds the dimensions of all cells, sets all to max of each axis, and then +// scales the remaining space evenly +type Table struct { + *Window + header CellRow + body CellGrid + list *List + Y, X []int + headerBackground string + cellBackground string + reverse bool +} + +func (w *Window) Table() *Table { + return &Table{ + Window: w, + list: w.List(), + } +} + +func (t *Table) SetReverse(color string) *Table { + t.reverse = true + return t +} + +func (t *Table) HeaderBackground(color string) *Table { + t.headerBackground = color + return t +} + +func (t *Table) CellBackground(color string) *Table { + t.cellBackground = color + return t +} + +func (t *Table) Header(h CellRow) *Table { + t.header = h + return t +} + +func (t *Table) Body(g CellGrid) *Table { + t.body = g + return t +} + +func (t *Table) Fn(gtx l.Context) l.Dimensions { + // D.Ln(len(t.body), len(t.header)) + if len(t.header) == 0 { + return l.Dimensions{} + } + // if len(t.body) == 0 || len(t.header) == 0 { + // return l.Dimensions{} + // } + + for i := range t.body { + if len(t.header) != len(t.body[i]) { + // this should never happen hence panic + panic("not all rows are equal number of cells") + } + } + gtx1 := CopyContextDimensionsWithMaxAxis(gtx, l.Vertical) + gtx1.Constraints.Max = image.Point{X: Inf, Y: Inf} + // gather the dimensions from all cells + for i := range t.header { + t.header[i].getWidgetDimensions(gtx1) + } + // D.S(t.header) + for i := range t.body { + for j := range t.body[i] { + t.body[i][j].getWidgetDimensions(gtx1) + } + } + // D.S(t.body) + + // find the max of each row and column + var table CellGrid + table = append(table, t.header) + table = append(table, t.body...) + t.Y = make([]int, len(table)) + t.X = make([]int, len(table[0])) + for i := range table { + for j := range table[i] { + y := table[i][j].dims.Size.Y + if y > t.Y[i] { + t.Y[i] = y + } + x := table[i][j].dims.Size.X + if x > t.X[j] { + t.X[j] = x + } + } + } + // // D.S(t.Y) + // D.S(t.X) + var total int + for i := range t.X { + total += t.X[i] + } + // D.S(t.X) + // D.Ln(total) + maxWidth := gtx.Constraints.Max.X + for i := range t.X { + t.X[i] = int(float32(t.X[i]) * float32(maxWidth) / float32(total)) + } + // D.S(t.X) + // D.Ln(maxWidth) + // // find the columns that will be rendered into the existing width + // // D.S(t.header) + // priorities := t.header.GetPriority() + // // D.S(priorities) + // var runningTotal, prev int + // columnsToRender := make([]int, 0) + // for i := range priorities { + // prev = runningTotal + // x := t.header[priorities[i].Column].dims.Size.X + // // D.Ln(priorities[i], x) + // runningTotal += x + // + // if runningTotal > maxWidth { + // // D.Ln(runningTotal, prev, maxWidth) + // break + // } + // columnsToRender = append(columnsToRender, priorities[i].Column) + // } + // // txsort the columns to render into their original order + // txsort.Ints(columnsToRender) + // // D.S(columnsToRender) + // // D.Ln(len(columnsToRender)) + // // All fields will be expanded by the following ratio to reach the target width + // expansionFactor := float32(maxWidth) / float32(prev) + // outColWidths := make([]int, len(columnsToRender)) + // for i := range columnsToRender { + // outColWidths[i] = int(float32(t.X[columnsToRender[i]]) * expansionFactor) + // } + // // D.Ln(outColWidths) + // // assemble the grid to be rendered as a two dimensional slice + // grid := make([][]l.Widget, len(t.body)+1) + // for i := 0; i < len(columnsToRender); i++ { + // grid[0] = append(grid[0], t.header[columnsToRender[i]].Widget) + // } + // // for i := 0; i < len(columnsToRender); i++ { + // // for j := range t.body[i] { + // // grid[i+1] = append(grid[i+1], t.body[i][j].Widget) + // // } + // // } + // // D.S(grid) + // // assemble each row into a flex + // out := make([]l.Widget, len(grid)) + // for i := range grid { + // outFlex := t.Theme.Flex() + // for jj, j := range grid[i] { + // x := j + // _ = jj + // // outFlex.Rigid(x) + // outFlex.Rigid(func(gtx l.Context) l.Dimensions { + // // lock the cell to the calculated width. + // gtx.Constraints.Max.X = outColWidths[jj] + // gtx.Constraints.Min.X = gtx.Constraints.Max.X + // return x(gtx) + // }) + // } + // out[i] = outFlex.Fn + // } + header := t.Theme.Flex() // .SpaceEvenly() + for x, oi := range t.header { + i := x + // header is not in the list but drawn above it + oie := oi + txi := t.X[i] + tyi := t.Y[0] + header.Rigid(func(gtx l.Context) l.Dimensions { + cs := gtx.Constraints + cs.Max.X = txi + cs.Min.X = gtx.Constraints.Max.X + cs.Max.Y = tyi + cs.Min.Y = gtx.Constraints.Max.Y + // gtx.Constraints.Constrain(image.Point{X: txi, Y: tyi}) + dims := t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, EmptySpace(txi, tyi)).Fn(gtx) + oie.Widget(gtx) + return dims + }) + } + + var out CellGrid + out = CellGrid{t.header} + if t.reverse { + // append the body elements in reverse order stored + lb := len(t.body) - 1 + for i := range t.body { + out = append(out, t.body[lb-i]) + } + } else { + out = append(out, t.body...) + } + le := func(gtx l.Context, index int) l.Dimensions { + f := t.Theme.Flex() // .SpaceEvenly() + oi := out[index] + for x, oiee := range oi { + i := x + if index == 0 { + // we skip the header, not implemented but the header could be part of the scrollable area if need + // arises later, unwrap this block on a flag + } else { + if index >= len(t.Y) { + break + } + oie := oiee + txi := t.X[i] + tyi := t.Y[index] + f.Rigid(t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, func(gtx l.Context) l.Dimensions { + cs := gtx.Constraints + cs.Max.X = txi + cs.Min.X = gtx.Constraints.Max.X + cs.Max.Y = tyi + cs.Min.Y = gtx.Constraints.Max.Y // gtx.Constraints.Constrain(image.Point{ + // X: t.X[i], + // Y: t.Y[index], + // }) + gtx.Constraints.Max.X = txi + // gtx.Constraints.Min.X = gtx.Constraints.Max.X + gtx.Constraints.Max.Y = tyi + // gtx.Constraints.Min.Y = gtx.Constraints.Max.Y + dims := EmptySpace(txi, tyi)(gtx) + // dims + oie.Widget(gtx) + return dims + }).Fn) + } + } + return f.Fn(gtx) + } + return t.Theme.VFlex(). + Rigid(func(gtx l.Context) l.Dimensions { + // header is fixed to the top of the widget + return t.Fill(t.headerBackground, l.Center, t.TextSize.V, 0, header.Fn).Fn(gtx) + }). + Flexed(1, + t.Fill(t.cellBackground, l.Center, t.TextSize.V, 0, func(gtx l.Context) l.Dimensions { + return t.list.Vertical(). + Length(len(out)). + Background(t.cellBackground). + ListElement(le). + Fn(gtx) + }).Fn, + ). + Fn(gtx) +} diff --git a/pkg/gel/text.go b/pkg/gel/text.go new file mode 100644 index 0000000..97a16fc --- /dev/null +++ b/pkg/gel/text.go @@ -0,0 +1,194 @@ +package gel + +import ( + "image" + "unicode/utf8" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "golang.org/x/image/math/fixed" +) + +// Text is a widget for laying out and drawing text. +type Text struct { + *Window + // alignment specify the text alignment. + alignment text.Alignment + // maxLines limits the number of lines. Zero means no limit. + maxLines int +} + +func (w *Window) Text() *Text { + return &Text{Window: w} +} + +// Alignment sets the alignment for the text +func (t *Text) Alignment(alignment text.Alignment) *Text { + t.alignment = alignment + return t +} + +// MaxLines sets the alignment for the text +func (t *Text) MaxLines(maxLines int) *Text { + t.maxLines = maxLines + return t +} + +type lineIterator struct { + Lines []text.Line + Clip image.Rectangle + Alignment text.Alignment + Width int + Offset image.Point + + y, prevDesc fixed.Int26_6 + txtOff int +} + +func (l *lineIterator) Next() (text.Layout, image.Point, bool) { + for len(l.Lines) > 0 { + line := l.Lines[0] + l.Lines = l.Lines[1:] + x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X) + l.y += l.prevDesc + line.Ascent + l.prevDesc = line.Descent + // Align baseline and line start to the pixel grid. + off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())} + l.y = off.Y + off.Y += fixed.I(l.Offset.Y) + if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { + break + } + lo := line.Layout + start := l.txtOff + l.txtOff += len(line.Layout.Text) + if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { + continue + } + for len(lo.Advances) > 0 { + _, n := utf8.DecodeRuneInString(lo.Text) + adv := lo.Advances[0] + if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X { + break + } + off.X += adv + lo.Text = lo.Text[n:] + lo.Advances = lo.Advances[1:] + start += n + } + end := start + endx := off.X + rn := 0 + for n, r := range lo.Text { + if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X { + lo.Advances = lo.Advances[:rn] + lo.Text = lo.Text[:n] + break + } + end += utf8.RuneLen(r) + endx += lo.Advances[rn] + rn++ + } + offf := image.Point{X: off.X.Floor(), Y: off.Y.Floor()} + return lo, offf, true + } + return text.Layout{}, image.Point{}, false +} + +// func linesDimens(lines []text.Line) l.Dimensions { +// var width fixed.Int26_6 +// var h int +// var baseline int +// if len(lines) > 0 { +// baseline = lines[0].Ascent.Ceil() +// var prevDesc fixed.Int26_6 +// for _, l := range lines { +// h += (prevDesc + l.Ascent).Ceil() +// prevDesc = l.Descent +// if l.Width > width { +// width = l.Width +// } +// } +// h += lines[len(lines)-1].Descent.Ceil() +// } +// w := width.Ceil() +// return l.Dimensions{ +// Size: image.Point{ +// X: w, +// Y: h, +// }, +// Baseline: h - baseline, +// } +// } + +func (t *Text) Fn(gtx l.Context, s text.Shaper, font text.Font, size unit.Value, txt string) l.Dimensions { + cs := gtx.Constraints + textSize := fixed.I(gtx.Px(size)) + lines := s.LayoutString(font, textSize, cs.Max.X, txt) + if max := t.maxLines; max > 0 && len(lines) > max { + lines = lines[:max] + } + dims := linesDimens(lines) + dims.Size = cs.Constrain(dims.Size) + cl := textPadding(lines) + cl.Max = cl.Max.Add(dims.Size) + it := lineIterator{ + Lines: lines, + Clip: cl, + Alignment: t.alignment, + Width: dims.Size.X, + } + for { + ll, off, ok := it.Next() + if !ok { + break + } + stack := op.Save(gtx.Ops) + op.Offset(l.FPt(off)).Add(gtx.Ops) + s.Shape(font, textSize, ll).Add(gtx.Ops) + clip.Rect(cl.Sub(off)).Add(gtx.Ops) + paint.PaintOp{}.Add(gtx.Ops) + stack.Load() + } + return dims +} + +// func textPadding(lines []text.Line) (padding image.Rectangle) { +// if len(lines) == 0 { +// return +// } +// first := lines[0] +// if d := first.Ascent + first.Bounds.Min.Y; d < 0 { +// padding.Min.Y = d.Ceil() +// } +// last := lines[len(lines)-1] +// if d := last.Bounds.Max.Y - last.Descent; d > 0 { +// padding.Max.Y = d.Ceil() +// } +// if d := first.Bounds.Min.X; d < 0 { +// padding.Min.X = d.Ceil() +// } +// if d := first.Bounds.Max.X - first.Width; d > 0 { +// padding.Max.X = d.Ceil() +// } +// return +// } + +// func align(align text.Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { +// mw := fixed.I(maxWidth) +// switch align { +// case text.Middle: +// return fixed.I(((mw - width) / 2).Floor()) +// case text.End: +// return fixed.I((mw - width).Floor()) +// case text.Start: +// return 0 +// default: +// panic(fmt.Errorf("unknown alignment %v", align)) +// } +// } diff --git a/pkg/gel/textinput.go b/pkg/gel/textinput.go new file mode 100644 index 0000000..fa57185 --- /dev/null +++ b/pkg/gel/textinput.go @@ -0,0 +1,143 @@ +package gel + +import ( + "image/color" + + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "github.com/p9c/p9/pkg/gel/f32color" +) + +// TextInput is a simple text input widget +type TextInput struct { + *Window + font text.Font + textSize unit.Value + // Color is the text color. + color color.NRGBA + // Hint contains the text displayed when the editor is empty. + hint string + // HintColor is the color of hint text. + hintColor color.NRGBA + // SelectionColor is the color of the background for selected text. + selectionColor color.NRGBA + editor *Editor + shaper text.Shaper +} + +// TextInput creates a simple text input widget +func (w *Window) TextInput(editor *Editor, hint string) *TextInput { + var fon text.Font + var e error + if fon, e = w.collection.Font("bariol regular"); E.Chk(e) { + panic(e) + } + ti := &TextInput{ + Window: w, + editor: editor, + textSize: w.TextSize, + font: fon, + color: w.Colors.GetNRGBAFromName("DocText"), + shaper: w.shaper, + hint: hint, + hintColor: w.Colors.GetNRGBAFromName("Hint"), + selectionColor: w.Colors.GetNRGBAFromName("scrim"), + } + ti.Font("bariol regular") + return ti +} + +// Font sets the font for the text input widget +func (ti *TextInput) Font(font string) *TextInput { + var fon text.Font + var e error + if fon, e = ti.Theme.collection.Font(font); !E.Chk(e) { + ti.editor.font = fon + } + return ti +} + +// TextScale sets the size of the text relative to the base font size +func (ti *TextInput) TextScale(scale float32) *TextInput { + ti.textSize = ti.Theme.TextSize.Scale(scale) + return ti +} + +// Color sets the color to render the text +func (ti *TextInput) Color(color string) *TextInput { + ti.color = ti.Theme.Colors.GetNRGBAFromName(color) + return ti +} + +// SelectionColor sets the color to render the text +func (ti *TextInput) SelectionColor(color string) *TextInput { + ti.selectionColor = ti.Theme.Colors.GetNRGBAFromName(color) + return ti +} + +// Hint sets the text to show when the box is empty +func (ti *TextInput) Hint(hint string) *TextInput { + ti.hint = hint + return ti +} + +// HintColor sets the color of the hint text +func (ti *TextInput) HintColor(color string) *TextInput { + ti.hintColor = ti.Theme.Colors.GetNRGBAFromName(color) + return ti +} + +// Fn renders the text input widget +func (ti *TextInput) Fn(gtx l.Context) l.Dimensions { + defer op.Save(gtx.Ops).Load() + macro := op.Record(gtx.Ops) + paint.ColorOp{Color: ti.hintColor}.Add(gtx.Ops) + var maxlines int + if ti.editor.singleLine { + maxlines = 1 + } + tl := Label{ + Window: ti.Window, + font: ti.font, + color: ti.hintColor, + alignment: ti.editor.alignment, + maxLines: maxlines, + text: ti.hint, + textSize: ti.textSize, + shaper: ti.shaper, + } + dims := tl.Fn(gtx) + call := macro.Stop() + if w := dims.Size.X; gtx.Constraints.Min.X < w { + gtx.Constraints.Min.X = w + } + if h := dims.Size.Y; gtx.Constraints.Min.Y < h { + gtx.Constraints.Min.Y = h + } + dims = ti.editor.Layout(gtx, ti.shaper, ti.font, ti.textSize) + disabled := gtx.Queue == nil + if ti.editor.Len() > 0 { + paint.ColorOp{Color: blendDisabledColor(disabled, ti.selectionColor)}.Add(gtx.Ops) + ti.editor.PaintSelection(gtx) + paint.ColorOp{Color: blendDisabledColor(disabled, ti.color)}.Add(gtx.Ops) + ti.editor.PaintText(gtx) + } else { + call.Add(gtx.Ops) + } + if !disabled { + paint.ColorOp{Color: ti.color}.Add(gtx.Ops) + ti.editor.PaintCaret(gtx) + } + return dims +} + +func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA { + if disabled { + return f32color.Disabled(c) + } + return c +} diff --git a/pkg/gel/texttable.go b/pkg/gel/texttable.go new file mode 100644 index 0000000..3afd22e --- /dev/null +++ b/pkg/gel/texttable.go @@ -0,0 +1,147 @@ +package gel + +import l "github.com/p9c/p9/pkg/gel/gio/layout" + +type TextTableHeader []string + +type TextTableRow []string + +type TextTableBody []TextTableRow + +// TextTable is a widget that renders a scrolling list of rows of data labeled by a header. Note that for the reasons of +// expedience and performance this widget assumes a growing but immutable list of rows of items. If this is used on +// data that is not immutable, nilling the body will cause it to be wholly regenerated, updating older content than the +// longest length the list has reached. +type TextTable struct { + *Window + Header TextTableHeader + Body TextTableBody + HeaderColor string + HeaderDarkTheme bool + HeaderBackground string + HeaderFont string + HeaderFontScale float32 + CellColor string + CellBackground string + CellFont string + CellFontScale float32 + CellInset float32 + List *List + Table *Table +} + +// Regenerate the text table. +func (tt *TextTable) Regenerate(fully bool) { + if len(tt.Header) == 0 || len(tt.Body) == 0 { + return + } + // // set defaults if unset + tt.SetDefaults() + if tt.Table.header == nil || len(tt.Table.header) < 1 || tt.HeaderDarkTheme != tt.Theme.Dark.True() { + tt.HeaderDarkTheme = tt.Theme.Dark.True() + // if this is being regenerated due to theme change + tt.Table.header = tt.Table.header[:0] + // this only has to be created once + for i := range tt.Header { + tt.Table.header = append(tt.Table.header, Cell{ + Widget: // tt.Theme.Fill(tt.HeaderBackground, + tt.Inset(tt.CellInset, + tt.Body1(tt.Header[i]). + Color(tt.HeaderColor). + TextScale(tt.HeaderFontScale). + Font(tt.HeaderFont).MaxLines(1). + Fn, + ).Fn, + // ).Fn, + }) + } + } + // var startIndex int + // if tt.Table.body == nil || len(tt.Table.body) < 1 { + // // tt.Table.body = tt.Table.body[:0] + // } else { + // if fully { + // tt.Body = tt.Body[:0] + // tt.Table.body = tt.Table.body[:0] + // } + // startIndex = len(tt.Table.body) + // D.Ln("startIndex", startIndex, len(tt.Body)) + // if startIndex < len(tt.Body) { + + // bd := tt.Body // [startIndex:] + diff := len(tt.Body) - len(tt.Table.body) + // D.Ln(len(tt.Table.body), len(tt.Body), diff) + if diff > 0 { + cg := make(CellGrid, diff) + for i := range cg { + cg[i] = make(CellRow, len(tt.Header)) + } + tt.Table.body = append(tt.Table.body, cg...) + } + // D.Ln(len(tt.Table.body), len(tt.Body)) + var body CellGrid + for i := range tt.Body { + var row CellRow + for j := range tt.Body[i] { + tt.Table.body[i][j] = Cell{ + Widget: tt.Inset(0.25, + tt.Body1(tt.Body[i][j]). + Color(tt.CellColor). + TextScale(tt.CellFontScale). + Font(tt.CellFont).MaxLines(1). + Fn, + ).Fn, + } + } + body = append(body, row) + } + // tt.Table.body = append(tt.Table.body, body...) + // } + // } +} + +func (tt *TextTable) SetReverse() *TextTable { + tt.Table.reverse = true + return tt +} + +func (tt *TextTable) SetDefaults() *TextTable { + if tt.HeaderColor == "" { + tt.HeaderColor = "PanelText" + } + if tt.HeaderBackground == "" { + tt.HeaderBackground = "PanelBg" + } + if tt.HeaderFont == "" { + tt.HeaderFont = "bariol bold" + } + if tt.HeaderFontScale == 0 { + tt.HeaderFontScale = Scales["Caption"] + } + if tt.CellColor == "" { + tt.CellColor = "DocText" + } + if tt.CellBackground == "" { + tt.CellBackground = "DocBg" + } + if tt.CellFont == "" { + tt.CellFont = "go regular" + } + if tt.CellFontScale == 0 { + tt.CellFontScale = Scales["Caption"] + } + // we assume the caller has intended a zero inset if it is zero + if tt.Table == nil { + tt.Table = &Table{ + Window: tt.Window, + list: tt.List, + headerBackground: tt.HeaderBackground, + cellBackground: tt.CellBackground, + } + } + return tt +} + +func (tt *TextTable) Fn(gtx l.Context) l.Dimensions { + return tt.Table.Fn(gtx) +} diff --git a/pkg/gel/theme.go b/pkg/gel/theme.go new file mode 100644 index 0000000..c68f938 --- /dev/null +++ b/pkg/gel/theme.go @@ -0,0 +1,55 @@ +package gel + +import ( + "os/exec" + "runtime" + "strconv" + "strings" + + "github.com/p9c/p9/pkg/gel/gio/text" + "github.com/p9c/p9/pkg/gel/gio/unit" + "github.com/p9c/p9/pkg/opts/binary" + "github.com/p9c/p9/pkg/qu" +) + +type Theme struct { + quit qu.C + shaper text.Shaper + collection Collection + TextSize unit.Value + *Colors + icons map[string]*Icon + scrollBarSize int + Dark *binary.Opt + iconCache IconCache + WidgetPool *Pool +} + +// NewTheme creates a new theme to use for rendering a user interface +func NewTheme(dark *binary.Opt, fontCollection []text.FontFace, quit qu.C) (th *Theme) { + textSize := unit.Sp(16) + if runtime.GOOS == "linux" { + var e error + var b []byte + runner := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "text-scaling-factor") + if b, e = runner.CombinedOutput(); D.Chk(e) { + } + var factor float64 + numberString := strings.TrimSpace(string(b)) + if factor, e = strconv.ParseFloat(numberString, 10); D.Chk(e) { + } + textSize = textSize.Scale(float32(factor)) + // I.Ln(w.TextSize) + } + th = &Theme{ + quit: quit, + shaper: text.NewCache(fontCollection), + collection: fontCollection, + TextSize: textSize, + Colors: newColors(), + scrollBarSize: 0, + iconCache: make(IconCache), + } + th.SetDarkTheme(dark.True()) + return +} diff --git a/pkg/gel/toast/example/main.go_ b/pkg/gel/toast/example/main.go_ new file mode 100644 index 0000000..2c40ceb --- /dev/null +++ b/pkg/gel/toast/example/main.go_ @@ -0,0 +1,88 @@ +package main + +import ( + "log" + "os" + + "github.com/p9c/p9/pkg/gel/gio/app" + "github.com/p9c/p9/pkg/gel/gio/io/system" + "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + + "github.com/p9c/p9/pkg/gui" + "github.com/p9c/p9/pkg/gui/fonts/p9fonts" + "github.com/p9c/p9/pkg/gui/toast" +) + +var ( + th = gui.NewTheme(p9fonts.Collection(), nil) + btnDanger = th.Clickable() + btnWarning = th.Clickable() + btnSuccess = th.Clickable() +) + +func main() { + go func() { + w := app.NewWindow(app.Size(unit.Px(150*6+50), unit.Px(150*6-50))) + if e := loop(w); dbg.Chk(e) { + log.ftl.Ln(err) + } + os.Exit(0) + }() + app.Main() +} + +func loop(w *app.Window) (e error) { + var ops op.Ops + t := toast.New(th) + for { + e := <-w.Events() + switch e := e.(type) { + case system.DestroyEvent: + return e.Err + case system.FrameEvent: + gtx := layout.NewContext(&ops, e) + paint.Fill(gtx.Ops, gui.HexNRGB("e5e5e5FF")) + op.InvalidateOp{}.Add(gtx.Ops) + + th.Inset( + 0.25, + th.VFlex(). + Rigid( + th.Inset( + 0.1, + th.Button(btnDanger).Text("Danger").Background("Gray").Color("Danger").Fn, + ).Fn, + ). + Rigid( + th.Inset( + 0.1, + th.Button(btnWarning).Text("Warning").Background("Gray").Color("Warning").Fn, + ).Fn, + ). + Rigid( + th.Inset( + 0.1, + th.Button(btnSuccess).Text("Success").Background("Gray").Color("Success").Fn, + ).Fn, + ).Fn, + ).Fn(gtx) + + for btnDanger.Clicked() { + t.AddToast("Danger", "Danger content", "Danger") + } + for btnSuccess.Clicked() { + t.AddToast("Success", "Success content", "Success") + } + for btnWarning.Clicked() { + t.AddToast("Warning", "Warning content", "Warning") + } + + t.DrawToasts()(gtx) + e.Frame(gtx.Ops) + w.Invalidate() + } + } +} diff --git a/pkg/gel/toast/toast.go_ b/pkg/gel/toast/toast.go_ new file mode 100644 index 0000000..5a6a430 --- /dev/null +++ b/pkg/gel/toast/toast.go_ @@ -0,0 +1,146 @@ +package toast + +import ( + "image" + "image/color" + + "github.com/p9c/p9/pkg/gel/gio/f32" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/op/clip" + "github.com/p9c/p9/pkg/gel/gio/op/paint" + "github.com/p9c/p9/pkg/gel/gio/unit" + icons2 "golang.org/x/exp/shiny/materialdesign/icons" + + "github.com/p9c/p9/pkg/gui" + "github.com/p9c/p9/pkg/gui/shadow" +) + +type Toasts struct { + toasts []toast + layout *gui.List + theme *gui.Theme + offset image.Point + duration int + singleSize image.Point + singleCornerRadius unit.Value + singleElevation unit.Value +} + +type toast struct { + title, content, level string + headerBackground color.NRGBA + bodyBackground color.NRGBA + icon *[]byte + ticker float32 + close gui.Clickable + cornerRadius unit.Value + elevation unit.Value +} + +func New(th *gui.Theme) *Toasts { + return &Toasts{ + layout: th.List(), + theme: th, + duration: 100, + singleSize: image.Pt(300, 80), + singleCornerRadius: unit.Dp(5), + singleElevation: unit.Dp(5), + } +} +func (t *Toasts) AddToast(title, content, level string) { + ic := &icons2.ActionInfo + switch level { + case "Warning": + ic = &icons2.AlertWarning + case "Success": + ic = &icons2.NavigationCheck + case "Danger": + ic = &icons2.AlertError + case "Info": + ic = &icons2.ActionInfo + } + t.toasts = append( + t.toasts, toast{ + title: title, + content: content, + level: level, + ticker: 0, + headerBackground: gui.HexNRGB(t.theme.Colors[level]), + bodyBackground: gui.HexNRGB(t.theme.Colors["PanelBg"]), + cornerRadius: t.singleCornerRadius, + elevation: t.singleElevation, + icon: ic, + }, + ) +} + +func (t *Toasts) DrawToasts() func(gtx l.Context) { + return func(gtx l.Context) { + defer op.Push(gtx.Ops).Pop() + op.Offset(f32.Pt(float32(gtx.Constraints.Max.X)-310, 0)).Add(gtx.Ops) + gtx.Constraints.Min = image.Pt(250, gtx.Constraints.Min.Y) + gtx.Constraints.Max.X = 250 + // paint.Fill(gtx.Ops, helper.HexARGB("ff559988")) + t.theme.Inset( + 0, + t.layout.Vertical().ScrollToEnd().Length(len(t.toasts)).ListElement(t.singleToast).Fn, + ).Fn(gtx) + } +} +func (t *Toasts) singleToast(gtx l.Context, index int) l.Dimensions { + if t.toasts[index].ticker < float32(t.duration) { + t.toasts[index].ticker += 1 + gtx.Constraints.Min = t.singleSize + // gtx.Constraints.Max = t.singleSize + gtx.Constraints.Max.X = t.singleSize.X + sz := gtx.Constraints.Min + rr := float32(gtx.Px(t.singleCornerRadius)) + + r := f32.Rect(0, 0, float32(sz.X), float32(sz.Y)) + + return t.theme.Inset( + 0.05, func(gtx l.Context) l.Dimensions { + return shadow.Shadow( + gtx, unit.Dp(3), unit.Dp(1), gui.HexNRGB("ee000000"), t.theme.Flex().Flexed( + 1, + func(gtx l.Context) l.Dimensions { + clip.UniformRRect(r, rr).Add(gtx.Ops) + paint.Fill(gtx.Ops, t.toasts[index].bodyBackground) + + return t.theme.Inset( + 0.25, + t.theme.VFlex(). + Rigid( + t.theme.Inset( + 0.1, + t.theme.Fill(t.toasts[index].level, t.theme.Flex(). + Rigid( + func(gtx l.Context) l.Dimensions { + return t.theme.Icon().Color("DocText").Scale(1).Src(t.toasts[index].icon).Fn(gtx) + }, + ). + Flexed( + 1, + t.theme.H6(t.toasts[index].title).Color("PanelBg").Fn, + ).Fn, l.Center).Fn, + ).Fn, + ). + Rigid( + t.theme.Body1(t.toasts[index].content).Color("PanelText").Fn, + ).Fn, + ).Fn(gtx) + }, + ).Fn, + ) + }, + ).Fn(gtx) + } else { + t.toasts = remove(t.toasts, index) + return gui.EmptySpace(0, 0)(gtx) + } +} + +func remove(slice []toast, s int) []toast { + return append(slice[:s], slice[s+1:]...) +} diff --git a/pkg/gel/window.go b/pkg/gel/window.go new file mode 100644 index 0000000..dcd3a94 --- /dev/null +++ b/pkg/gel/window.go @@ -0,0 +1,273 @@ +package gel + +import ( + "math" + "os/exec" + "runtime" + "strconv" + "strings" + "time" + + "github.com/p9c/p9/pkg/opts/binary" + "github.com/p9c/p9/pkg/opts/meta" + + clipboard2 "github.com/p9c/p9/pkg/gel/gio/io/clipboard" + + "github.com/p9c/p9/pkg/gel/clipboard" + "github.com/p9c/p9/pkg/gel/fonts/p9fonts" + + "github.com/p9c/p9/pkg/gel/gio/io/event" + + "github.com/p9c/p9/pkg/qu" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/gel/gio/app" + "github.com/p9c/p9/pkg/gel/gio/io/system" + l "github.com/p9c/p9/pkg/gel/gio/layout" + "github.com/p9c/p9/pkg/gel/gio/op" + "github.com/p9c/p9/pkg/gel/gio/unit" +) + +type CallbackQueue chan func() error + +func NewCallbackQueue(bufSize int) CallbackQueue { + return make(CallbackQueue, bufSize) +} + +type scaledConfig struct { + Scale float32 +} + +func (s *scaledConfig) Now() time.Time { + return time.Now() +} + +func (s *scaledConfig) Px(v unit.Value) int { + scale := s.Scale + if v.U == unit.UnitPx { + scale = 1 + } + return int(math.Round(float64(scale * v.V))) +} + +type Window struct { + *Theme + *app.Window + opts []app.Option + scale *scaledConfig + Width *uberatomic.Int32 // stores the width at the beginning of render + Height *uberatomic.Int32 + ops op.Ops + evQ system.FrameEvent + Runner CallbackQueue + overlay []*func(gtx l.Context) + ClipboardWriteReqs chan string + ClipboardReadReqs chan func(string) + ClipboardContent string + clipboardReadReady qu.C + clipboardReadResponse chan string +} + +func (w *Window) PushOverlay(overlay *func(gtx l.Context)) { + w.overlay = append(w.overlay, overlay) +} + +func (w *Window) PopOverlay(overlay *func(gtx l.Context)) { + if len(w.overlay) == 0 { + return + } + index := -1 + for i := range w.overlay { + if overlay == w.overlay[i] { + index = i + break + } + } + if index != -1 { + if index == len(w.overlay)-1 { + w.overlay = w.overlay[:index] + } else if index == 0 { + w.overlay = w.overlay[1:] + } else { + w.overlay = append(w.overlay[:index], w.overlay[index+1:]...) + } + } +} + +func (w *Window) Overlay(gtx l.Context) { + for _, overlay := range w.overlay { + (*overlay)(gtx) + } +} + +// NewWindowP9 creates a new window +func NewWindowP9(quit chan struct{}) (out *Window) { + out = &Window{ + scale: &scaledConfig{1}, + Runner: NewCallbackQueue(32), + Width: uberatomic.NewInt32(0), + Height: uberatomic.NewInt32(0), + ClipboardWriteReqs: make(chan string, 1), + ClipboardReadReqs: make(chan func(string), 32), + clipboardReadReady: qu.Ts(1), + clipboardReadResponse: make(chan string,1), + } + out.Theme = NewTheme( + binary.New(meta.Data{}, false, nil), + p9fonts.Collection(), quit, + ) + out.Theme.WidgetPool = out.NewPool() + clipboard.Start() + return +} + +// NewWindow creates a new window +func NewWindow(th *Theme) (out *Window) { + out = &Window{ + Theme: th, + scale: &scaledConfig{1}, + } + return +} + +// Title sets the title of the window +func (w *Window) Title(title string) (out *Window) { + w.opts = append(w.opts, app.Title(title)) + return w +} + +// Size sets the dimensions of the window +func (w *Window) Size(width, height float32) (out *Window) { + w.opts = append( + w.opts, + app.Size(w.TextSize.Scale(width), w.TextSize.Scale(height)), + ) + return w +} + +// Scale sets the scale factor for rendering +func (w *Window) Scale(s float32) *Window { + w.scale = &scaledConfig{s} + return w +} + +// Open sets the window options and initialise the node.window +func (w *Window) Open() (out *Window) { + if w.scale == nil { + w.Scale(1) + } + if w.opts != nil { + w.Window = app.NewWindow(w.opts...) + w.opts = nil + } + + return w +} + +func (w *Window) Run(frame func(ctx l.Context) l.Dimensions, destroy func(), quit qu.C,) (e error) { + runner := func() { + ticker := time.NewTicker(time.Second) + for { + select { + case content := <-w.ClipboardWriteReqs: + w.WriteClipboard(content) + case fn := <-w.ClipboardReadReqs: + go func() { + w.ReadClipboard() + fn(<-w.clipboardReadResponse) + }() + case <-ticker.C: + if runtime.GOOS == "linux" { + var e error + var b []byte + textSize := unit.Sp(16) + runner := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "text-scaling-factor") + if b, e = runner.CombinedOutput(); D.Chk(e) { + } + var factor float64 + numberString := strings.TrimSpace(string(b)) + if factor, e = strconv.ParseFloat(numberString, 10); D.Chk(e) { + } + w.TextSize = textSize.Scale(float32(factor)) + // I.Ln(w.TextSize) + } + w.Invalidate() + case fn := <-w.Runner: + if e = fn(); E.Chk(e) { + return + } + case <-quit.Wait(): + return + // by repeating selectors we decrease the chance of a runner delaying + // a frame event hitting the physical frame deadline + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + case ev := <-w.Window.Events(): + if e = w.processEvents(ev, frame, destroy); E.Chk(e) { + return + } + } + } + } + switch runtime.GOOS { + case "ios", "android": + go runner() + default: + runner() + } + return +} + +func (w *Window) processEvents(e event.Event, frame func(ctx l.Context) l.Dimensions, destroy func()) error { + switch ev := e.(type) { + case system.DestroyEvent: + D.Ln("received destroy event", ev.Err) + destroy() + return ev.Err + case system.FrameEvent: + ops := op.Ops{} + c := l.NewContext(&ops, ev) + // update dimensions for responsive sizing widgets + w.Width.Store(int32(c.Constraints.Max.X)) + w.Height.Store(int32(c.Constraints.Max.Y)) + frame(c) + w.Overlay(c) + ev.Frame(c.Ops) + case clipboard2.Event: + // w.ClipboardContent = ev.Text + // w.clipboardReadReady.Signal() + w.clipboardReadResponse <- ev.Text + } + return nil +} diff --git a/pkg/gel/wraplist.go b/pkg/gel/wraplist.go new file mode 100644 index 0000000..5a22a17 --- /dev/null +++ b/pkg/gel/wraplist.go @@ -0,0 +1,95 @@ +package gel + +import ( + l "github.com/p9c/p9/pkg/gel/gio/layout" + "golang.org/x/exp/shiny/text" +) + +// WrapList is a generalised layout for creating lists from widgets lined up in one axis and wrapped into lines across +// a given axis. It can be used for an icon view, for a text console cell grid, for laying out selectable text. +type WrapList struct { + *Window + list *List + widgets []l.Widget + axis l.Axis + dir text.Direction +} + +// WrapList creates a new WrapList +func (w *Window) WrapList() *WrapList { + return &WrapList{ + Window: w, + list: w.WidgetPool.GetList(), + } +} + +// Axis sets the axis that will be scrollable +func (w *WrapList) Axis(axis l.Axis) *WrapList { + w.axis = axis + return w +} + +// Direction sets the direction across the axis, for vertical, text.Forwards means left to right, text.Backwards means +// right to left, and for horizontal, text.Forwards means top to bottom, text.Backwards means bottom to top +func (w *WrapList) Direction(dir text.Direction) *WrapList { + w.dir = dir + return w +} + +// Widgets loads a set of widgets into the WrapList +func (w *WrapList) Widgets(widgets []l.Widget) *WrapList { + w.widgets = widgets + return w +} + +// Fn renders the WrapList in the current context +// todo: this needs to be cached and have a hook in the WrapList.Widgets method to trigger generation +func (w *WrapList) Fn(gtx l.Context) l.Dimensions { + // first get the dimensions of the widget list + if len(w.widgets) > 0 { + le := func(gtx l.Context, index int) l.Dimensions { + dims := w.widgets[index](gtx) + return dims + } + gtx1 := CopyContextDimensionsWithMaxAxis(gtx, w.axis) + // generate the dimensions for all the list elements + allDims := GetDimensionList(gtx1, len(w.widgets), le) + var out = [][]l.Widget{{}} + cursor := 0 + runningTotal := 0 + _, width := axisCrossConstraint(w.axis, gtx.Constraints) + for i := range allDims { + runningTotal += axisCross(w.axis, allDims[i].Size) + if runningTotal > width { + cursor++ + runningTotal = 0 + out = append(out, []l.Widget{}) + } + out[cursor] = append(out[cursor], w.widgets[i]) + } + le2 := func(gtx l.Context, index int) l.Dimensions { + o := w.Flex().AlignStart() + if w.axis == l.Horizontal { + o = w.VFlex().AlignStart() + } + // for _, y := range out { + y := out[index] + var outRow []l.Widget + for j, x := range y { + if w.dir == text.Forwards { + outRow = append(outRow, x) + } else { + outRow = append(outRow, y[len(y)-1-j]) + } + } + for i := range outRow { + o.Rigid(outRow[i]) + } + // } + return o.Fn(gtx) + } + return w.list.Vertical().Length(len(out)).ListElement(le2).Fn(gtx) + } else { + return l.Dimensions{} + } +} diff --git a/pkg/hardfork/blacklist.go b/pkg/hardfork/blacklist.go new file mode 100644 index 0000000..b4b2d12 --- /dev/null +++ b/pkg/hardfork/blacklist.go @@ -0,0 +1,11 @@ +package hardfork + +import ( + "github.com/p9c/p9/pkg/btcaddr" +) + +// Blacklist is a list of addresses that have been suspended +var Blacklist = []btcaddr.Address{ + // Cryptopia liquidation wallet + // Addr("8JEEhaMxJf4dZh5rvVCVSA7JKeYBvy8fir", mn), +} diff --git a/pkg/hardfork/log.go b/pkg/hardfork/log.go new file mode 100644 index 0000000..41fe90c --- /dev/null +++ b/pkg/hardfork/log.go @@ -0,0 +1,43 @@ +package hardfork + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/hardfork/subsidy.go b/pkg/hardfork/subsidy.go new file mode 100644 index 0000000..4d5c19a --- /dev/null +++ b/pkg/hardfork/subsidy.go @@ -0,0 +1,92 @@ +package hardfork + +import ( + "encoding/hex" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" +) + +// Payee is an address and amount +type Payee struct { + Address btcaddr.Address + Amount amt.Amount +} + +var ( + // The following prepares hard fork disbursement payout transactions + tn = &chaincfg.TestNet3Params + mn = &chaincfg.MainNetParams + // Payees are the list of payments to be made on the hard fork activation on mainnet + Payees = []Payee{ + {Addr("ag7s5bmcA8XoP1CcS1QPjiD4C5hhMWATik", mn), Amount(4400)}, + } + // TestnetPayees are the list of payments to be made on the hard fork activation in the testnet + // + // these are made using the following seed for testnet + // f4d2c4c542bb52512ed9e6bbfa2d000e576a0c8b4ebd1acafd7efa37247366bc + TestnetPayees = []Payee{ + {Addr("8K73LTaMHZmwwqe4vTHu7wm7QtwusvRCwC", tn), Amount(100)}, + // {Addr("8JEEhaMxJf4dZh5rvVCVSA7JKeYBvy8fir", tn), Amount(15500)}, + {Addr("8bec3m8qpMePrBPHDAyCrkSm7TanGX8yWW", tn), Amount(1223)}, + {Addr("8MCLEWq8pjXikrpb9rF9M5DpnpaoWPUD2W", tn), Amount(4000)}, + {Addr("8cYGvT7km339nVukTj3ztfyQDFEHFivBNk", tn), Amount(2440)}, + {Addr("8YUAAfUeS2mqUnsfiwDwQcEbMfM3tazKr7", tn), Amount(100)}, + {Addr("8MMam6gxH1ns5LqASfhkHfRV2vsQaoM9VC", tn), Amount(8800)}, + {Addr("8JABYpdqqyRD5FbACtMJ3XF5HJ38jaytrk", tn), Amount(422)}, + {Addr("8MUnJMYi5Fo7Bm5Pmpr7JjdL3ZDJ7wqmXJ", tn), Amount(5000)}, + {Addr("8d2RLbCBE8CiF4DetVuRfFFLEJJaXYjhdH", tn), Amount(30000)}, + } + // CorePubkeyBytes is the address and public keys for the core dev disbursement + CorePubkeyBytes = [][]byte{ + // nWo + Key("021a00c7e054279124e2d3eb8b64a58f1fda515464cd8df3c0823d2ff2931ebf37"), + // loki + Key("0387484f75bc5e45092b1334684def6b47f3dba1566b4b87f62d11c73d8f98db3e"), + // trax0r + Key("02daf0bda15f83899f4ebb62fd837c2dd2368ec8ed90ed0f050054d75d35935c99"), + } + // CoreAmount is the amount paid into the dev pool + CoreAmount = Amount(30000) + // TestnetCorePubkeyBytes are the addresses for the 3 of 4 multisig payment for dev costs + // + // these are made using the following seed for testnet + // f4d2c4c542bb52512ed9e6bbfa2d000e576a0c8b4ebd1acafd7efa37247366bc + TestnetCorePubkeyBytes = [][]byte{ + // "8cL2fDzTSMu9Cd2rFi1dceWitQheAaCgTs", + Key("03f040c0cff7918415974f05154c8ffe126ad93db7216103fb6f4080dc3bcf4803"), + // "8YUDhyrcGrQk4PMpxnaNk1XyLRpQLa7N47", + Key("03f5a5ff1ce0564c7f4565a108220ebac9bd544b44e79ca5a2a805e585d8297cc6"), + // "8Rpf7CT4ikJQqXRpSp4EnAypKmidhHADN2", + Key("022976653e490cea689faafa899aa41b6295c32a5fb3e02d0fa201ac698e0c0c24"), + // "8Yw41PD1A3RviyjFQc38L9VufZasDU1pY8", + Key("029ed2885ea597fddea070a5c4c9f40900a514f67f9d5f662aa7b556e8bc5a26f8"), + } + // TestnetCoreAmount is the amount paid into the dev pool + TestnetCoreAmount = Amount(30000) +) + +func Amount(f float64) (amount amt.Amount) { + var e error + amount, e = amt.NewAmount(f) + if e != nil { + panic(e) + } + return +} + +func Addr(addr string, defaultNet *chaincfg.Params) (out btcaddr.Address) { + out, e := btcaddr.Decode(addr, defaultNet) + if e != nil { + panic(e) + } + return +} + +func Key(key string) (out []byte) { + out, e := hex.DecodeString(key) + if e != nil { + panic(e) + } + return +} diff --git a/pkg/indexers/README.md b/pkg/indexers/README.md new file mode 100755 index 0000000..391a4e5 --- /dev/null +++ b/pkg/indexers/README.md @@ -0,0 +1,30 @@ +# indexers + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://godoc.org/github.com/p9c/p9/blockchain/indexers?status.png)](http://godoc.org/github.com/p9c/p9/blockchain/indexers) + +Package indexers implements optional block chain indexes. + +These indexes are typically used to enhance the amount of information available +via an RPC interface. + +## Supported Indexers + +- Transaction-by-hash (txbyhashidx) Index + - Creates a mapping from the hash of each transaction to the block that + contains it along with its offset and length within the serialized block +- Transaction-by-address (txbyaddridx) Index + - Creates a mapping from every address to all transactions which either + credit or debit the address + - Requires the transaction-by-hash index + +## Installation + +```bash +$ go get -u github.com/p9c/p9/blockchain/indexers +``` + +## License + +Package indexers is licensed under the [copyfree](http://copyfree.org) +ISCLicense. diff --git a/pkg/indexers/addrindex.go b/pkg/indexers/addrindex.go new file mode 100644 index 0000000..b2d5b01 --- /dev/null +++ b/pkg/indexers/addrindex.go @@ -0,0 +1,806 @@ +package indexers + +import ( + "errors" + "fmt" + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "sync" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // addrIndexName is the human-readable name for the index. + addrIndexName = "address index" + // level0MaxEntries is the maximum number of transactions that are stored in level 0 of an address index entry. + // Subsequent levels store 2^n * level0MaxEntries entries, or in words, double the maximum of the previous level. + level0MaxEntries = 8 + // addrKeySize is the number of bytes an address key consumes in the index. It consists of 1 byte address type + 20 + // bytes hash160. + addrKeySize = 1 + 20 + // levelKeySize is the number of bytes a level key in the address index consumes. It consists of the address key + 1 + // byte for the level. + levelKeySize = addrKeySize + 1 + // levelOffset is the offset in the level key which identifes the level. + levelOffset = levelKeySize - 1 + // addrKeyTypePubKeyHash is the address type in an address key which represents both a pay-to-pubkey-hash and a + // pay-to-pubkey address. This is done because both are identical for the purposes of the address index. + addrKeyTypePubKeyHash = 0 + // addrKeyTypeScriptHash is the address type in an address key which represents a pay-to-script-hash address. This + // is necessary because the hash of a pubkey address might be the same as that of a script hash. + addrKeyTypeScriptHash = 1 + // // addrKeyTypePubKeyHash is the address type in an address key which represents + // // a pay-to-witness-pubkey-hash address. This is required as the 20-byte data + // // push of a p2wkh witness program may be the same data push used a p2pkh + // // address. + // addrKeyTypeWitnessPubKeyHash = 2 + // // addrKeyTypeScriptHash is the address type in an address key which represents + // // a pay-to-witness-script-hash address. This is required, as p2wsh are distinct + // // from p2sh addresses since they use a new script template, as well as a + // // 32-byte data push. + // addrKeyTypeWitnessScriptHash = 3 + // Size of a transaction entry. It consists of 4 bytes block id + 4 bytes offset + 4 bytes length. + txEntrySize = 4 + 4 + 4 +) + +var ( + // addrIndexKey is the key of the address index and the db bucket used to house it. + addrIndexKey = []byte("txbyaddridx") + // errUnsupportedAddressType is an error that is used to signal an unsupported address type has been used. + errUnsupportedAddressType = errors.New( + "address type is not supported " + + "by the address index", + ) +) + +// The address index maps addresses referenced in the blockchain to a list of all the transactions involving that +// address. Transactions are stored according to their order of appearance in the blockchain. That is to say first by +// block height and then by offset inside the block. It is also important to note that this implementation requires the +// transaction index since it is needed in order to catch up old blocks due to the fact the spent outputs will already +// be pruned from the utxo set. +// +// The approach used to store the index is similar to a log-structured merge tree (LSM tree) and is thus similar to how +// leveldb works internally.// Every address consists of one or more entries identified by a level starting from 0 where +// each level holds a maximum number of entries such that each subsequent level holds double the maximum of the previous +// one. In equation form, the number of entries each level holds is 2^n * firstLevelMaxSize. New transactions are +// appended to level 0 until it becomes full at which point the entire level 0 entry is appended to the level 1 entry +// and level 0 is cleared. This process continues until level 1 becomes full at which point it will be appended to level +// 2 and cleared and so on. +// +// The result of this is the lower levels contain newer transactions and the transactions within each level are ordered +// from oldest to newest. The intent of this approach is to provide a balance between space efficiency and indexing +// cost. Storing one entry per transaction would have the lowest indexing cost, but would waste a lot of space because +// the same address hash would be duplicated for every transaction key. On the other hand, storing a single entry with +// all transactions would be the most space efficient, but would cause indexing cost to grow quadratically with the +// number of transactions involving the same address. The approach used here provides logarithmic insertion and +// retrieval. +// +// The serialized key format is: +// +// Field Type Size +// addr type uint8 1 byte +// addr hash hash160 20 bytes +// level uint8 1 byte +// ----- +// Total: 22 bytes +// The serialized value format is: +// [,...] +// Field Type Size +// block id uint32 4 bytes +// start offset uint32 4 bytes +// tx length uint32 4 bytes +// ----- +// Total: 12 bytes per indexed tx +// fetchBlockHashFunc defines a callback function to use in order to convert a serialized block ID to an associated +// block hash. +type fetchBlockHashFunc func(serializedID []byte) (*chainhash.Hash, error) + +// serializeAddrIndexEntry serializes the provided block id and transaction location according to the format described +// in detail above. +func serializeAddrIndexEntry(blockID uint32, txLoc wire.TxLoc) []byte { + // Serialize the entry. + serialized := make([]byte, 12) + byteOrder.PutUint32(serialized, blockID) + byteOrder.PutUint32(serialized[4:], uint32(txLoc.TxStart)) + byteOrder.PutUint32(serialized[8:], uint32(txLoc.TxLen)) + return serialized +} + +// deserializeAddrIndexEntry decodes the passed serialized byte slice into the provided region struct according to the +// format described in detail above and uses the passed block hash fetching function in order to conver the block ID to +// the associated block hash. +func deserializeAddrIndexEntry( + serialized []byte, + region *database.BlockRegion, + fetchBlockHash fetchBlockHashFunc, +) (e error) { + // Ensure there are enough bytes to decode. + if len(serialized) < txEntrySize { + return errDeserialize("unexpected end of data") + } + hash, e := fetchBlockHash(serialized[0:4]) + if e != nil { + return e + } + region.Hash = hash + region.Offset = byteOrder.Uint32(serialized[4:8]) + region.Len = byteOrder.Uint32(serialized[8:12]) + return nil +} + +// keyForLevel returns the key for a specific address and level in the address index entry. +func keyForLevel(addrKey [addrKeySize]byte, level uint8) [levelKeySize]byte { + var key [levelKeySize]byte + copy(key[:], addrKey[:]) + key[levelOffset] = level + return key +} + +// dbPutAddrIndexEntry updates the address index to include the provided entry according to the level-based scheme +// described in detail above. +func dbPutAddrIndexEntry(bucket internalBucket, addrKey [addrKeySize]byte, blockID uint32, txLoc wire.TxLoc) (e error) { + // Start with level 0 and its initial max number of entries. + curLevel := uint8(0) + maxLevelBytes := level0MaxEntries * txEntrySize + // Simply append the new entry to level 0 and return now when it will fit. This is the most common path. + newData := serializeAddrIndexEntry(blockID, txLoc) + level0Key := keyForLevel(addrKey, 0) + level0Data := bucket.Get(level0Key[:]) + if len(level0Data)+len(newData) <= maxLevelBytes { + mergedData := newData + if len(level0Data) > 0 { + mergedData = make([]byte, len(level0Data)+len(newData)) + copy(mergedData, level0Data) + copy(mergedData[len(level0Data):], newData) + } + return bucket.Put(level0Key[:], mergedData) + } + // At this point, level 0 is full, so merge each level into higher levels as many times as needed to free up level + // 0. + prevLevelData := level0Data + for { + // Each new level holds twice as much as the previous one. + curLevel++ + maxLevelBytes *= 2 + // Move to the next level as long as the current level is full. + curLevelKey := keyForLevel(addrKey, curLevel) + curLevelData := bucket.Get(curLevelKey[:]) + if len(curLevelData) == maxLevelBytes { + prevLevelData = curLevelData + continue + } + // The current level has room for the data in the previous one, so merge the data from previous level into it. + mergedData := prevLevelData + if len(curLevelData) > 0 { + mergedData = make( + []byte, len(curLevelData)+ + len(prevLevelData), + ) + copy(mergedData, curLevelData) + copy(mergedData[len(curLevelData):], prevLevelData) + } + e := bucket.Put(curLevelKey[:], mergedData) + if e != nil { + return e + } + // Move all of the levels before the previous one up a level. + for mergeLevel := curLevel - 1; mergeLevel > 0; mergeLevel-- { + mergeLevelKey := keyForLevel(addrKey, mergeLevel) + prevLevelKey := keyForLevel(addrKey, mergeLevel-1) + prevData := bucket.Get(prevLevelKey[:]) + e := bucket.Put(mergeLevelKey[:], prevData) + if e != nil { + return e + } + } + break + } + // Finally, insert the new entry into level 0 now that it is empty. + return bucket.Put(level0Key[:], newData) +} + +// dbFetchAddrIndexEntries returns block regions for transactions referenced by the given address key and the number of +// entries skipped since it could have been less in the case where there are less total entries than the requested +// number of entries to skip. +func dbFetchAddrIndexEntries( + bucket internalBucket, addrKey [addrKeySize]byte, numToSkip, numRequested uint32, + reverse bool, fetchBlockHash fetchBlockHashFunc, +) ([]database.BlockRegion, uint32, error) { + // When the reverse flag is not set, all levels need to be fetched because numToSkip and numRequested are counted + // from the oldest transactions (highest level) and thus the total count is needed. However, when the reverse flag + // is set, only enough records to satisfy the requested amount are needed. + var level uint8 + var serialized []byte + for !reverse || len(serialized) < int(numToSkip+numRequested)*txEntrySize { + curLevelKey := keyForLevel(addrKey, level) + levelData := bucket.Get(curLevelKey[:]) + if levelData == nil { + // Stop when there are no more levels. + break + } + // Higher levels contain older transactions, so prepend them. + prepended := make([]byte, len(serialized)+len(levelData)) + copy(prepended, levelData) + copy(prepended[len(levelData):], serialized) + serialized = prepended + level++ + } + // When the requested number of entries to skip is larger than the number available, skip them all and return now + // with the actual number skipped. + numEntries := uint32(len(serialized) / txEntrySize) + if numToSkip >= numEntries { + return nil, numEntries, nil + } + // Nothing more to do when there are no requested entries. + if numRequested == 0 { + return nil, numToSkip, nil + } + // Limit the number to load based on the number of available entries, the number to skip, and the number requested. + numToLoad := numEntries - numToSkip + if numToLoad > numRequested { + numToLoad = numRequested + } + // Start the offset after all skipped entries and load the calculated number. + results := make([]database.BlockRegion, numToLoad) + for i := uint32(0); i < numToLoad; i++ { + // Calculate the read offset according to the reverse flag. + var offset uint32 + if reverse { + offset = (numEntries - numToSkip - i - 1) * txEntrySize + } else { + offset = (numToSkip + i) * txEntrySize + } + // Deserialize and populate the result. + e := deserializeAddrIndexEntry( + serialized[offset:], + &results[i], fetchBlockHash, + ) + if e != nil { + // Ensure any deserialization errors are returned as database corruption errors. + if isDeserializeErr(e) { + e = database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "failed to "+ + "deserialized address index "+ + "for key %x: %v", addrKey, e, + ), + } + } + return nil, 0, e + } + } + return results, numToSkip, nil +} + +// minEntriesToReachLevel returns the minimum number of entries that are required to reach the given address index level. +func minEntriesToReachLevel(level uint8) int { + maxEntriesForLevel := level0MaxEntries + minRequired := 1 + for l := uint8(1); l <= level; l++ { + minRequired += maxEntriesForLevel + maxEntriesForLevel *= 2 + } + return minRequired +} + +// maxEntriesForLevel returns the maximum number of entries allowed for the given address index level. +func maxEntriesForLevel(level uint8) int { + numEntries := level0MaxEntries + for l := level; l > 0; l-- { + numEntries *= 2 + } + return numEntries +} + +// dbRemoveAddrIndexEntries removes the specified number of entries from from the address index for the provided key. An +// assertion error will be returned if the count exceeds the total number of entries in the index. +func dbRemoveAddrIndexEntries(bucket internalBucket, addrKey [addrKeySize]byte, count int) (e error) { + // Nothing to do if no entries are being deleted. + if count <= 0 { + return nil + } + // Make use of a local map to track pending updates and define a closure to apply it to the database. This is done + // in order to reduce the number of database reads and because there is more than one exit path that needs to apply + // the updates. + pendingUpdates := make(map[uint8][]byte) + applyPending := func() (e error) { + for level, data := range pendingUpdates { + curLevelKey := keyForLevel(addrKey, level) + if len(data) == 0 { + e := bucket.Delete(curLevelKey[:]) + if e != nil { + return e + } + continue + } + e := bucket.Put(curLevelKey[:], data) + if e != nil { + return e + } + } + return nil + } + // Loop forwards through the levels while removing entries until the specified number has been removed. This will + // potentially result in entirely empty lower levels which will be backfilled below. + var highestLoadedLevel uint8 + numRemaining := count + for level := uint8(0); numRemaining > 0; level++ { + // Load the data for the level from the database. + curLevelKey := keyForLevel(addrKey, level) + curLevelData := bucket.Get(curLevelKey[:]) + if len(curLevelData) == 0 && numRemaining > 0 { + return AssertError( + fmt.Sprintf( + "dbRemoveAddrIndexEntries "+ + "not enough entries for address key %x to "+ + "delete %d entries", addrKey, count, + ), + ) + } + pendingUpdates[level] = curLevelData + highestLoadedLevel = level + // Delete the entire level as needed. + numEntries := len(curLevelData) / txEntrySize + if numRemaining >= numEntries { + pendingUpdates[level] = nil + numRemaining -= numEntries + continue + } + // Remove remaining entries to delete from the level. + offsetEnd := len(curLevelData) - (numRemaining * txEntrySize) + pendingUpdates[level] = curLevelData[:offsetEnd] + break + } + // When all elements in level 0 were not removed there is nothing left to do other than updating the database. + if len(pendingUpdates[0]) != 0 { + return applyPending() + } + // At this point there are one or more empty levels before the current level which need to be backfilled and the + // current level might have had some entries deleted from it as well. Since all levels after/ level 0 are required + // to either be empty, half full, or completely full, the current level must be adjusted accordingly by backfilling + // each previous levels in a way which satisfies the requirements. Any entries that are left are assigned to level 0 + // after the loop as they are guaranteed to fit by the logic in the loop. In other words, this effectively squashes + // all remaining entries in the current level into the lowest possible levels while following the level rules. Note + // that the level after the current level might also have entries and gaps are not allowed, so this also keeps track + // of the lowest empty level so the code below knows how far to backfill in case it is required. + lowestEmptyLevel := uint8(255) + curLevelData := pendingUpdates[highestLoadedLevel] + curLevelMaxEntries := maxEntriesForLevel(highestLoadedLevel) + for level := highestLoadedLevel; level > 0; level-- { + // When there are not enough entries left in the current level for the number that would be required to reach + // it, clear the the current level which effectively moves them all up to the previous level on the next + // iteration. Otherwise, there are are sufficient entries, so update the current level to contain as many + // entries as possible while still leaving enough remaining entries required to reach the level. + numEntries := len(curLevelData) / txEntrySize + prevLevelMaxEntries := curLevelMaxEntries / 2 + minPrevRequired := minEntriesToReachLevel(level - 1) + if numEntries < prevLevelMaxEntries+minPrevRequired { + lowestEmptyLevel = level + pendingUpdates[level] = nil + } else { + // This level can only be completely full or half full, so choose the appropriate offset to ensure enough + // entries remain to reach the level. + var offset int + if numEntries-curLevelMaxEntries >= minPrevRequired { + offset = curLevelMaxEntries * txEntrySize + } else { + offset = prevLevelMaxEntries * txEntrySize + } + pendingUpdates[level] = curLevelData[:offset] + curLevelData = curLevelData[offset:] + } + curLevelMaxEntries = prevLevelMaxEntries + } + pendingUpdates[0] = curLevelData + if len(curLevelData) == 0 { + lowestEmptyLevel = 0 + } + // When the highest loaded level is empty, it's possible the level after it still has data and thus that data needs + // to be backfilled as well. + for len(pendingUpdates[highestLoadedLevel]) == 0 { + // When the next level is empty too, the is no data left to continue backfilling, so there is nothing left to + // do. Otherwise, populate the pending updates map with the newly loaded data and update the highest loaded + // level accordingly. + level := highestLoadedLevel + 1 + curLevelKey := keyForLevel(addrKey, level) + levelData := bucket.Get(curLevelKey[:]) + if len(levelData) == 0 { + break + } + pendingUpdates[level] = levelData + highestLoadedLevel = level + // At this point the highest level is not empty, but it might be half full. When that is the case, move it up a + // level to simplify the code below which backfills all lower levels that are still empty. This also means the + // current level will be empty, so the loop will perform another another iteration to potentially backfill this + // level with data from the next one. + curLevelMaxEntries := maxEntriesForLevel(level) + if len(levelData)/txEntrySize != curLevelMaxEntries { + pendingUpdates[level] = nil + pendingUpdates[level-1] = levelData + level-- + curLevelMaxEntries /= 2 + } + // Backfill all lower levels that are still empty by iteratively halfing the data until the lowest empty level + // is filled. + for level > lowestEmptyLevel { + offset := (curLevelMaxEntries / 2) * txEntrySize + pendingUpdates[level] = levelData[:offset] + levelData = levelData[offset:] + pendingUpdates[level-1] = levelData + level-- + curLevelMaxEntries /= 2 + } + // The lowest possible empty level is now the highest loaded level. + lowestEmptyLevel = highestLoadedLevel + } + // Apply the pending updates. + return applyPending() +} + +// addrToKey converts known address types to an addrindex key. An error is returned for unsupported types. +func addrToKey(addr btcaddr.Address) ([addrKeySize]byte, error) { + switch addr := addr.(type) { + case *btcaddr.PubKeyHash: + var result [addrKeySize]byte + result[0] = addrKeyTypePubKeyHash + copy(result[1:], addr.Hash160()[:]) + return result, nil + case *btcaddr.ScriptHash: + var result [addrKeySize]byte + result[0] = addrKeyTypeScriptHash + copy(result[1:], addr.Hash160()[:]) + return result, nil + case *btcaddr.PubKey: + var result [addrKeySize]byte + result[0] = addrKeyTypePubKeyHash + copy(result[1:], addr.PubKeyHash().Hash160()[:]) + return result, nil + // case *util.AddressWitnessScriptHash: + // var result [addrKeySize]byte + // result[0] = addrKeyTypeWitnessScriptHash + // // P2WSH outputs utilize a 32-byte data push created by hashing the script with sha256 instead of hash160. In + // // order to keep all address entries within the database uniform and compact, we use a hash160 here to reduce + // // the size of the salient data push to 20-bytes. + // copy(result[1:], btcaddr.Hash160(addr.ScriptAddress())) + // return result, nil + // case *util.AddressWitnessPubKeyHash: + // var result [addrKeySize]byte + // result[0] = addrKeyTypeWitnessPubKeyHash + // copy(result[1:], addr.Hash160()[:]) + // return result, nil + } + return [addrKeySize]byte{}, errUnsupportedAddressType +} + +// AddrIndex implements a transaction by address index. That is to say, it supports querying all transactions that +// reference a given address because they are either crediting or debiting the address. The returned transactions are +// ordered according to their order of appearance in the blockchain. In other words, first by block height and then by +// offset inside the block. In addition, support is provided for a memory-only index of unconfirmed transactions such as +// those which are kept in the memory pool before inclusion in a block. +type AddrIndex struct { + // The following fields are set when the instance is created and can't be changed afterwards, so there is no need to + // protect them with a separate mutex. + db database.DB + chainParams *chaincfg.Params + // The following fields are used to quickly link transactions and addresses that have not been included into a block + // yet when an address index is being maintained. The are protected by the unconfirmedLock field. The txnsByAddr + // field is used to keep an index of all transactions which either create an output to a given address or spend from + // a previous output to it keyed by the address. The addrsByTx field is essentially the reverse and is used to keep + // an index of all addresses which a given transaction involves. This allows fairly efficient updates when + // transactions are removed once they are included into a block. + unconfirmedLock sync.RWMutex + txnsByAddr map[[addrKeySize]byte]map[chainhash.Hash]*util.Tx + addrsByTx map[chainhash.Hash]map[[addrKeySize]byte]struct{} +} + +// Ensure the AddrIndex type implements the Indexer interface. +var _ Indexer = (*AddrIndex)(nil) + +// Ensure the AddrIndex type implements the NeedsInputser interface. +var _ NeedsInputser = (*AddrIndex)(nil) + +// NeedsInputs signals that the index requires the referenced inputs in order to properly create the index. This +// implements the NeedsInputser interface. +func (idx *AddrIndex) NeedsInputs() bool { + return true +} + +// Init is only provided to satisfy the Indexer interface as there is nothing to initialize for this index. This is part +// of the Indexer interface. +func (idx *AddrIndex) Init() (e error) { + // Nothing to do. + return nil +} + +// Key returns the database key to use for the index as a byte slice. This is part of the Indexer interface. +func (idx *AddrIndex) Key() []byte { + return addrIndexKey +} + +// Name returns the human-readable name of the index. This is part of the Indexer interface. +func (idx *AddrIndex) Name() string { + return addrIndexName +} + +// Create is invoked when the indexer manager determines the index needs to be created for the first time. It creates +// the bucket for the address index. This is part of the Indexer interface. +func (idx *AddrIndex) Create(dbTx database.Tx) (e error) { + _, e = dbTx.Metadata().CreateBucket(addrIndexKey) + return e +} + +// writeIndexData represents the address index data to be written for one block. It consists of the address mapped to an +// ordered list of the transactions that involve the address in block. It is ordered so the transactions can be stored +// in the order they appear in the block. +type writeIndexData map[[addrKeySize]byte][]int + +// indexPkScript extracts all standard addresses from the passed public key script and maps each of them to the +// associated transaction using the passed map. +func (idx *AddrIndex) indexPkScript(data writeIndexData, pkScript []byte, txIdx int) { + // Nothing to index if the script is non-standard or otherwise doesn't contain any addresses. + var addrs []btcaddr.Address + var e error + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + pkScript, + idx.chainParams, + ) + if e != nil || len(addrs) == 0 { + return + } + for _, addr := range addrs { + var addrKey [21]byte + addrKey, e = addrToKey(addr) + if e != nil { + // Ignore unsupported address types. + continue + } + // Avoid inserting the transaction more than once. Since the transactions are indexed serially any duplicates + // will be indexed in a row, so checking the most recent entry for the address is enough to detect duplicates. + indexedTxns := data[addrKey] + numTxns := len(indexedTxns) + if numTxns > 0 && indexedTxns[numTxns-1] == txIdx { + continue + } + indexedTxns = append(indexedTxns, txIdx) + data[addrKey] = indexedTxns + } +} + +// indexBlock extract all of the standard addresses from all of the transactions in the passed block and maps each of +// them to the associated transaction using the passed map. +func (idx *AddrIndex) indexBlock( + data writeIndexData, block *block.Block, + stxos []blockchain.SpentTxOut, +) { + stxoIndex := 0 + for txIdx, tx := range block.Transactions() { + // Coinbases do not reference any inputs. Since the block is required to have already gone through full + // validation, it has already been proven on the first transaction in the block is a coinbase. + if txIdx != 0 { + for range tx.MsgTx().TxIn { + // We'll access the slice of all the transactions spent in this block properly ordered to fetch the + // previous input script. + pkScript := stxos[stxoIndex].PkScript + idx.indexPkScript(data, pkScript, txIdx) + // With an input indexed, we'll advance the stxo coutner. + stxoIndex++ + } + } + for _, txOut := range tx.MsgTx().TxOut { + idx.indexPkScript(data, txOut.PkScript, txIdx) + } + } +} + +// ConnectBlock is invoked by the index manager when a new block has been connected to the main chain. This indexer adds +// a mapping for each address the transactions in the block involve. This is part of the Indexer interface. +func (idx *AddrIndex) ConnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // The offset and length of the transactions within the serialized block. + txLocs, e := block.TxLoc() + if e != nil { + return e + } + // Get the internal block ID associated with the block. + blockID, e := dbFetchBlockIDByHash(dbTx, block.Hash()) + if e != nil { + return e + } + // Build all of the address to transaction mappings in a local map. + addrsToTxns := make(writeIndexData) + idx.indexBlock(addrsToTxns, block, stxos) + // Add all of the index entries for each address. + addrIdxBucket := dbTx.Metadata().Bucket(addrIndexKey) + for addrKey, txIdxs := range addrsToTxns { + for _, txIdx := range txIdxs { + e := dbPutAddrIndexEntry( + addrIdxBucket, addrKey, + blockID, txLocs[txIdx], + ) + if e != nil { + return e + } + } + } + return nil +} + +// DisconnectBlock is invoked by the index manager when a block has been disconnected from the main chain. This indexer +// removes the address mappings each transaction in the block involve. This is part of the Indexer interface. +func (idx *AddrIndex) DisconnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // Build all of the address to transaction mappings in a local map. + addrsToTxns := make(writeIndexData) + idx.indexBlock(addrsToTxns, block, stxos) + // Remove all of the index entries for each address. + bucket := dbTx.Metadata().Bucket(addrIndexKey) + for addrKey, txIdxs := range addrsToTxns { + e := dbRemoveAddrIndexEntries(bucket, addrKey, len(txIdxs)) + if e != nil { + return e + } + } + return nil +} + +// TxRegionsForAddress returns a slice of block regions which identify each transaction that involves the passed address +// according to the specified number to skip, number requested, and whether or not the results should be reversed. It +// also returns the number actually skipped since it could be less in the case where there are not enough entries. NOTE: +// These results only include transactions confirmed in blocks. See the UnconfirmedTxnsForAddress method for obtaining +// unconfirmed transactions that involve a given address. This function is safe for concurrent access. +func (idx *AddrIndex) TxRegionsForAddress( + dbTx database.Tx, addr btcaddr.Address, numToSkip, numRequested uint32, + reverse bool, +) ([]database.BlockRegion, uint32, error) { + addrKey, e := addrToKey(addr) + if e != nil { + return nil, 0, e + } + var regions []database.BlockRegion + var skipped uint32 + e = idx.db.View( + func(dbTx database.Tx) (e error) { + // Create closure to lookup the block hash given the ID using the database transaction. + fetchBlockHash := func(id []byte) (*chainhash.Hash, error) { + // Deserialize and populate the result. + return dbFetchBlockHashBySerializedID(dbTx, id) + } + addrIdxBucket := dbTx.Metadata().Bucket(addrIndexKey) + regions, skipped, e = dbFetchAddrIndexEntries( + addrIdxBucket, + addrKey, numToSkip, numRequested, reverse, + fetchBlockHash, + ) + return e + }, + ) + return regions, skipped, e +} + +// indexUnconfirmedAddresses modifies the unconfirmed (memory-only) address index to include mappings for the addresses +// encoded by the passed public key script to the transaction. This function is safe for concurrent access. +func (idx *AddrIndex) indexUnconfirmedAddresses(pkScript []byte, tx *util.Tx) { + // The error is ignored here since the only reason it can fail is if the script fails to parse and it was already + // validated before being admitted to the mempool. + _, addresses, _, _ := txscript.ExtractPkScriptAddrs( + pkScript, + idx.chainParams, + ) + for _, addr := range addresses { + // Ignore unsupported address types. + addrKey, e := addrToKey(addr) + if e != nil { + continue + } + // Add a mapping from the address to the transaction. + idx.unconfirmedLock.Lock() + addrIndexEntry := idx.txnsByAddr[addrKey] + if addrIndexEntry == nil { + addrIndexEntry = make(map[chainhash.Hash]*util.Tx) + idx.txnsByAddr[addrKey] = addrIndexEntry + } + addrIndexEntry[*tx.Hash()] = tx + // Add a mapping from the transaction to the address. + addrsByTxEntry := idx.addrsByTx[*tx.Hash()] + if addrsByTxEntry == nil { + addrsByTxEntry = make(map[[addrKeySize]byte]struct{}) + idx.addrsByTx[*tx.Hash()] = addrsByTxEntry + } + addrsByTxEntry[addrKey] = struct{}{} + idx.unconfirmedLock.Unlock() + } +} + +// AddUnconfirmedTx adds all addresses related to the transaction to the unconfirmed (memory-only) address index. NOTE: +// This transaction MUST have already been validated by the memory pool before calling this function with it and have +// all of the inputs available in the provided utxo view. Failure to do so could result in some or all addresses not +// being indexed. This function is safe for concurrent access. +func (idx *AddrIndex) AddUnconfirmedTx(tx *util.Tx, utxoView *blockchain.UtxoViewpoint) { + // Index addresses of all referenced previous transaction outputs. The existence checks are elided since this is + // only called after the transaction has already been validated and thus all inputs are already known to exist. + for _, txIn := range tx.MsgTx().TxIn { + entry := utxoView.LookupEntry(txIn.PreviousOutPoint) + if entry == nil { + // Ignore missing entries. This should never happen in practice since the function comments specifically + // call out all inputs must be available. + continue + } + idx.indexUnconfirmedAddresses(entry.PkScript(), tx) + } + // Index addresses of all created outputs. + for _, txOut := range tx.MsgTx().TxOut { + idx.indexUnconfirmedAddresses(txOut.PkScript, tx) + } +} + +// RemoveUnconfirmedTx removes the passed transaction from the unconfirmed (memory-only) address index. This function is +// safe for concurrent access. +func (idx *AddrIndex) RemoveUnconfirmedTx(hash *chainhash.Hash) { + idx.unconfirmedLock.Lock() + defer idx.unconfirmedLock.Unlock() + // Remove all address references to the transaction from the address index and remove the entry for the address + // altogether if it no longer references any transactions. + for addrKey := range idx.addrsByTx[*hash] { + delete(idx.txnsByAddr[addrKey], *hash) + if len(idx.txnsByAddr[addrKey]) == 0 { + delete(idx.txnsByAddr, addrKey) + } + } + // Remove the entry from the transaction to address lookup map as well. + delete(idx.addrsByTx, *hash) +} + +// UnconfirmedTxnsForAddress returns all transactions currently in the unconfirmed (memory-only) address index that +// involve the passed address. Unsupported address types are ignored and will result in no results. This function is +// safe for concurrent access. +func (idx *AddrIndex) UnconfirmedTxnsForAddress(addr btcaddr.Address) []*util.Tx { + // Ignore unsupported address types. + addrKey, e := addrToKey(addr) + if e != nil { + return nil + } + // Protect concurrent access. + idx.unconfirmedLock.RLock() + defer idx.unconfirmedLock.RUnlock() + // Return a new slice with the results if there are any. This ensures safe concurrency. + if txns, exists := idx.txnsByAddr[addrKey]; exists { + addressTxns := make([]*util.Tx, 0, len(txns)) + for _, tx := range txns { + addressTxns = append(addressTxns, tx) + } + return addressTxns + } + return nil +} + +// NewAddrIndex returns a new instance of an indexer that is used to create a mapping of all addresses in the blockchain +// to the respective transactions that involve them. It implements the Indexer interface which plugs into the +// IndexManager that in turn is used by the blockchain package. This allows the index to be seamlessly maintained along +// with the chain. +func NewAddrIndex(db database.DB, chainParams *chaincfg.Params) *AddrIndex { + return &AddrIndex{ + db: db, + chainParams: chainParams, + txnsByAddr: make(map[[addrKeySize]byte]map[chainhash.Hash]*util.Tx), + addrsByTx: make(map[chainhash.Hash]map[[addrKeySize]byte]struct{}), + } +} + +// DropAddrIndex drops the address index from the provided database if it exists. +func DropAddrIndex(db database.DB, interrupt qu.C) (e error) { + return dropIndex(db, addrIndexKey, addrIndexName, interrupt) +} diff --git a/pkg/indexers/addrindex_test.go b/pkg/indexers/addrindex_test.go new file mode 100644 index 0000000..262136a --- /dev/null +++ b/pkg/indexers/addrindex_test.go @@ -0,0 +1,258 @@ +package indexers + +import ( + "bytes" + "fmt" + "testing" + + "github.com/p9c/p9/pkg/wire" +) + +// addrIndexBucket provides a mock address index database bucket by implementing the internalBucket interface. +type addrIndexBucket struct { + levels map[[levelKeySize]byte][]byte +} + +// Clone returns a deep copy of the mock address index bucket. +func (b *addrIndexBucket) Clone() *addrIndexBucket { + levels := make(map[[levelKeySize]byte][]byte) + for k, v := range b.levels { + vCopy := make([]byte, len(v)) + copy(vCopy, v) + levels[k] = vCopy + } + return &addrIndexBucket{levels: levels} +} + +// Get returns the value associated with the key from the mock address index bucket. +// +// This is part of the internalBucket interface. +func (b *addrIndexBucket) Get(key []byte) []byte { + var levelKey [levelKeySize]byte + copy(levelKey[:], key) + return b.levels[levelKey] +} + +// Put stores the provided key/value pair to the mock address index bucket. +// +// This is part of the internalBucket interface. +func (b *addrIndexBucket) Put(key []byte, value []byte) (e error) { + var levelKey [levelKeySize]byte + copy(levelKey[:], key) + b.levels[levelKey] = value + return nil +} + +// Delete removes the provided key from the mock address index bucket. +// +// This is part of the internalBucket interface. +func (b *addrIndexBucket) Delete(key []byte) (e error) { + var levelKey [levelKeySize]byte + copy(levelKey[:], key) + delete(b.levels, levelKey) + return nil +} + +// printLevels returns a string with a visual representation of the provided address key taking into account the max +// size of each level. It is useful when creating and debugging test cases. +func (b *addrIndexBucket) printLevels(addrKey [addrKeySize]byte) string { + highestLevel := uint8(0) + for k := range b.levels { + if !bytes.Equal(k[:levelOffset], addrKey[:]) { + continue + } + level := k[levelOffset] + if level > highestLevel { + highestLevel = level + } + } + var levelBuf bytes.Buffer + _, _ = levelBuf.WriteString("\n") + maxEntries := level0MaxEntries + for level := uint8(0); level <= highestLevel; level++ { + data := b.levels[keyForLevel(addrKey, level)] + numEntries := len(data) / txEntrySize + for i := 0; i < numEntries; i++ { + start := i * txEntrySize + num := byteOrder.Uint32(data[start:]) + _, _ = levelBuf.WriteString(fmt.Sprintf("%02d ", num)) + } + for i := numEntries; i < maxEntries; i++ { + _, _ = levelBuf.WriteString("_ ") + } + _, _ = levelBuf.WriteString("\n") + maxEntries *= 2 + } + return levelBuf.String() +} + +// sanityCheck ensures that all data stored in the bucket for the given address adheres to the level-based rules +// described by the address index documentation. +func (b *addrIndexBucket) sanityCheck(addrKey [addrKeySize]byte, expectedTotal int) (e error) { + // Find the highest level for the key. + highestLevel := uint8(0) + for k := range b.levels { + if !bytes.Equal(k[:levelOffset], addrKey[:]) { + continue + } + level := k[levelOffset] + if level > highestLevel { + highestLevel = level + } + } + // Ensure the expected total number of entries are present and that all levels adhere to the rules described in the + // address index documentation. + var totalEntries int + maxEntries := level0MaxEntries + for level := uint8(0); level <= highestLevel; level++ { + // Level 0 can'have more entries than the max allowed if the levels after it have data and it can't be empty. + // All other levels must either be half full or full. + data := b.levels[keyForLevel(addrKey, level)] + numEntries := len(data) / txEntrySize + totalEntries += numEntries + if level == 0 { + if (highestLevel != 0 && numEntries == 0) || + numEntries > maxEntries { + return fmt.Errorf("level %d has %d entries", + level, numEntries, + ) + } + } else if numEntries != maxEntries && numEntries != maxEntries/2 { + return fmt.Errorf("level %d has %d entries", level, + numEntries, + ) + } + maxEntries *= 2 + } + if totalEntries != expectedTotal { + return fmt.Errorf("expected %d entries - got %d", expectedTotal, + totalEntries, + ) + } + // Ensure all of the numbers are in order starting from the highest level moving to the lowest level. + expectedNum := uint32(0) + for level := highestLevel + 1; level > 0; level-- { + data := b.levels[keyForLevel(addrKey, level)] + numEntries := len(data) / txEntrySize + for i := 0; i < numEntries; i++ { + start := i * txEntrySize + num := byteOrder.Uint32(data[start:]) + if num != expectedNum { + return fmt.Errorf("level %d offset %d does "+ + "not contain the expected number of "+ + "%d - got %d", level, i, num, + expectedNum, + ) + } + expectedNum++ + } + } + return nil +} + +// TestAddrIndexLevels ensures that adding and deleting entries to the address index creates multiple levels as +// described by the address index documentation. +func TestAddrIndexLevels(t *testing.T) { + t.Parallel() + tests := []struct { + name string + key [addrKeySize]byte + numInsert int + printLevels bool // Set to help debug a specific test. + }{ + { + name: "level 0 not full", + numInsert: level0MaxEntries - 1, + }, + { + name: "level 1 half", + numInsert: level0MaxEntries + 1, + }, + { + name: "level 1 full", + numInsert: level0MaxEntries*2 + 1, + }, + { + name: "level 2 half, level 1 half", + numInsert: level0MaxEntries*3 + 1, + }, + { + name: "level 2 half, level 1 full", + numInsert: level0MaxEntries*4 + 1, + }, + { + name: "level 2 full, level 1 half", + numInsert: level0MaxEntries*5 + 1, + }, + { + name: "level 2 full, level 1 full", + numInsert: level0MaxEntries*6 + 1, + }, + { + name: "level 3 half, level 2 half, level 1 half", + numInsert: level0MaxEntries*7 + 1, + }, + { + name: "level 3 full, level 2 half, level 1 full", + numInsert: level0MaxEntries*12 + 1, + }, + } +nextTest: + for testNum, test := range tests { + // Insert entries in order. + populatedBucket := &addrIndexBucket{ + levels: make(map[[levelKeySize]byte][]byte), + } + for i := 0; i < test.numInsert; i++ { + txLoc := wire.TxLoc{TxStart: i * 2} + e := dbPutAddrIndexEntry(populatedBucket, test.key, + uint32(i), txLoc, + ) + if e != nil { + t.Errorf("dbPutAddrIndexEntry #%d (%s) - "+ + "unexpected error: %v", testNum, + test.name, e, + ) + continue nextTest + } + } + if test.printLevels { + t.Log(populatedBucket.printLevels(test.key)) + } + // Delete entries from the populated bucket until all entries have been deleted. The bucket is reset to the + // fully populated bucket on each iteration so every combination is tested. Notice the upper limit purposes + // exceeds the number of entries to ensure attempting to delete more entries than there are works correctly. + for numDelete := 0; numDelete <= test.numInsert+1; numDelete++ { + // Clone populated bucket to run each delete against. + bucket := populatedBucket.Clone() + // Remove the number of entries for this iteration. + e := dbRemoveAddrIndexEntries(bucket, test.key, + numDelete, + ) + if e != nil { + if numDelete <= test.numInsert { + t.Errorf("dbRemoveAddrIndexEntries (%s) "+ + " delete %d - unexpected error: "+ + "%v", test.name, numDelete, e, + ) + continue nextTest + } + } + if test.printLevels { + t.Log(bucket.printLevels(test.key)) + } + // Sanity check the levels to ensure the adhere to all rules. + numExpected := test.numInsert + if numDelete <= test.numInsert { + numExpected -= numDelete + } + e = bucket.sanityCheck(test.key, numExpected) + if e != nil { + t.Errorf("sanity check fail (%s) delete %d: %v", + test.name, numDelete, e, + ) + continue nextTest + } + } + } +} diff --git a/pkg/indexers/blocklogger.go b/pkg/indexers/blocklogger.go new file mode 100644 index 0000000..1944571 --- /dev/null +++ b/pkg/indexers/blocklogger.go @@ -0,0 +1,72 @@ +package indexers + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + "sync" + "time" +) + +// blockProgressLogger provides periodic logging for other services in order to show users progress of certain "actions" +// involving some or all current blocks. Ex: syncing to best chain, indexing all blocks, etc. +type blockProgressLogger struct { + receivedLogBlocks int64 + receivedLogTx int64 + lastBlockLogTime time.Time + // subsystemLogger *log.Logger + progressAction string + sync.Mutex +} + +// newBlockProgressLogger returns a new block progress logger. The progress message is templated as follows: +// {progressAction} {numProcessed} {blocks|block} in the last {timePeriod} +// ({numTxs}, height {lastBlockHeight}, {lastBlockTimeStamp}) +func newBlockProgressLogger( + progressMessage string, +) *blockProgressLogger { + return &blockProgressLogger{ + lastBlockLogTime: time.Now(), + progressAction: progressMessage, + // subsystemLogger: logger, + } +} + +// LogBlockHeight logs a new block height as an information message to show progress to the user. In order to prevent +// spam, it limits logging to one message every 10 seconds with duration and totals included. +func (b *blockProgressLogger) LogBlockHeight(block *block.Block) { + b.Lock() + defer b.Unlock() + b.receivedLogBlocks++ + b.receivedLogTx += int64(len(block.WireBlock().Transactions)) + now := time.Now() + duration := now.Sub(b.lastBlockLogTime) + if duration < time.Second*10 { + return + } + // Truncate the duration to 10s of milliseconds. + durationMillis := int64(duration / time.Millisecond) + tDuration := 10 * time.Millisecond * time.Duration(durationMillis/10) + // Log information about new block height. + blockStr := "blocks" + if b.receivedLogBlocks == 1 { + blockStr = "block " + } + txStr := "transactions" + if b.receivedLogTx == 1 { + txStr = "transaction " + } + I.F( + "%s %6d %s in the last %s (%6d %s, height %6d, %s)", + b.progressAction, + b.receivedLogBlocks, + blockStr, + fmt.Sprintf("%0.1fs", tDuration.Seconds()), + b.receivedLogTx, + txStr, + block.Height(), + block.WireBlock().Header.Timestamp, + ) + b.receivedLogBlocks = 0 + b.receivedLogTx = 0 + b.lastBlockLogTime = now +} diff --git a/pkg/indexers/cfindex.go b/pkg/indexers/cfindex.go new file mode 100644 index 0000000..2a62d46 --- /dev/null +++ b/pkg/indexers/cfindex.go @@ -0,0 +1,333 @@ +package indexers + +import ( + "errors" + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/gcs" + "github.com/p9c/p9/pkg/gcs/builder" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // cfIndexName is the human-readable name for the index. + cfIndexName = "committed filter index" +) + +// Committed filters come in one flavor currently: basic. They are generated and dropped in pairs, and both are indexed +// by a block's hash. Besides holding different content, they also live in different buckets. +var ( + // cfIndexParentBucketKey is the name of the parent bucket used to house the index. The rest of the buckets live + // below this bucket. + cfIndexParentBucketKey = []byte("cfindexparentbucket") + // cfIndexKeys is an array of db bucket names used to house indexes of block hashes to cfilters. + cfIndexKeys = [][]byte{ + []byte("cf0byhashidx"), + } + // cfHeaderKeys is an array of db bucket names used to house indexes of block hashes to cf headers. + cfHeaderKeys = [][]byte{ + []byte("cf0headerbyhashidx"), + } + // cfHashKeys is an array of db bucket names used to house indexes of block hashes to cf hashes. + cfHashKeys = [][]byte{ + []byte("cf0hashbyhashidx"), + } + maxFilterType = uint8(len(cfHeaderKeys) - 1) + // zeroHash is the chainhash.Hash value of all zero bytes, defined here for convenience. + zeroHash chainhash.Hash +) + +// dbFetchFilterIdxEntry retrieves a data blob from the filter index database. An entry's absence is not considered an +// error. +func dbFetchFilterIdxEntry(dbTx database.Tx, key []byte, h *chainhash.Hash) ([]byte, error) { + idx := dbTx.Metadata().Bucket(cfIndexParentBucketKey).Bucket(key) + return idx.Get(h[:]), nil +} + +// dbStoreFilterIdxEntry stores a data blob in the filter index database. +func dbStoreFilterIdxEntry(dbTx database.Tx, key []byte, h *chainhash.Hash, f []byte) (e error) { + idx := dbTx.Metadata().Bucket(cfIndexParentBucketKey).Bucket(key) + return idx.Put(h[:], f) +} + +// dbDeleteFilterIdxEntry deletes a data blob from the filter index database. +func dbDeleteFilterIdxEntry(dbTx database.Tx, key []byte, h *chainhash.Hash) (e error) { + idx := dbTx.Metadata().Bucket(cfIndexParentBucketKey).Bucket(key) + return idx.Delete(h[:]) +} + +// CFIndex implements a committed filter (cf) by hash index. +type CFIndex struct { + db database.DB + chainParams *chaincfg.Params +} + +// Ensure the CfIndex type implements the Indexer interface. +var _ Indexer = (*CFIndex)(nil) + +// Ensure the CfIndex type implements the NeedsInputser interface. +var _ NeedsInputser = (*CFIndex)(nil) + +// NeedsInputs signals that the index requires the referenced inputs in order to properly create the index. This +// implements the NeedsInputser interface. +func (idx *CFIndex) NeedsInputs() bool { + return true +} + +// Init initializes the hash-based cf index. This is part of the Indexer interface. +func (idx *CFIndex) Init() (e error) { + return nil // Nothing to do. +} + +// Key returns the database key to use for the index as a byte slice. This is part of the Indexer interface. +func (idx *CFIndex) Key() []byte { + return cfIndexParentBucketKey +} + +// Name returns the human-readable name of the index. This is part of the Indexer interface. +func (idx *CFIndex) Name() string { + return cfIndexName +} + +// Create is invoked when the indexer manager determines the index needs to be created for the first time. It creates +// buckets for the two hash-based cf indexes (regular only currently). +func (idx *CFIndex) Create(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + cfIndexParentBucket, e := meta.CreateBucket(cfIndexParentBucketKey) + if e != nil { + return e + } + for _, bucketName := range cfIndexKeys { + _, e = cfIndexParentBucket.CreateBucket(bucketName) + if e != nil { + return e + } + } + for _, bucketName := range cfHeaderKeys { + _, e = cfIndexParentBucket.CreateBucket(bucketName) + if e != nil { + return e + } + } + for _, bucketName := range cfHashKeys { + _, e = cfIndexParentBucket.CreateBucket(bucketName) + if e != nil { + return e + } + } + return nil +} + +// storeFilter stores a given filter, and performs the steps needed to generate the filter's header. +func storeFilter( + dbTx database.Tx, block *block.Block, f *gcs.Filter, + filterType wire.FilterType, +) (e error) { + if uint8(filterType) > maxFilterType { + return errors.New("unsupported filter type") + } + // Figure out which buckets to use. + fkey := cfIndexKeys[filterType] + hkey := cfHeaderKeys[filterType] + hashkey := cfHashKeys[filterType] + // Start by storing the filter. + h := block.Hash() + filterBytes, e := f.NBytes() + if e != nil { + return e + } + e = dbStoreFilterIdxEntry(dbTx, fkey, h, filterBytes) + if e != nil { + return e + } + // Next store the filter hash. + filterHash, e := builder.GetFilterHash(f) + if e != nil { + return e + } + e = dbStoreFilterIdxEntry(dbTx, hashkey, h, filterHash[:]) + if e != nil { + return e + } + // Then fetch the previous block's filter header. + var prevHeader *chainhash.Hash + ph := &block.WireBlock().Header.PrevBlock + if ph.IsEqual(&zeroHash) { + prevHeader = &zeroHash + } else { + var pfh []byte + pfh, e = dbFetchFilterIdxEntry(dbTx, hkey, ph) + if e != nil { + return e + } + // Construct the new block's filter header, and store it. + prevHeader, e = chainhash.NewHash(pfh) + if e != nil { + return e + } + } + fh, e := builder.MakeHeaderForFilter(f, *prevHeader) + if e != nil { + return e + } + return dbStoreFilterIdxEntry(dbTx, hkey, h, fh[:]) +} + +// ConnectBlock is invoked by the index manager when a new block has been connected to the main chain. This indexer adds +// a hash-to-cf mapping for every passed block. This is part of the Indexer interface. +func (idx *CFIndex) ConnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + prevScripts := make([][]byte, len(stxos)) + for i, stxo := range stxos { + prevScripts[i] = stxo.PkScript + } + f, e := builder.BuildBasicFilter(block.WireBlock(), prevScripts) + if e != nil { + return e + } + return storeFilter(dbTx, block, f, wire.GCSFilterRegular) +} + +// DisconnectBlock is invoked by the index manager when a block has been disconnected from the main chain. This indexer +// removes the hash-to-cf mapping for every passed block. This is part of the Indexer interface. +func (idx *CFIndex) DisconnectBlock( + dbTx database.Tx, block *block.Block, + _ []blockchain.SpentTxOut, +) (e error) { + for _, key := range cfIndexKeys { + e := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) + if e != nil { + return e + } + } + for _, key := range cfHeaderKeys { + e := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) + if e != nil { + return e + } + } + for _, key := range cfHashKeys { + e := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) + if e != nil { + return e + } + } + return nil +} + +// entryByBlockHash fetches a filter index entry of a particular type (eg. filter, filter header, etc) for a filter type +// and block hash. +func (idx *CFIndex) entryByBlockHash( + filterTypeKeys [][]byte, + filterType wire.FilterType, h *chainhash.Hash, +) (entry []byte, e error) { + if uint8(filterType) > maxFilterType { + return nil, errors.New("unsupported filter type") + } + key := filterTypeKeys[filterType] + e = idx.db.View( + func(dbTx database.Tx) (e error) { + entry, e = dbFetchFilterIdxEntry(dbTx, key, h) + return e + }, + ) + return entry, e +} + +// entriesByBlockHashes batch fetches a filter index entry of a particular type (eg. filter, filter header, etc) for a +// filter type and slice of block hashes. +func (idx *CFIndex) entriesByBlockHashes( + filterTypeKeys [][]byte, + filterType wire.FilterType, blockHashes []*chainhash.Hash, +) (entries [][]byte, e error) { + if uint8(filterType) > maxFilterType { + return nil, errors.New("unsupported filter type") + } + key := filterTypeKeys[filterType] + entries = make([][]byte, 0, len(blockHashes)) + e = idx.db.View( + func(dbTx database.Tx) (e error) { + for _, blockHash := range blockHashes { + entry, e := dbFetchFilterIdxEntry(dbTx, key, blockHash) + if e != nil { + return e + } + entries = append(entries, entry) + } + return nil + }, + ) + return entries, e +} + +// FilterByBlockHash returns the serialized contents of a block's basic or committed filter. +func (idx *CFIndex) FilterByBlockHash( + h *chainhash.Hash, + filterType wire.FilterType, +) ([]byte, error) { + return idx.entryByBlockHash(cfIndexKeys, filterType, h) +} + +// FiltersByBlockHashes returns the serialized contents of a block's basic or committed filter for a set of blocks by +// hash. +func (idx *CFIndex) FiltersByBlockHashes( + blockHashes []*chainhash.Hash, + filterType wire.FilterType, +) ([][]byte, error) { + return idx.entriesByBlockHashes(cfIndexKeys, filterType, blockHashes) +} + +// FilterHeaderByBlockHash returns the serialized contents of a block's basic committed filter header. +func (idx *CFIndex) FilterHeaderByBlockHash( + h *chainhash.Hash, + filterType wire.FilterType, +) ([]byte, error) { + return idx.entryByBlockHash(cfHeaderKeys, filterType, h) +} + +// FilterHeadersByBlockHashes returns the serialized contents of a block's basic committed filter header for a set of +// blocks by hash. +func (idx *CFIndex) FilterHeadersByBlockHashes( + blockHashes []*chainhash.Hash, + filterType wire.FilterType, +) ([][]byte, error) { + return idx.entriesByBlockHashes(cfHeaderKeys, filterType, blockHashes) +} + +// FilterHashByBlockHash returns the serialized contents of a block's basic committed filter hash. +func (idx *CFIndex) FilterHashByBlockHash( + h *chainhash.Hash, + filterType wire.FilterType, +) ([]byte, error) { + return idx.entryByBlockHash(cfHashKeys, filterType, h) +} + +// FilterHashesByBlockHashes returns the serialized contents of a block's basic committed filter hash for a set of +// blocks by hash. +func (idx *CFIndex) FilterHashesByBlockHashes( + blockHashes []*chainhash.Hash, + filterType wire.FilterType, +) ([][]byte, error) { + return idx.entriesByBlockHashes(cfHashKeys, filterType, blockHashes) +} + +// NewCfIndex returns a new instance of an indexer that is used to create a mapping of the hashes of all blocks in the +// blockchain to their respective committed filters. It implements the Indexer interface which plugs into the +// IndexManager that in turn is used by the blockchain package. This allows the index to be seamlessly maintained along +// with the chain. +func NewCfIndex(db database.DB, chainParams *chaincfg.Params) *CFIndex { + return &CFIndex{db: db, chainParams: chainParams} +} + +// DropCfIndex drops the CF index from the provided database if exists. +func DropCfIndex(db database.DB, interrupt qu.C) (e error) { + return dropIndex(db, cfIndexParentBucketKey, cfIndexName, interrupt) +} diff --git a/pkg/indexers/common.go b/pkg/indexers/common.go new file mode 100644 index 0000000..3124ec9 --- /dev/null +++ b/pkg/indexers/common.go @@ -0,0 +1,86 @@ +// Package indexers implements optional block chain indexes. +package indexers + +import ( + "encoding/binary" + "errors" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/database" +) + +var ( + // byteOrder is the preferred byte order used for serializing numeric fields for storage in the database. + byteOrder = binary.LittleEndian + // errInterruptRequested indicates that an operation was cancelled due to a user-requested interrupt. + errInterruptRequested = errors.New("interrupt requested") +) + +// NeedsInputser provides a generic interface for an indexer to specify the it requires the ability to look up inputs +// for a transaction. +type NeedsInputser interface { + NeedsInputs() bool +} + +// Indexer provides a generic interface for an indexer that is managed by an index manager such as the Manager type +// provided by this package. +type Indexer interface { + // Key returns the key of the index as a byte slice. + Key() []byte + // Name returns the human-readable name of the index. + Name() string + // Create is invoked when the indexer manager determines the index needs to be created for the first time. + Create(dbTx database.Tx) error + // Init is invoked when the index manager is first initializing the index. This differs from the Create method in + // that it is called on every load, including the case the index was just created. + Init() error + // ConnectBlock is invoked when a new block has been connected to the main chain. The set of output spent within a + // block is also passed in so indexers can access the pevious output scripts input spent if required. + ConnectBlock(database.Tx, *block.Block, []blockchain.SpentTxOut) error + // DisconnectBlock is invoked when a block has been disconnected from the main chain. The set of outputs scripts + // that were spent within this block is also returned so indexers can clean up the prior index state for this block + DisconnectBlock(database.Tx, *block.Block, []blockchain.SpentTxOut) error +} + +// AssertError identifies an error that indicates an internal code consistency issue and should be treated as a critical +// and unrecoverable error. +type AssertError string + +// DBError returns the assertion error as a huma-readable string and satisfies the error interface. +func (e AssertError) Error() string { + return "assertion failed: " + string(e) +} + +// errDeserialize signifies that a problem was encountered when deserializing data. +type errDeserialize string + +// DBError implements the error interface. +func (e errDeserialize) Error() string { + return string(e) +} + +// isDeserializeErr returns whether or not the passed error is an errDeserialize error. +func isDeserializeErr(e error) bool { + _, ok := e.(errDeserialize) + return ok +} + +// internalBucket is an abstraction over a database bucket. It is used to make the code easier to test since it allows +// mock objects in the tests to only implement these functions instead of everything a database.Bucket supports. +type internalBucket interface { + Get(key []byte) []byte + Put(key []byte, value []byte) error + Delete(key []byte) error +} + +// interruptRequested returns true when the provided channel has been closed. This simplifies early shutdown slightly +// since the caller can just use an if statement instead of a select. +func interruptRequested(interrupted <-chan struct{}) bool { + select { + case <-interrupted: + return true + default: + } + return false +} diff --git a/pkg/indexers/index/README.md b/pkg/indexers/index/README.md new file mode 100755 index 0000000..391a4e5 --- /dev/null +++ b/pkg/indexers/index/README.md @@ -0,0 +1,30 @@ +# indexers + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://godoc.org/github.com/p9c/p9/blockchain/indexers?status.png)](http://godoc.org/github.com/p9c/p9/blockchain/indexers) + +Package indexers implements optional block chain indexes. + +These indexes are typically used to enhance the amount of information available +via an RPC interface. + +## Supported Indexers + +- Transaction-by-hash (txbyhashidx) Index + - Creates a mapping from the hash of each transaction to the block that + contains it along with its offset and length within the serialized block +- Transaction-by-address (txbyaddridx) Index + - Creates a mapping from every address to all transactions which either + credit or debit the address + - Requires the transaction-by-hash index + +## Installation + +```bash +$ go get -u github.com/p9c/p9/blockchain/indexers +``` + +## License + +Package indexers is licensed under the [copyfree](http://copyfree.org) +ISCLicense. diff --git a/pkg/indexers/index/addrindex.go b/pkg/indexers/index/addrindex.go new file mode 100644 index 0000000..e077f4e --- /dev/null +++ b/pkg/indexers/index/addrindex.go @@ -0,0 +1,806 @@ +package index + +import ( + "errors" + "fmt" + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "sync" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // addrIndexName is the human-readable name for the index. + addrIndexName = "address index" + // level0MaxEntries is the maximum number of transactions that are stored in level 0 of an address index entry. + // Subsequent levels store 2^n * level0MaxEntries entries, or in words, double the maximum of the previous level. + level0MaxEntries = 8 + // addrKeySize is the number of bytes an address key consumes in the index. It consists of 1 byte address type + 20 + // bytes hash160. + addrKeySize = 1 + 20 + // levelKeySize is the number of bytes a level key in the address index consumes. It consists of the address key + 1 + // byte for the level. + levelKeySize = addrKeySize + 1 + // levelOffset is the offset in the level key which identifes the level. + levelOffset = levelKeySize - 1 + // addrKeyTypePubKeyHash is the address type in an address key which represents both a pay-to-pubkey-hash and a + // pay-to-pubkey address. This is done because both are identical for the purposes of the address index. + addrKeyTypePubKeyHash = 0 + // addrKeyTypeScriptHash is the address type in an address key which represents a pay-to-script-hash address. This + // is necessary because the hash of a pubkey address might be the same as that of a script hash. + addrKeyTypeScriptHash = 1 + // // addrKeyTypePubKeyHash is the address type in an address key which represents + // // a pay-to-witness-pubkey-hash address. This is required as the 20-byte data + // // push of a p2wkh witness program may be the same data push used a p2pkh + // // address. + // addrKeyTypeWitnessPubKeyHash = 2 + // // addrKeyTypeScriptHash is the address type in an address key which represents + // // a pay-to-witness-script-hash address. This is required, as p2wsh are distinct + // // from p2sh addresses since they use a new script template, as well as a + // // 32-byte data push. + // addrKeyTypeWitnessScriptHash = 3 + // Size of a transaction entry. It consists of 4 bytes block id + 4 bytes offset + 4 bytes length. + txEntrySize = 4 + 4 + 4 +) + +var ( + // addrIndexKey is the key of the address index and the db bucket used to house it. + addrIndexKey = []byte("txbyaddridx") + // errUnsupportedAddressType is an error that is used to signal an unsupported address type has been used. + errUnsupportedAddressType = errors.New( + "address type is not supported " + + "by the address index", + ) +) + +// The address index maps addresses referenced in the blockchain to a list of all the transactions involving that +// address. Transactions are stored according to their order of appearance in the blockchain. That is to say first by +// block height and then by offset inside the block. It is also important to note that this implementation requires the +// transaction index since it is needed in order to catch up old blocks due to the fact the spent outputs will already +// be pruned from the utxo set. +// +// The approach used to store the index is similar to a log-structured merge tree (LSM tree) and is thus similar to how +// leveldb works internally.// Every address consists of one or more entries identified by a level starting from 0 where +// each level holds a maximum number of entries such that each subsequent level holds double the maximum of the previous +// one. In equation form, the number of entries each level holds is 2^n * firstLevelMaxSize. New transactions are +// appended to level 0 until it becomes full at which point the entire level 0 entry is appended to the level 1 entry +// and level 0 is cleared. This process continues until level 1 becomes full at which point it will be appended to level +// 2 and cleared and so on. +// +// The result of this is the lower levels contain newer transactions and the transactions within each level are ordered +// from oldest to newest. The intent of this approach is to provide a balance between space efficiency and indexing +// cost. Storing one entry per transaction would have the lowest indexing cost, but would waste a lot of space because +// the same address hash would be duplicated for every transaction key. On the other hand, storing a single entry with +// all transactions would be the most space efficient, but would cause indexing cost to grow quadratically with the +// number of transactions involving the same address. The approach used here provides logarithmic insertion and +// retrieval. +// +// The serialized key format is: +// +// Field Type Size +// addr type uint8 1 byte +// addr hash hash160 20 bytes +// level uint8 1 byte +// ----- +// Total: 22 bytes +// The serialized value format is: +// [,...] +// Field Type Size +// block id uint32 4 bytes +// start offset uint32 4 bytes +// tx length uint32 4 bytes +// ----- +// Total: 12 bytes per indexed tx +// fetchBlockHashFunc defines a callback function to use in order to convert a serialized block ID to an associated +// block hash. +type fetchBlockHashFunc func(serializedID []byte) (*chainhash.Hash, error) + +// serializeAddrIndexEntry serializes the provided block id and transaction location according to the format described +// in detail above. +func serializeAddrIndexEntry(blockID uint32, txLoc wire.TxLoc) []byte { + // Serialize the entry. + serialized := make([]byte, 12) + byteOrder.PutUint32(serialized, blockID) + byteOrder.PutUint32(serialized[4:], uint32(txLoc.TxStart)) + byteOrder.PutUint32(serialized[8:], uint32(txLoc.TxLen)) + return serialized +} + +// deserializeAddrIndexEntry decodes the passed serialized byte slice into the provided region struct according to the +// format described in detail above and uses the passed block hash fetching function in order to conver the block ID to +// the associated block hash. +func deserializeAddrIndexEntry( + serialized []byte, + region *database.BlockRegion, + fetchBlockHash fetchBlockHashFunc, +) (e error) { + // Ensure there are enough bytes to decode. + if len(serialized) < txEntrySize { + return errDeserialize("unexpected end of data") + } + hash, e := fetchBlockHash(serialized[0:4]) + if e != nil { + return e + } + region.Hash = hash + region.Offset = byteOrder.Uint32(serialized[4:8]) + region.Len = byteOrder.Uint32(serialized[8:12]) + return nil +} + +// keyForLevel returns the key for a specific address and level in the address index entry. +func keyForLevel(addrKey [addrKeySize]byte, level uint8) [levelKeySize]byte { + var key [levelKeySize]byte + copy(key[:], addrKey[:]) + key[levelOffset] = level + return key +} + +// dbPutAddrIndexEntry updates the address index to include the provided entry according to the level-based scheme +// described in detail above. +func dbPutAddrIndexEntry(bucket internalBucket, addrKey [addrKeySize]byte, blockID uint32, txLoc wire.TxLoc) (e error) { + // Start with level 0 and its initial max number of entries. + curLevel := uint8(0) + maxLevelBytes := level0MaxEntries * txEntrySize + // Simply append the new entry to level 0 and return now when it will fit. This is the most common path. + newData := serializeAddrIndexEntry(blockID, txLoc) + level0Key := keyForLevel(addrKey, 0) + level0Data := bucket.Get(level0Key[:]) + if len(level0Data)+len(newData) <= maxLevelBytes { + mergedData := newData + if len(level0Data) > 0 { + mergedData = make([]byte, len(level0Data)+len(newData)) + copy(mergedData, level0Data) + copy(mergedData[len(level0Data):], newData) + } + return bucket.Put(level0Key[:], mergedData) + } + // At this point, level 0 is full, so merge each level into higher levels as many times as needed to free up level + // 0. + prevLevelData := level0Data + for { + // Each new level holds twice as much as the previous one. + curLevel++ + maxLevelBytes *= 2 + // Move to the next level as long as the current level is full. + curLevelKey := keyForLevel(addrKey, curLevel) + curLevelData := bucket.Get(curLevelKey[:]) + if len(curLevelData) == maxLevelBytes { + prevLevelData = curLevelData + continue + } + // The current level has room for the data in the previous one, so merge the data from previous level into it. + mergedData := prevLevelData + if len(curLevelData) > 0 { + mergedData = make( + []byte, len(curLevelData)+ + len(prevLevelData), + ) + copy(mergedData, curLevelData) + copy(mergedData[len(curLevelData):], prevLevelData) + } + e := bucket.Put(curLevelKey[:], mergedData) + if e != nil { + return e + } + // Move all of the levels before the previous one up a level. + for mergeLevel := curLevel - 1; mergeLevel > 0; mergeLevel-- { + mergeLevelKey := keyForLevel(addrKey, mergeLevel) + prevLevelKey := keyForLevel(addrKey, mergeLevel-1) + prevData := bucket.Get(prevLevelKey[:]) + e := bucket.Put(mergeLevelKey[:], prevData) + if e != nil { + return e + } + } + break + } + // Finally, insert the new entry into level 0 now that it is empty. + return bucket.Put(level0Key[:], newData) +} + +// dbFetchAddrIndexEntries returns block regions for transactions referenced by the given address key and the number of +// entries skipped since it could have been less in the case where there are less total entries than the requested +// number of entries to skip. +func dbFetchAddrIndexEntries( + bucket internalBucket, addrKey [addrKeySize]byte, numToSkip, numRequested uint32, + reverse bool, fetchBlockHash fetchBlockHashFunc, +) ([]database.BlockRegion, uint32, error) { + // When the reverse flag is not set, all levels need to be fetched because numToSkip and numRequested are counted + // from the oldest transactions (highest level) and thus the total count is needed. However, when the reverse flag + // is set, only enough records to satisfy the requested amount are needed. + var level uint8 + var serialized []byte + for !reverse || len(serialized) < int(numToSkip+numRequested)*txEntrySize { + curLevelKey := keyForLevel(addrKey, level) + levelData := bucket.Get(curLevelKey[:]) + if levelData == nil { + // Stop when there are no more levels. + break + } + // Higher levels contain older transactions, so prepend them. + prepended := make([]byte, len(serialized)+len(levelData)) + copy(prepended, levelData) + copy(prepended[len(levelData):], serialized) + serialized = prepended + level++ + } + // When the requested number of entries to skip is larger than the number available, skip them all and return now + // with the actual number skipped. + numEntries := uint32(len(serialized) / txEntrySize) + if numToSkip >= numEntries { + return nil, numEntries, nil + } + // Nothing more to do when there are no requested entries. + if numRequested == 0 { + return nil, numToSkip, nil + } + // Limit the number to load based on the number of available entries, the number to skip, and the number requested. + numToLoad := numEntries - numToSkip + if numToLoad > numRequested { + numToLoad = numRequested + } + // Start the offset after all skipped entries and load the calculated number. + results := make([]database.BlockRegion, numToLoad) + for i := uint32(0); i < numToLoad; i++ { + // Calculate the read offset according to the reverse flag. + var offset uint32 + if reverse { + offset = (numEntries - numToSkip - i - 1) * txEntrySize + } else { + offset = (numToSkip + i) * txEntrySize + } + // Deserialize and populate the result. + e := deserializeAddrIndexEntry( + serialized[offset:], + &results[i], fetchBlockHash, + ) + if e != nil { + // Ensure any deserialization errors are returned as database corruption errors. + if isDeserializeErr(e) { + e = database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "failed to "+ + "deserialized address index "+ + "for key %x: %v", addrKey, e, + ), + } + } + return nil, 0, e + } + } + return results, numToSkip, nil +} + +// minEntriesToReachLevel returns the minimum number of entries that are required to reach the given address index level. +func minEntriesToReachLevel(level uint8) int { + maxEntriesForLevel := level0MaxEntries + minRequired := 1 + for l := uint8(1); l <= level; l++ { + minRequired += maxEntriesForLevel + maxEntriesForLevel *= 2 + } + return minRequired +} + +// maxEntriesForLevel returns the maximum number of entries allowed for the given address index level. +func maxEntriesForLevel(level uint8) int { + numEntries := level0MaxEntries + for l := level; l > 0; l-- { + numEntries *= 2 + } + return numEntries +} + +// dbRemoveAddrIndexEntries removes the specified number of entries from from the address index for the provided key. An +// assertion error will be returned if the count exceeds the total number of entries in the index. +func dbRemoveAddrIndexEntries(bucket internalBucket, addrKey [addrKeySize]byte, count int) (e error) { + // Nothing to do if no entries are being deleted. + if count <= 0 { + return nil + } + // Make use of a local map to track pending updates and define a closure to apply it to the database. This is done + // in order to reduce the number of database reads and because there is more than one exit path that needs to apply + // the updates. + pendingUpdates := make(map[uint8][]byte) + applyPending := func() (e error) { + for level, data := range pendingUpdates { + curLevelKey := keyForLevel(addrKey, level) + if len(data) == 0 { + e := bucket.Delete(curLevelKey[:]) + if e != nil { + return e + } + continue + } + e := bucket.Put(curLevelKey[:], data) + if e != nil { + return e + } + } + return nil + } + // Loop forwards through the levels while removing entries until the specified number has been removed. This will + // potentially result in entirely empty lower levels which will be backfilled below. + var highestLoadedLevel uint8 + numRemaining := count + for level := uint8(0); numRemaining > 0; level++ { + // Load the data for the level from the database. + curLevelKey := keyForLevel(addrKey, level) + curLevelData := bucket.Get(curLevelKey[:]) + if len(curLevelData) == 0 && numRemaining > 0 { + return AssertError( + fmt.Sprintf( + "dbRemoveAddrIndexEntries "+ + "not enough entries for address key %x to "+ + "delete %d entries", addrKey, count, + ), + ) + } + pendingUpdates[level] = curLevelData + highestLoadedLevel = level + // Delete the entire level as needed. + numEntries := len(curLevelData) / txEntrySize + if numRemaining >= numEntries { + pendingUpdates[level] = nil + numRemaining -= numEntries + continue + } + // Remove remaining entries to delete from the level. + offsetEnd := len(curLevelData) - (numRemaining * txEntrySize) + pendingUpdates[level] = curLevelData[:offsetEnd] + break + } + // When all elements in level 0 were not removed there is nothing left to do other than updating the database. + if len(pendingUpdates[0]) != 0 { + return applyPending() + } + // At this point there are one or more empty levels before the current level which need to be backfilled and the + // current level might have had some entries deleted from it as well. Since all levels after/ level 0 are required + // to either be empty, half full, or completely full, the current level must be adjusted accordingly by backfilling + // each previous levels in a way which satisfies the requirements. Any entries that are left are assigned to level 0 + // after the loop as they are guaranteed to fit by the logic in the loop. In other words, this effectively squashes + // all remaining entries in the current level into the lowest possible levels while following the level rules. Note + // that the level after the current level might also have entries and gaps are not allowed, so this also keeps track + // of the lowest empty level so the code below knows how far to backfill in case it is required. + lowestEmptyLevel := uint8(255) + curLevelData := pendingUpdates[highestLoadedLevel] + curLevelMaxEntries := maxEntriesForLevel(highestLoadedLevel) + for level := highestLoadedLevel; level > 0; level-- { + // When there are not enough entries left in the current level for the number that would be required to reach + // it, clear the the current level which effectively moves them all up to the previous level on the next + // iteration. Otherwise, there are are sufficient entries, so update the current level to contain as many + // entries as possible while still leaving enough remaining entries required to reach the level. + numEntries := len(curLevelData) / txEntrySize + prevLevelMaxEntries := curLevelMaxEntries / 2 + minPrevRequired := minEntriesToReachLevel(level - 1) + if numEntries < prevLevelMaxEntries+minPrevRequired { + lowestEmptyLevel = level + pendingUpdates[level] = nil + } else { + // This level can only be completely full or half full, so choose the appropriate offset to ensure enough + // entries remain to reach the level. + var offset int + if numEntries-curLevelMaxEntries >= minPrevRequired { + offset = curLevelMaxEntries * txEntrySize + } else { + offset = prevLevelMaxEntries * txEntrySize + } + pendingUpdates[level] = curLevelData[:offset] + curLevelData = curLevelData[offset:] + } + curLevelMaxEntries = prevLevelMaxEntries + } + pendingUpdates[0] = curLevelData + if len(curLevelData) == 0 { + lowestEmptyLevel = 0 + } + // When the highest loaded level is empty, it's possible the level after it still has data and thus that data needs + // to be backfilled as well. + for len(pendingUpdates[highestLoadedLevel]) == 0 { + // When the next level is empty too, the is no data left to continue backfilling, so there is nothing left to + // do. Otherwise, populate the pending updates map with the newly loaded data and update the highest loaded + // level accordingly. + level := highestLoadedLevel + 1 + curLevelKey := keyForLevel(addrKey, level) + levelData := bucket.Get(curLevelKey[:]) + if len(levelData) == 0 { + break + } + pendingUpdates[level] = levelData + highestLoadedLevel = level + // At this point the highest level is not empty, but it might be half full. When that is the case, move it up a + // level to simplify the code below which backfills all lower levels that are still empty. This also means the + // current level will be empty, so the loop will perform another another iteration to potentially backfill this + // level with data from the next one. + curLevelMaxEntries := maxEntriesForLevel(level) + if len(levelData)/txEntrySize != curLevelMaxEntries { + pendingUpdates[level] = nil + pendingUpdates[level-1] = levelData + level-- + curLevelMaxEntries /= 2 + } + // Backfill all lower levels that are still empty by iteratively halfing the data until the lowest empty level + // is filled. + for level > lowestEmptyLevel { + offset := (curLevelMaxEntries / 2) * txEntrySize + pendingUpdates[level] = levelData[:offset] + levelData = levelData[offset:] + pendingUpdates[level-1] = levelData + level-- + curLevelMaxEntries /= 2 + } + // The lowest possible empty level is now the highest loaded level. + lowestEmptyLevel = highestLoadedLevel + } + // Apply the pending updates. + return applyPending() +} + +// addrToKey converts known address types to an addrindex key. An error is returned for unsupported types. +func addrToKey(addr btcaddr.Address) ([addrKeySize]byte, error) { + switch addr := addr.(type) { + case *btcaddr.PubKeyHash: + var result [addrKeySize]byte + result[0] = addrKeyTypePubKeyHash + copy(result[1:], addr.Hash160()[:]) + return result, nil + case *btcaddr.ScriptHash: + var result [addrKeySize]byte + result[0] = addrKeyTypeScriptHash + copy(result[1:], addr.Hash160()[:]) + return result, nil + case *btcaddr.PubKey: + var result [addrKeySize]byte + result[0] = addrKeyTypePubKeyHash + copy(result[1:], addr.PubKeyHash().Hash160()[:]) + return result, nil + // case *util.AddressWitnessScriptHash: + // var result [addrKeySize]byte + // result[0] = addrKeyTypeWitnessScriptHash + // // P2WSH outputs utilize a 32-byte data push created by hashing the script with sha256 instead of hash160. In + // // order to keep all address entries within the database uniform and compact, we use a hash160 here to reduce + // // the size of the salient data push to 20-bytes. + // copy(result[1:], btcaddr.Hash160(addr.ScriptAddress())) + // return result, nil + // case *util.AddressWitnessPubKeyHash: + // var result [addrKeySize]byte + // result[0] = addrKeyTypeWitnessPubKeyHash + // copy(result[1:], addr.Hash160()[:]) + // return result, nil + } + return [addrKeySize]byte{}, errUnsupportedAddressType +} + +// AddrIndex implements a transaction by address index. That is to say, it supports querying all transactions that +// reference a given address because they are either crediting or debiting the address. The returned transactions are +// ordered according to their order of appearance in the blockchain. In other words, first by block height and then by +// offset inside the block. In addition, support is provided for a memory-only index of unconfirmed transactions such as +// those which are kept in the memory pool before inclusion in a block. +type AddrIndex struct { + // The following fields are set when the instance is created and can't be changed afterwards, so there is no need to + // protect them with a separate mutex. + db database.DB + chainParams *chaincfg.Params + // The following fields are used to quickly link transactions and addresses that have not been included into a block + // yet when an address index is being maintained. The are protected by the unconfirmedLock field. The txnsByAddr + // field is used to keep an index of all transactions which either create an output to a given address or spend from + // a previous output to it keyed by the address. The addrsByTx field is essentially the reverse and is used to keep + // an index of all addresses which a given transaction involves. This allows fairly efficient updates when + // transactions are removed once they are included into a block. + unconfirmedLock sync.RWMutex + txnsByAddr map[[addrKeySize]byte]map[chainhash.Hash]*util.Tx + addrsByTx map[chainhash.Hash]map[[addrKeySize]byte]struct{} +} + +// Ensure the AddrIndex type implements the Indexer interface. +var _ Indexer = (*AddrIndex)(nil) + +// Ensure the AddrIndex type implements the NeedsInputser interface. +var _ NeedsInputser = (*AddrIndex)(nil) + +// NeedsInputs signals that the index requires the referenced inputs in order to properly create the index. This +// implements the NeedsInputser interface. +func (idx *AddrIndex) NeedsInputs() bool { + return true +} + +// Init is only provided to satisfy the Indexer interface as there is nothing to initialize for this index. This is part +// of the Indexer interface. +func (idx *AddrIndex) Init() (e error) { + // Nothing to do. + return nil +} + +// Key returns the database key to use for the index as a byte slice. This is part of the Indexer interface. +func (idx *AddrIndex) Key() []byte { + return addrIndexKey +} + +// Name returns the human-readable name of the index. This is part of the Indexer interface. +func (idx *AddrIndex) Name() string { + return addrIndexName +} + +// Create is invoked when the indexer manager determines the index needs to be created for the first time. It creates +// the bucket for the address index. This is part of the Indexer interface. +func (idx *AddrIndex) Create(dbTx database.Tx) (e error) { + _, e = dbTx.Metadata().CreateBucket(addrIndexKey) + return e +} + +// writeIndexData represents the address index data to be written for one block. It consists of the address mapped to an +// ordered list of the transactions that involve the address in block. It is ordered so the transactions can be stored +// in the order they appear in the block. +type writeIndexData map[[addrKeySize]byte][]int + +// indexPkScript extracts all standard addresses from the passed public key script and maps each of them to the +// associated transaction using the passed map. +func (idx *AddrIndex) indexPkScript(data writeIndexData, pkScript []byte, txIdx int) { + // Nothing to index if the script is non-standard or otherwise doesn't contain any addresses. + var addrs []btcaddr.Address + var e error + _, addrs, _, e = txscript.ExtractPkScriptAddrs( + pkScript, + idx.chainParams, + ) + if e != nil || len(addrs) == 0 { + return + } + for _, addr := range addrs { + var addrKey [21]byte + addrKey, e = addrToKey(addr) + if e != nil { + // Ignore unsupported address types. + continue + } + // Avoid inserting the transaction more than once. Since the transactions are indexed serially any duplicates + // will be indexed in a row, so checking the most recent entry for the address is enough to detect duplicates. + indexedTxns := data[addrKey] + numTxns := len(indexedTxns) + if numTxns > 0 && indexedTxns[numTxns-1] == txIdx { + continue + } + indexedTxns = append(indexedTxns, txIdx) + data[addrKey] = indexedTxns + } +} + +// indexBlock extract all of the standard addresses from all of the transactions in the passed block and maps each of +// them to the associated transaction using the passed map. +func (idx *AddrIndex) indexBlock( + data writeIndexData, block *block.Block, + stxos []blockchain.SpentTxOut, +) { + stxoIndex := 0 + for txIdx, tx := range block.Transactions() { + // Coinbases do not reference any inputs. Since the block is required to have already gone through full + // validation, it has already been proven on the first transaction in the block is a coinbase. + if txIdx != 0 { + for range tx.MsgTx().TxIn { + // We'll access the slice of all the transactions spent in this block properly ordered to fetch the + // previous input script. + pkScript := stxos[stxoIndex].PkScript + idx.indexPkScript(data, pkScript, txIdx) + // With an input indexed, we'll advance the stxo coutner. + stxoIndex++ + } + } + for _, txOut := range tx.MsgTx().TxOut { + idx.indexPkScript(data, txOut.PkScript, txIdx) + } + } +} + +// ConnectBlock is invoked by the index manager when a new block has been connected to the main chain. This indexer adds +// a mapping for each address the transactions in the block involve. This is part of the Indexer interface. +func (idx *AddrIndex) ConnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // The offset and length of the transactions within the serialized block. + txLocs, e := block.TxLoc() + if e != nil { + return e + } + // Get the internal block ID associated with the block. + blockID, e := dbFetchBlockIDByHash(dbTx, block.Hash()) + if e != nil { + return e + } + // Build all of the address to transaction mappings in a local map. + addrsToTxns := make(writeIndexData) + idx.indexBlock(addrsToTxns, block, stxos) + // Add all of the index entries for each address. + addrIdxBucket := dbTx.Metadata().Bucket(addrIndexKey) + for addrKey, txIdxs := range addrsToTxns { + for _, txIdx := range txIdxs { + e := dbPutAddrIndexEntry( + addrIdxBucket, addrKey, + blockID, txLocs[txIdx], + ) + if e != nil { + return e + } + } + } + return nil +} + +// DisconnectBlock is invoked by the index manager when a block has been disconnected from the main chain. This indexer +// removes the address mappings each transaction in the block involve. This is part of the Indexer interface. +func (idx *AddrIndex) DisconnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // Build all of the address to transaction mappings in a local map. + addrsToTxns := make(writeIndexData) + idx.indexBlock(addrsToTxns, block, stxos) + // Remove all of the index entries for each address. + bucket := dbTx.Metadata().Bucket(addrIndexKey) + for addrKey, txIdxs := range addrsToTxns { + e := dbRemoveAddrIndexEntries(bucket, addrKey, len(txIdxs)) + if e != nil { + return e + } + } + return nil +} + +// TxRegionsForAddress returns a slice of block regions which identify each transaction that involves the passed address +// according to the specified number to skip, number requested, and whether or not the results should be reversed. It +// also returns the number actually skipped since it could be less in the case where there are not enough entries. NOTE: +// These results only include transactions confirmed in blocks. See the UnconfirmedTxnsForAddress method for obtaining +// unconfirmed transactions that involve a given address. This function is safe for concurrent access. +func (idx *AddrIndex) TxRegionsForAddress( + dbTx database.Tx, addr btcaddr.Address, numToSkip, numRequested uint32, + reverse bool, +) ([]database.BlockRegion, uint32, error) { + addrKey, e := addrToKey(addr) + if e != nil { + return nil, 0, e + } + var regions []database.BlockRegion + var skipped uint32 + e = idx.db.View( + func(dbTx database.Tx) (e error) { + // Create closure to lookup the block hash given the ID using the database transaction. + fetchBlockHash := func(id []byte) (*chainhash.Hash, error) { + // Deserialize and populate the result. + return dbFetchBlockHashBySerializedID(dbTx, id) + } + addrIdxBucket := dbTx.Metadata().Bucket(addrIndexKey) + regions, skipped, e = dbFetchAddrIndexEntries( + addrIdxBucket, + addrKey, numToSkip, numRequested, reverse, + fetchBlockHash, + ) + return e + }, + ) + return regions, skipped, e +} + +// indexUnconfirmedAddresses modifies the unconfirmed (memory-only) address index to include mappings for the addresses +// encoded by the passed public key script to the transaction. This function is safe for concurrent access. +func (idx *AddrIndex) indexUnconfirmedAddresses(pkScript []byte, tx *util.Tx) { + // The error is ignored here since the only reason it can fail is if the script fails to parse and it was already + // validated before being admitted to the mempool. + _, addresses, _, _ := txscript.ExtractPkScriptAddrs( + pkScript, + idx.chainParams, + ) + for _, addr := range addresses { + // Ignore unsupported address types. + addrKey, e := addrToKey(addr) + if e != nil { + continue + } + // Add a mapping from the address to the transaction. + idx.unconfirmedLock.Lock() + addrIndexEntry := idx.txnsByAddr[addrKey] + if addrIndexEntry == nil { + addrIndexEntry = make(map[chainhash.Hash]*util.Tx) + idx.txnsByAddr[addrKey] = addrIndexEntry + } + addrIndexEntry[*tx.Hash()] = tx + // Add a mapping from the transaction to the address. + addrsByTxEntry := idx.addrsByTx[*tx.Hash()] + if addrsByTxEntry == nil { + addrsByTxEntry = make(map[[addrKeySize]byte]struct{}) + idx.addrsByTx[*tx.Hash()] = addrsByTxEntry + } + addrsByTxEntry[addrKey] = struct{}{} + idx.unconfirmedLock.Unlock() + } +} + +// AddUnconfirmedTx adds all addresses related to the transaction to the unconfirmed (memory-only) address index. NOTE: +// This transaction MUST have already been validated by the memory pool before calling this function with it and have +// all of the inputs available in the provided utxo view. Failure to do so could result in some or all addresses not +// being indexed. This function is safe for concurrent access. +func (idx *AddrIndex) AddUnconfirmedTx(tx *util.Tx, utxoView *blockchain.UtxoViewpoint) { + // Index addresses of all referenced previous transaction outputs. The existence checks are elided since this is + // only called after the transaction has already been validated and thus all inputs are already known to exist. + for _, txIn := range tx.MsgTx().TxIn { + entry := utxoView.LookupEntry(txIn.PreviousOutPoint) + if entry == nil { + // Ignore missing entries. This should never happen in practice since the function comments specifically + // call out all inputs must be available. + continue + } + idx.indexUnconfirmedAddresses(entry.PkScript(), tx) + } + // Index addresses of all created outputs. + for _, txOut := range tx.MsgTx().TxOut { + idx.indexUnconfirmedAddresses(txOut.PkScript, tx) + } +} + +// RemoveUnconfirmedTx removes the passed transaction from the unconfirmed (memory-only) address index. This function is +// safe for concurrent access. +func (idx *AddrIndex) RemoveUnconfirmedTx(hash *chainhash.Hash) { + idx.unconfirmedLock.Lock() + defer idx.unconfirmedLock.Unlock() + // Remove all address references to the transaction from the address index and remove the entry for the address + // altogether if it no longer references any transactions. + for addrKey := range idx.addrsByTx[*hash] { + delete(idx.txnsByAddr[addrKey], *hash) + if len(idx.txnsByAddr[addrKey]) == 0 { + delete(idx.txnsByAddr, addrKey) + } + } + // Remove the entry from the transaction to address lookup map as well. + delete(idx.addrsByTx, *hash) +} + +// UnconfirmedTxnsForAddress returns all transactions currently in the unconfirmed (memory-only) address index that +// involve the passed address. Unsupported address types are ignored and will result in no results. This function is +// safe for concurrent access. +func (idx *AddrIndex) UnconfirmedTxnsForAddress(addr btcaddr.Address) []*util.Tx { + // Ignore unsupported address types. + addrKey, e := addrToKey(addr) + if e != nil { + return nil + } + // Protect concurrent access. + idx.unconfirmedLock.RLock() + defer idx.unconfirmedLock.RUnlock() + // Return a new slice with the results if there are any. This ensures safe concurrency. + if txns, exists := idx.txnsByAddr[addrKey]; exists { + addressTxns := make([]*util.Tx, 0, len(txns)) + for _, tx := range txns { + addressTxns = append(addressTxns, tx) + } + return addressTxns + } + return nil +} + +// NewAddrIndex returns a new instance of an indexer that is used to create a mapping of all addresses in the blockchain +// to the respective transactions that involve them. It implements the Indexer interface which plugs into the +// IndexManager that in turn is used by the blockchain package. This allows the index to be seamlessly maintained along +// with the chain. +func NewAddrIndex(db database.DB, chainParams *chaincfg.Params) *AddrIndex { + return &AddrIndex{ + db: db, + chainParams: chainParams, + txnsByAddr: make(map[[addrKeySize]byte]map[chainhash.Hash]*util.Tx), + addrsByTx: make(map[chainhash.Hash]map[[addrKeySize]byte]struct{}), + } +} + +// DropAddrIndex drops the address index from the provided database if it exists. +func DropAddrIndex(db database.DB, interrupt qu.C) (e error) { + return dropIndex(db, addrIndexKey, addrIndexName, interrupt) +} diff --git a/pkg/indexers/index/addrindex_test.go b/pkg/indexers/index/addrindex_test.go new file mode 100644 index 0000000..79331f7 --- /dev/null +++ b/pkg/indexers/index/addrindex_test.go @@ -0,0 +1,258 @@ +package index + +import ( + "bytes" + "fmt" + "testing" + + "github.com/p9c/p9/pkg/wire" +) + +// addrIndexBucket provides a mock address index database bucket by implementing the internalBucket interface. +type addrIndexBucket struct { + levels map[[levelKeySize]byte][]byte +} + +// Clone returns a deep copy of the mock address index bucket. +func (b *addrIndexBucket) Clone() *addrIndexBucket { + levels := make(map[[levelKeySize]byte][]byte) + for k, v := range b.levels { + vCopy := make([]byte, len(v)) + copy(vCopy, v) + levels[k] = vCopy + } + return &addrIndexBucket{levels: levels} +} + +// Get returns the value associated with the key from the mock address index bucket. +// +// This is part of the internalBucket interface. +func (b *addrIndexBucket) Get(key []byte) []byte { + var levelKey [levelKeySize]byte + copy(levelKey[:], key) + return b.levels[levelKey] +} + +// Put stores the provided key/value pair to the mock address index bucket. +// +// This is part of the internalBucket interface. +func (b *addrIndexBucket) Put(key []byte, value []byte) (e error) { + var levelKey [levelKeySize]byte + copy(levelKey[:], key) + b.levels[levelKey] = value + return nil +} + +// Delete removes the provided key from the mock address index bucket. +// +// This is part of the internalBucket interface. +func (b *addrIndexBucket) Delete(key []byte) (e error) { + var levelKey [levelKeySize]byte + copy(levelKey[:], key) + delete(b.levels, levelKey) + return nil +} + +// printLevels returns a string with a visual representation of the provided address key taking into account the max +// size of each level. It is useful when creating and debugging test cases. +func (b *addrIndexBucket) printLevels(addrKey [addrKeySize]byte) string { + highestLevel := uint8(0) + for k := range b.levels { + if !bytes.Equal(k[:levelOffset], addrKey[:]) { + continue + } + level := k[levelOffset] + if level > highestLevel { + highestLevel = level + } + } + var levelBuf bytes.Buffer + _, _ = levelBuf.WriteString("\n") + maxEntries := level0MaxEntries + for level := uint8(0); level <= highestLevel; level++ { + data := b.levels[keyForLevel(addrKey, level)] + numEntries := len(data) / txEntrySize + for i := 0; i < numEntries; i++ { + start := i * txEntrySize + num := byteOrder.Uint32(data[start:]) + _, _ = levelBuf.WriteString(fmt.Sprintf("%02d ", num)) + } + for i := numEntries; i < maxEntries; i++ { + _, _ = levelBuf.WriteString("_ ") + } + _, _ = levelBuf.WriteString("\n") + maxEntries *= 2 + } + return levelBuf.String() +} + +// sanityCheck ensures that all data stored in the bucket for the given address adheres to the level-based rules +// described by the address index documentation. +func (b *addrIndexBucket) sanityCheck(addrKey [addrKeySize]byte, expectedTotal int) (e error) { + // Find the highest level for the key. + highestLevel := uint8(0) + for k := range b.levels { + if !bytes.Equal(k[:levelOffset], addrKey[:]) { + continue + } + level := k[levelOffset] + if level > highestLevel { + highestLevel = level + } + } + // Ensure the expected total number of entries are present and that all levels adhere to the rules described in the + // address index documentation. + var totalEntries int + maxEntries := level0MaxEntries + for level := uint8(0); level <= highestLevel; level++ { + // Level 0 can'have more entries than the max allowed if the levels after it have data and it can't be empty. + // All other levels must either be half full or full. + data := b.levels[keyForLevel(addrKey, level)] + numEntries := len(data) / txEntrySize + totalEntries += numEntries + if level == 0 { + if (highestLevel != 0 && numEntries == 0) || + numEntries > maxEntries { + return fmt.Errorf("level %d has %d entries", + level, numEntries, + ) + } + } else if numEntries != maxEntries && numEntries != maxEntries/2 { + return fmt.Errorf("level %d has %d entries", level, + numEntries, + ) + } + maxEntries *= 2 + } + if totalEntries != expectedTotal { + return fmt.Errorf("expected %d entries - got %d", expectedTotal, + totalEntries, + ) + } + // Ensure all of the numbers are in order starting from the highest level moving to the lowest level. + expectedNum := uint32(0) + for level := highestLevel + 1; level > 0; level-- { + data := b.levels[keyForLevel(addrKey, level)] + numEntries := len(data) / txEntrySize + for i := 0; i < numEntries; i++ { + start := i * txEntrySize + num := byteOrder.Uint32(data[start:]) + if num != expectedNum { + return fmt.Errorf("level %d offset %d does "+ + "not contain the expected number of "+ + "%d - got %d", level, i, num, + expectedNum, + ) + } + expectedNum++ + } + } + return nil +} + +// TestAddrIndexLevels ensures that adding and deleting entries to the address index creates multiple levels as +// described by the address index documentation. +func TestAddrIndexLevels(t *testing.T) { + t.Parallel() + tests := []struct { + name string + key [addrKeySize]byte + numInsert int + printLevels bool // Set to help debug a specific test. + }{ + { + name: "level 0 not full", + numInsert: level0MaxEntries - 1, + }, + { + name: "level 1 half", + numInsert: level0MaxEntries + 1, + }, + { + name: "level 1 full", + numInsert: level0MaxEntries*2 + 1, + }, + { + name: "level 2 half, level 1 half", + numInsert: level0MaxEntries*3 + 1, + }, + { + name: "level 2 half, level 1 full", + numInsert: level0MaxEntries*4 + 1, + }, + { + name: "level 2 full, level 1 half", + numInsert: level0MaxEntries*5 + 1, + }, + { + name: "level 2 full, level 1 full", + numInsert: level0MaxEntries*6 + 1, + }, + { + name: "level 3 half, level 2 half, level 1 half", + numInsert: level0MaxEntries*7 + 1, + }, + { + name: "level 3 full, level 2 half, level 1 full", + numInsert: level0MaxEntries*12 + 1, + }, + } +nextTest: + for testNum, test := range tests { + // Insert entries in order. + populatedBucket := &addrIndexBucket{ + levels: make(map[[levelKeySize]byte][]byte), + } + for i := 0; i < test.numInsert; i++ { + txLoc := wire.TxLoc{TxStart: i * 2} + e := dbPutAddrIndexEntry(populatedBucket, test.key, + uint32(i), txLoc, + ) + if e != nil { + t.Errorf("dbPutAddrIndexEntry #%d (%s) - "+ + "unexpected error: %v", testNum, + test.name, e, + ) + continue nextTest + } + } + if test.printLevels { + t.Log(populatedBucket.printLevels(test.key)) + } + // Delete entries from the populated bucket until all entries have been deleted. The bucket is reset to the + // fully populated bucket on each iteration so every combination is tested. Notice the upper limit purposes + // exceeds the number of entries to ensure attempting to delete more entries than there are works correctly. + for numDelete := 0; numDelete <= test.numInsert+1; numDelete++ { + // Clone populated bucket to run each delete against. + bucket := populatedBucket.Clone() + // Remove the number of entries for this iteration. + e := dbRemoveAddrIndexEntries(bucket, test.key, + numDelete, + ) + if e != nil { + if numDelete <= test.numInsert { + t.Errorf("dbRemoveAddrIndexEntries (%s) "+ + " delete %d - unexpected error: "+ + "%v", test.name, numDelete, e, + ) + continue nextTest + } + } + if test.printLevels { + t.Log(bucket.printLevels(test.key)) + } + // Sanity check the levels to ensure the adhere to all rules. + numExpected := test.numInsert + if numDelete <= test.numInsert { + numExpected -= numDelete + } + e = bucket.sanityCheck(test.key, numExpected) + if e != nil { + t.Errorf("sanity check fail (%s) delete %d: %v", + test.name, numDelete, e, + ) + continue nextTest + } + } + } +} diff --git a/pkg/indexers/index/blocklogger.go b/pkg/indexers/index/blocklogger.go new file mode 100644 index 0000000..0d1fd9d --- /dev/null +++ b/pkg/indexers/index/blocklogger.go @@ -0,0 +1,73 @@ +package index + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + "sync" + "time" +) + +// blockProgressLogger provides periodic logging for other services in order to show users progress of certain "actions" +// involving some or all current blocks. Ex: syncing to best chain, indexing all blocks, etc. +type blockProgressLogger struct { + receivedLogBlocks int64 + receivedLogTx int64 + lastBlockLogTime time.Time + // subsystemLogger *log.Logger + progressAction string + sync.Mutex +} + +// newBlockProgressLogger returns a new block progress logger. The progress message is templated as follows: +// {progressAction} {numProcessed} {blocks|block} in the last {timePeriod} +// ({numTxs}, height {lastBlockHeight}, {lastBlockTimeStamp}) +func newBlockProgressLogger( + progressMessage string, +// logger *log.Logger +) *blockProgressLogger { + return &blockProgressLogger{ + lastBlockLogTime: time.Now(), + progressAction: progressMessage, + // subsystemLogger: logger, + } +} + +// LogBlockHeight logs a new block height as an information message to show progress to the user. In order to prevent +// spam, it limits logging to one message every 10 seconds with duration and totals included. +func (b *blockProgressLogger) LogBlockHeight(block *block.Block) { + b.Lock() + defer b.Unlock() + b.receivedLogBlocks++ + b.receivedLogTx += int64(len(block.WireBlock().Transactions)) + now := time.Now() + duration := now.Sub(b.lastBlockLogTime) + if duration < time.Second*10 { + return + } + // Truncate the duration to 10s of milliseconds. + durationMillis := int64(duration / time.Millisecond) + tDuration := 10 * time.Millisecond * time.Duration(durationMillis/10) + // Log information about new block height. + blockStr := "blocks" + if b.receivedLogBlocks == 1 { + blockStr = "block " + } + txStr := "transactions" + if b.receivedLogTx == 1 { + txStr = "transaction " + } + I.F( + "%s %6d %s in the last %s (%6d %s, height %6d, %s)", + b.progressAction, + b.receivedLogBlocks, + blockStr, + fmt.Sprintf("%0.1fs", tDuration.Seconds()), + b.receivedLogTx, + txStr, + block.Height(), + block.WireBlock().Header.Timestamp, + ) + b.receivedLogBlocks = 0 + b.receivedLogTx = 0 + b.lastBlockLogTime = now +} diff --git a/pkg/indexers/index/cfindex.go b/pkg/indexers/index/cfindex.go new file mode 100644 index 0000000..b5567fd --- /dev/null +++ b/pkg/indexers/index/cfindex.go @@ -0,0 +1,333 @@ +package index + +import ( + "errors" + "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/gcs" + "github.com/p9c/p9/pkg/gcs/builder" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // cfIndexName is the human-readable name for the index. + cfIndexName = "committed filter index" +) + +// Committed filters come in one flavor currently: basic. They are generated and dropped in pairs, and both are indexed +// by a block's hash. Besides holding different content, they also live in different buckets. +var ( + // cfIndexParentBucketKey is the name of the parent bucket used to house the index. The rest of the buckets live + // below this bucket. + cfIndexParentBucketKey = []byte("cfindexparentbucket") + // cfIndexKeys is an array of db bucket names used to house indexes of block hashes to cfilters. + cfIndexKeys = [][]byte{ + []byte("cf0byhashidx"), + } + // cfHeaderKeys is an array of db bucket names used to house indexes of block hashes to cf headers. + cfHeaderKeys = [][]byte{ + []byte("cf0headerbyhashidx"), + } + // cfHashKeys is an array of db bucket names used to house indexes of block hashes to cf hashes. + cfHashKeys = [][]byte{ + []byte("cf0hashbyhashidx"), + } + maxFilterType = uint8(len(cfHeaderKeys) - 1) + // zeroHash is the chainhash.Hash value of all zero bytes, defined here for convenience. + zeroHash chainhash.Hash +) + +// dbFetchFilterIdxEntry retrieves a data blob from the filter index database. An entry's absence is not considered an +// error. +func dbFetchFilterIdxEntry(dbTx database.Tx, key []byte, h *chainhash.Hash) ([]byte, error) { + idx := dbTx.Metadata().Bucket(cfIndexParentBucketKey).Bucket(key) + return idx.Get(h[:]), nil +} + +// dbStoreFilterIdxEntry stores a data blob in the filter index database. +func dbStoreFilterIdxEntry(dbTx database.Tx, key []byte, h *chainhash.Hash, f []byte) (e error) { + idx := dbTx.Metadata().Bucket(cfIndexParentBucketKey).Bucket(key) + return idx.Put(h[:], f) +} + +// dbDeleteFilterIdxEntry deletes a data blob from the filter index database. +func dbDeleteFilterIdxEntry(dbTx database.Tx, key []byte, h *chainhash.Hash) (e error) { + idx := dbTx.Metadata().Bucket(cfIndexParentBucketKey).Bucket(key) + return idx.Delete(h[:]) +} + +// CFIndex implements a committed filter (cf) by hash index. +type CFIndex struct { + db database.DB + chainParams *chaincfg.Params +} + +// Ensure the CfIndex type implements the Indexer interface. +var _ Indexer = (*CFIndex)(nil) + +// Ensure the CfIndex type implements the NeedsInputser interface. +var _ NeedsInputser = (*CFIndex)(nil) + +// NeedsInputs signals that the index requires the referenced inputs in order to properly create the index. This +// implements the NeedsInputser interface. +func (idx *CFIndex) NeedsInputs() bool { + return true +} + +// Init initializes the hash-based cf index. This is part of the Indexer interface. +func (idx *CFIndex) Init() (e error) { + return nil // Nothing to do. +} + +// Key returns the database key to use for the index as a byte slice. This is part of the Indexer interface. +func (idx *CFIndex) Key() []byte { + return cfIndexParentBucketKey +} + +// Name returns the human-readable name of the index. This is part of the Indexer interface. +func (idx *CFIndex) Name() string { + return cfIndexName +} + +// Create is invoked when the indexer manager determines the index needs to be created for the first time. It creates +// buckets for the two hash-based cf indexes (regular only currently). +func (idx *CFIndex) Create(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + cfIndexParentBucket, e := meta.CreateBucket(cfIndexParentBucketKey) + if e != nil { + return e + } + for _, bucketName := range cfIndexKeys { + _, e = cfIndexParentBucket.CreateBucket(bucketName) + if e != nil { + return e + } + } + for _, bucketName := range cfHeaderKeys { + _, e = cfIndexParentBucket.CreateBucket(bucketName) + if e != nil { + return e + } + } + for _, bucketName := range cfHashKeys { + _, e = cfIndexParentBucket.CreateBucket(bucketName) + if e != nil { + return e + } + } + return nil +} + +// storeFilter stores a given filter, and performs the steps needed to generate the filter's header. +func storeFilter( + dbTx database.Tx, block *block.Block, f *gcs.Filter, + filterType wire.FilterType, +) (e error) { + if uint8(filterType) > maxFilterType { + return errors.New("unsupported filter type") + } + // Figure out which buckets to use. + fkey := cfIndexKeys[filterType] + hkey := cfHeaderKeys[filterType] + hashkey := cfHashKeys[filterType] + // Start by storing the filter. + h := block.Hash() + filterBytes, e := f.NBytes() + if e != nil { + return e + } + e = dbStoreFilterIdxEntry(dbTx, fkey, h, filterBytes) + if e != nil { + return e + } + // Next store the filter hash. + filterHash, e := builder.GetFilterHash(f) + if e != nil { + return e + } + e = dbStoreFilterIdxEntry(dbTx, hashkey, h, filterHash[:]) + if e != nil { + return e + } + // Then fetch the previous block's filter header. + var prevHeader *chainhash.Hash + ph := &block.WireBlock().Header.PrevBlock + if ph.IsEqual(&zeroHash) { + prevHeader = &zeroHash + } else { + var pfh []byte + pfh, e = dbFetchFilterIdxEntry(dbTx, hkey, ph) + if e != nil { + return e + } + // Construct the new block's filter header, and store it. + prevHeader, e = chainhash.NewHash(pfh) + if e != nil { + return e + } + } + fh, e := builder.MakeHeaderForFilter(f, *prevHeader) + if e != nil { + return e + } + return dbStoreFilterIdxEntry(dbTx, hkey, h, fh[:]) +} + +// ConnectBlock is invoked by the index manager when a new block has been connected to the main chain. This indexer adds +// a hash-to-cf mapping for every passed block. This is part of the Indexer interface. +func (idx *CFIndex) ConnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + prevScripts := make([][]byte, len(stxos)) + for i, stxo := range stxos { + prevScripts[i] = stxo.PkScript + } + f, e := builder.BuildBasicFilter(block.WireBlock(), prevScripts) + if e != nil { + return e + } + return storeFilter(dbTx, block, f, wire.GCSFilterRegular) +} + +// DisconnectBlock is invoked by the index manager when a block has been disconnected from the main chain. This indexer +// removes the hash-to-cf mapping for every passed block. This is part of the Indexer interface. +func (idx *CFIndex) DisconnectBlock( + dbTx database.Tx, block *block.Block, + _ []blockchain.SpentTxOut, +) (e error) { + for _, key := range cfIndexKeys { + e := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) + if e != nil { + return e + } + } + for _, key := range cfHeaderKeys { + e := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) + if e != nil { + return e + } + } + for _, key := range cfHashKeys { + e := dbDeleteFilterIdxEntry(dbTx, key, block.Hash()) + if e != nil { + return e + } + } + return nil +} + +// entryByBlockHash fetches a filter index entry of a particular type (eg. filter, filter header, etc) for a filter type +// and block hash. +func (idx *CFIndex) entryByBlockHash( + filterTypeKeys [][]byte, + filterType wire.FilterType, h *chainhash.Hash, +) (entry []byte, e error) { + if uint8(filterType) > maxFilterType { + return nil, errors.New("unsupported filter type") + } + key := filterTypeKeys[filterType] + e = idx.db.View( + func(dbTx database.Tx) (e error) { + entry, e = dbFetchFilterIdxEntry(dbTx, key, h) + return e + }, + ) + return entry, e +} + +// entriesByBlockHashes batch fetches a filter index entry of a particular type (eg. filter, filter header, etc) for a +// filter type and slice of block hashes. +func (idx *CFIndex) entriesByBlockHashes( + filterTypeKeys [][]byte, + filterType wire.FilterType, blockHashes []*chainhash.Hash, +) (entries [][]byte, e error) { + if uint8(filterType) > maxFilterType { + return nil, errors.New("unsupported filter type") + } + key := filterTypeKeys[filterType] + entries = make([][]byte, 0, len(blockHashes)) + e = idx.db.View( + func(dbTx database.Tx) (e error) { + for _, blockHash := range blockHashes { + entry, e := dbFetchFilterIdxEntry(dbTx, key, blockHash) + if e != nil { + return e + } + entries = append(entries, entry) + } + return nil + }, + ) + return entries, e +} + +// FilterByBlockHash returns the serialized contents of a block's basic or committed filter. +func (idx *CFIndex) FilterByBlockHash( + h *chainhash.Hash, + filterType wire.FilterType, +) ([]byte, error) { + return idx.entryByBlockHash(cfIndexKeys, filterType, h) +} + +// FiltersByBlockHashes returns the serialized contents of a block's basic or committed filter for a set of blocks by +// hash. +func (idx *CFIndex) FiltersByBlockHashes( + blockHashes []*chainhash.Hash, + filterType wire.FilterType, +) ([][]byte, error) { + return idx.entriesByBlockHashes(cfIndexKeys, filterType, blockHashes) +} + +// FilterHeaderByBlockHash returns the serialized contents of a block's basic committed filter header. +func (idx *CFIndex) FilterHeaderByBlockHash( + h *chainhash.Hash, + filterType wire.FilterType, +) ([]byte, error) { + return idx.entryByBlockHash(cfHeaderKeys, filterType, h) +} + +// FilterHeadersByBlockHashes returns the serialized contents of a block's basic committed filter header for a set of +// blocks by hash. +func (idx *CFIndex) FilterHeadersByBlockHashes( + blockHashes []*chainhash.Hash, + filterType wire.FilterType, +) ([][]byte, error) { + return idx.entriesByBlockHashes(cfHeaderKeys, filterType, blockHashes) +} + +// FilterHashByBlockHash returns the serialized contents of a block's basic committed filter hash. +func (idx *CFIndex) FilterHashByBlockHash( + h *chainhash.Hash, + filterType wire.FilterType, +) ([]byte, error) { + return idx.entryByBlockHash(cfHashKeys, filterType, h) +} + +// FilterHashesByBlockHashes returns the serialized contents of a block's basic committed filter hash for a set of +// blocks by hash. +func (idx *CFIndex) FilterHashesByBlockHashes( + blockHashes []*chainhash.Hash, + filterType wire.FilterType, +) ([][]byte, error) { + return idx.entriesByBlockHashes(cfHashKeys, filterType, blockHashes) +} + +// NewCfIndex returns a new instance of an indexer that is used to create a mapping of the hashes of all blocks in the +// blockchain to their respective committed filters. It implements the Indexer interface which plugs into the +// IndexManager that in turn is used by the blockchain package. This allows the index to be seamlessly maintained along +// with the chain. +func NewCfIndex(db database.DB, chainParams *chaincfg.Params) *CFIndex { + return &CFIndex{db: db, chainParams: chainParams} +} + +// DropCfIndex drops the CF index from the provided database if exists. +func DropCfIndex(db database.DB, interrupt qu.C) (e error) { + return dropIndex(db, cfIndexParentBucketKey, cfIndexName, interrupt) +} diff --git a/pkg/indexers/index/common.go b/pkg/indexers/index/common.go new file mode 100644 index 0000000..c1da112 --- /dev/null +++ b/pkg/indexers/index/common.go @@ -0,0 +1,86 @@ +// Package indexers implements optional block chain indexes. +package index + +import ( + "encoding/binary" + "errors" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/database" +) + +var ( + // byteOrder is the preferred byte order used for serializing numeric fields for storage in the database. + byteOrder = binary.LittleEndian + // errInterruptRequested indicates that an operation was cancelled due to a user-requested interrupt. + errInterruptRequested = errors.New("interrupt requested") +) + +// NeedsInputser provides a generic interface for an indexer to specify the it requires the ability to look up inputs +// for a transaction. +type NeedsInputser interface { + NeedsInputs() bool +} + +// Indexer provides a generic interface for an indexer that is managed by an index manager such as the Manager type +// provided by this package. +type Indexer interface { + // Key returns the key of the index as a byte slice. + Key() []byte + // Name returns the human-readable name of the index. + Name() string + // Create is invoked when the indexer manager determines the index needs to be created for the first time. + Create(dbTx database.Tx) error + // Init is invoked when the index manager is first initializing the index. This differs from the Create method in + // that it is called on every load, including the case the index was just created. + Init() error + // ConnectBlock is invoked when a new block has been connected to the main chain. The set of output spent within a + // block is also passed in so indexers can access the pevious output scripts input spent if required. + ConnectBlock(database.Tx, *block.Block, []blockchain.SpentTxOut) error + // DisconnectBlock is invoked when a block has been disconnected from the main chain. The set of outputs scripts + // that were spent within this block is also returned so indexers can clean up the prior index state for this block + DisconnectBlock(database.Tx, *block.Block, []blockchain.SpentTxOut) error +} + +// AssertError identifies an error that indicates an internal code consistency issue and should be treated as a critical +// and unrecoverable error. +type AssertError string + +// DBError returns the assertion error as a huma-readable string and satisfies the error interface. +func (e AssertError) Error() string { + return "assertion failed: " + string(e) +} + +// errDeserialize signifies that a problem was encountered when deserializing data. +type errDeserialize string + +// DBError implements the error interface. +func (e errDeserialize) Error() string { + return string(e) +} + +// isDeserializeErr returns whether or not the passed error is an errDeserialize error. +func isDeserializeErr(e error) bool { + _, ok := e.(errDeserialize) + return ok +} + +// internalBucket is an abstraction over a database bucket. It is used to make the code easier to test since it allows +// mock objects in the tests to only implement these functions instead of everything a database.Bucket supports. +type internalBucket interface { + Get(key []byte) []byte + Put(key []byte, value []byte) error + Delete(key []byte) error +} + +// interruptRequested returns true when the provided channel has been closed. This simplifies early shutdown slightly +// since the caller can just use an if statement instead of a select. +func interruptRequested(interrupted <-chan struct{}) bool { + select { + case <-interrupted: + return true + default: + } + return false +} diff --git a/pkg/indexers/index/log.go b/pkg/indexers/index/log.go new file mode 100644 index 0000000..fd19289 --- /dev/null +++ b/pkg/indexers/index/log.go @@ -0,0 +1,43 @@ +package index + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/indexers/index/manager.go b/pkg/indexers/index/manager.go new file mode 100644 index 0000000..3505e25 --- /dev/null +++ b/pkg/indexers/index/manager.go @@ -0,0 +1,646 @@ +package index + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" +) + +var ( + // indexTipsBucketName is the name of the db bucket used to house the current tip of each index. + indexTipsBucketName = []byte("idxtips") +) + +// The index manager tracks the current tip of each index by using a parent bucket that contains an entry for index. +// +// The serialized format for an index tip is: +// +// [],... +// Field Type Size +// block hash chainhash.Hash chainhash.HashSize +// block height uint32 4 bytes + +// dbPutIndexerTip uses an existing database transaction to update or add the current tip for the given index to the +// provided values. +func dbPutIndexerTip(dbTx database.Tx, idxKey []byte, hash *chainhash.Hash, height int32) (e error) { + serialized := make([]byte, chainhash.HashSize+4) + copy(serialized, hash[:]) + byteOrder.PutUint32(serialized[chainhash.HashSize:], uint32(height)) + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + return indexesBucket.Put(idxKey, serialized) +} + +// dbFetchIndexerTip uses an existing database transaction to retrieve the hash and height of the current tip for the +// provided index. +func dbFetchIndexerTip(dbTx database.Tx, idxKey []byte) (*chainhash.Hash, int32, error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + serialized := indexesBucket.Get(idxKey) + if len(serialized) < chainhash.HashSize+4 { + return nil, 0, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "unexpected end of data for "+ + "index %q tip", string(idxKey), + ), + } + } + var hash chainhash.Hash + copy(hash[:], serialized[:chainhash.HashSize]) + height := int32(byteOrder.Uint32(serialized[chainhash.HashSize:])) + return &hash, height, nil +} + +// dbIndexConnectBlock adds all of the index entries associated with the given block using the provided indexer and +// updates the tip of the indexer accordingly. An error will be returned if the current tip for the indexer is not the +// previous block for the passed block. +func dbIndexConnectBlock( + dbTx database.Tx, indexer Indexer, block *block.Block, + stxo []blockchain.SpentTxOut, +) (e error) { + // Assert that the block being connected properly connects to the current tip of the index. + idxKey := indexer.Key() + var curTipHash *chainhash.Hash + if curTipHash, _, e = dbFetchIndexerTip(dbTx, idxKey); E.Chk(e) { + return e + } + if !curTipHash.IsEqual(&block.WireBlock().Header.PrevBlock) { + return AssertError( + fmt.Sprintf( + "dbIndexConnectBlock must be "+ + "called with a block that extends the current index "+ + "tip (%s, tip %s, block %s)", indexer.Name(), + curTipHash, block.Hash(), + ), + ) + } + // Notify the indexer with the connected block so it can index it. + if e := indexer.ConnectBlock(dbTx, block, stxo); E.Chk(e) { + return e + } + // Update the current index tip. + return dbPutIndexerTip(dbTx, idxKey, block.Hash(), block.Height()) +} + +// dbIndexDisconnectBlock removes all of the index entries associated with the given block using the provided indexer +// and updates the tip of the indexer accordingly. An error will be returned if the current tip for the indexer is not +// the passed block. +func dbIndexDisconnectBlock( + dbTx database.Tx, indexer Indexer, block *block.Block, + stxo []blockchain.SpentTxOut, +) (e error) { + // Assert that the block being disconnected is the current tip of the index. + idxKey := indexer.Key() + var curTipHash *chainhash.Hash + if curTipHash, _, e = dbFetchIndexerTip(dbTx, idxKey); E.Chk(e) { + return e + } + if !curTipHash.IsEqual(block.Hash()) { + return AssertError( + fmt.Sprintf( + "dbIndexDisconnectBlock must "+ + "be called with the block at the current index tip "+ + "(%s, tip %s, block %s)", indexer.Name(), + curTipHash, block.Hash(), + ), + ) + } + // Notify the indexer with the disconnected block so it can remove all of + // the appropriate entries. + if e := indexer.DisconnectBlock(dbTx, block, stxo); E.Chk(e) { + return e + } + // Update the current index tip. + prevHash := &block.WireBlock().Header.PrevBlock + return dbPutIndexerTip(dbTx, idxKey, prevHash, block.Height()-1) +} + +// Manager defines an index manager that manages multiple optional indexes and implements the blockchain. IndexManager +// interface so it can be seamlessly plugged into normal chain processing. +type Manager struct { + db database.DB + enabledIndexes []Indexer +} + +// Ensure the Manager type implements the blockchain.IndexManager interface. +var _ blockchain.IndexManager = (*Manager)(nil) + +// indexDropKey returns the key for an index which indicates it is in the process of being dropped. +func indexDropKey(idxKey []byte) []byte { + dropKey := make([]byte, len(idxKey)+1) + dropKey[0] = 'd' + copy(dropKey[1:], idxKey) + return dropKey +} + +// maybeFinishDrops determines if each of the enabled indexes are in the middle of being dropped and finishes dropping +// them when the are. This is necessary because dropping and index has to be done in several atomic steps rather than +// one big atomic step due to the massive number of entries. +func (m *Manager) maybeFinishDrops(interrupt <-chan struct{}) (e error) { + indexNeedsDrop := make([]bool, len(m.enabledIndexes)) + if e = m.db.View( + func(dbTx database.Tx) (e error) { + // None of the indexes needs to be dropped if the index tips bucket hasn't been created yet. + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + if indexesBucket == nil { + return nil + } + // Mark the indexer as requiring a drop if one is already in progress. + for i, indexer := range m.enabledIndexes { + dropKey := indexDropKey(indexer.Key()) + if indexesBucket.Get(dropKey) != nil { + indexNeedsDrop[i] = true + } + } + return nil + }, + ); E.Chk(e) { + return e + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Finish dropping any of the enabled indexes that are already in the + // middle of being dropped. + for i, indexer := range m.enabledIndexes { + if !indexNeedsDrop[i] { + continue + } + I.C( + func() string { + return fmt.Sprintf("Resuming %s drop", indexer.Name()) + }, + ) + if e = dropIndex(m.db, indexer.Key(), indexer.Name(), interrupt); E.Chk(e) { + return e + } + } + return nil +} + +// maybeCreateIndexes determines if each of the enabled indexes have already been created and creates them if not. +func (m *Manager) maybeCreateIndexes(dbTx database.Tx) (e error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + for _, indexer := range m.enabledIndexes { + // Nothing to do if the index tip already exists. + idxKey := indexer.Key() + if indexesBucket.Get(idxKey) != nil { + continue + } + // The tip for the index does not exist, so create it and invoke the create callback for the index so it can + // perform any one-time initialization it requires. + if e := indexer.Create(dbTx); E.Chk(e) { + return e + } + // Set the tip for the index to values which represent an uninitialized index. + e := dbPutIndexerTip(dbTx, idxKey, &chainhash.Hash{}, -1) + if e != nil { + return e + } + } + return nil +} + +// Init initializes the enabled indexes. This is called during chain initialization and primarily consists of catching +// up all indexes to the current best chain tip. This is necessary since each index can be disabled and re-enabled at +// any time and attempting to catch-up indexes at the same time new blocks are being downloaded would lead to an overall +// longer time to catch up due to the I/O contention. This is part of the blockchain.IndexManager interface. +func (m *Manager) Init(chain *blockchain.BlockChain, interrupt <-chan struct{}) (e error) { + // Nothing to do when no indexes are enabled. + if len(m.enabledIndexes) == 0 { + return nil + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Finish and drops that were previously interrupted. + if e = m.maybeFinishDrops(interrupt); E.Chk(e) { + return e + } + // Create the initial state for the indexes as needed. + e = m.db.Update( + func(dbTx database.Tx) (e error) { + // Create the bucket for the current tips as needed. + meta := dbTx.Metadata() + if _, e = meta.CreateBucketIfNotExists(indexTipsBucketName); E.Chk(e) { + return e + } + return m.maybeCreateIndexes(dbTx) + }, + ) + if E.Chk(e) { + return e + } + // Initialize each of the enabled indexes. + for _, indexer := range m.enabledIndexes { + if e = indexer.Init(); E.Chk(e) { + return e + } + } + // Rollback indexes to the main chain if their tip is an orphaned fork. This is fairly unlikely, but it can happen + // if the chain is reorganized while the index is disabled. This has to be done in reverse order because later + // indexes can depend on earlier ones. + var height int32 + var hash *chainhash.Hash + for i := len(m.enabledIndexes); i > 0; i-- { + indexer := m.enabledIndexes[i-1] + // Fetch the current tip for the index. + e = m.db.View( + func(dbTx database.Tx) (e error) { + idxKey := indexer.Key() + hash, height, e = dbFetchIndexerTip(dbTx, idxKey) + return e + }, + ) + if e != nil { + return e + } + // Nothing to do if the index does not have any entries yet. + if height == -1 { + continue + } + // Loop until the tip is a block that exists in the main chain. + initialHeight := height + for !chain.MainChainHasBlock(hash) { + // At this point the index tip is orphaned, so load the orphaned block from the database directly and + // disconnect it from the index. The block has to be loaded directly since it is no longer in the main chain + // and thus the chain. BlockByHash function would error. + var b *block.Block + e = m.db.View( + func(dbTx database.Tx) (e error) { + blockBytes, e := dbTx.FetchBlock(hash) + if e != nil { + return e + } + b, e = block.NewFromBytes(blockBytes) + if e != nil { + return e + } + b.SetHeight(height) + return e + }, + ) + if e != nil { + return e + } + // We'll also grab the set of outputs spent by this block so we can remove them from the index. + var spentTxos []blockchain.SpentTxOut + spentTxos, e = chain.FetchSpendJournal(b) + if e != nil { + return e + } + // With the block and stxo set for that block retrieved, we can now update the index itself. + e = m.db.Update( + func(dbTx database.Tx) (e error) { + // Remove all of the index entries associated with the block and update the indexer tip. + e = dbIndexDisconnectBlock( + dbTx, indexer, b, spentTxos, + ) + if e != nil { + return e + } + // Update the tip to the previous block. + hash = &b.WireBlock().Header.PrevBlock + height-- + return nil + }, + ) + if e != nil { + return e + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + } + if initialHeight != height { + I.F( + "removed %d orphaned blocks from %s (heights %d to %d)", + initialHeight-height, + indexer.Name(), + height+1, + initialHeight, + ) + } + } + // Fetch the current tip heights for each index along with tracking the lowest one so the catchup code only needs to + // start at the earliest block and is able to skip connecting the block for the indexes that don't need it. + bestHeight := chain.BestSnapshot().Height + lowestHeight := bestHeight + indexerHeights := make([]int32, len(m.enabledIndexes)) + e = m.db.View( + func(dbTx database.Tx) (e error) { + for i, indexer := range m.enabledIndexes { + idxKey := indexer.Key() + _, height, e := dbFetchIndexerTip(dbTx, idxKey) + if e != nil { + return e + } + T.F( + "current %s tip (height %d, hash %v)", + indexer.Name(), + height, + hash, + ) + indexerHeights[i] = height + if height < lowestHeight { + lowestHeight = height + } + } + return nil + }, + ) + if e != nil { + return e + } + // Nothing to index if all of the indexes are caught up. + if lowestHeight == bestHeight { + return nil + } + // Create a progress logger for the indexing process below. + progressLogger := newBlockProgressLogger( + "Indexed", + ) + // At this point, one or more indexes are behind the current best chain tip and need to be caught up, so log the + // details and loop through each block that needs to be indexed. + I.F( + "catching up indexes from height %d to %d", + lowestHeight, + bestHeight, + ) + for height := lowestHeight + 1; height <= bestHeight; height++ { + // Load the block for the height since it is required to index it. + var blk *block.Block + blk, e = chain.BlockByHeight(height) + if e != nil { + return e + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Connect the block for all indexes that need it. + var spentTxos []blockchain.SpentTxOut + for i, indexer := range m.enabledIndexes { + // Skip indexes that don't need to be updated with this block. + if indexerHeights[i] >= height { + continue + } + // When the index requires all of the referenced txouts and they haven't been loaded yet, they need to be + // retrieved from the spend journal. + if spentTxos == nil && indexNeedsInputs(indexer) { + spentTxos, e = chain.FetchSpendJournal(blk) + if e != nil { + return e + } + } + e := m.db.Update( + func(dbTx database.Tx) (e error) { + return dbIndexConnectBlock( + dbTx, indexer, blk, spentTxos, + ) + }, + ) + if e != nil { + return e + } + indexerHeights[i] = height + } + // Log indexing progress. + progressLogger.LogBlockHeight(blk) + if interruptRequested(interrupt) { + return errInterruptRequested + } + } + I.Ln("indexes caught up to height", bestHeight) + return nil +} + +// indexNeedsInputs returns whether or not the index needs access to the txouts referenced by the transaction inputs +// being indexed. +func indexNeedsInputs(index Indexer) bool { + if idx, ok := index.(NeedsInputser); ok { + return idx.NeedsInputs() + } + return false +} + +// // dbFetchTx looks up the passed transaction hash in the transaction index +// and loads it from the database. +// func dbFetchTx(// dbTx database.Tx, hash *chainhash.Hash) (*wire.MsgTx, +// error) { +// // Look up the location of the transaction. +// blockRegion, e := dbFetchTxIndexEntry(dbTx, hash) +// if e != nil { +// DB// return nil, e +// } +// if blockRegion == nil { +// return nil, fmt.Errorf("transaction %v not found", hash) +// } +// // Load the raw transaction bytes from the database. +// txBytes, e := dbTx.FetchBlockRegion(blockRegion) +// if e != nil { +// DB// return nil, e +// } +// // Deserialize the transaction. +// var msgTx wire.MsgTx +// e = msgTx.Deserialize(bytes.NewReader(txBytes)) +// if e != nil { +// DB// return nil, e +// } +// return &msgTx, nil +// } + +// ConnectBlock must be invoked when a block is extending the main chain. It keeps track of the state of each index it +// is managing, performs some sanity checks, and invokes each indexer. This is part of the blockchain.IndexManager +// interface. +func (m *Manager) ConnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // Call each of the currently active optional indexes with the block being connected so they can update accordingly. + for _, index := range m.enabledIndexes { + e := dbIndexConnectBlock(dbTx, index, block, stxos) + if e != nil { + return e + } + } + return nil +} + +// DisconnectBlock must be invoked when a block is being disconnected from the end of the main chain. It keeps track of +// the state of each index it is managing, performs some sanity checks, and invokes each indexer to remove the index +// entries associated with the block. This is part of the blockchain.IndexManager interface. +func (m *Manager) DisconnectBlock( + dbTx database.Tx, block *block.Block, + stxo []blockchain.SpentTxOut, +) (e error) { + // Call each of the currently active optional indexes with the block being disconnected so they can update + // accordingly. + for _, index := range m.enabledIndexes { + e := dbIndexDisconnectBlock(dbTx, index, block, stxo) + if e != nil { + return e + } + } + return nil +} + +// NewManager returns a new index manager with the provided indexes enabled. The manager returned satisfies the +// blockchain. IndexManager interface and thus cleanly plugs into the normal blockchain processing path. +func NewManager(db database.DB, enabledIndexes []Indexer) *Manager { + return &Manager{ + db: db, + enabledIndexes: enabledIndexes, + } +} + +// dropIndex drops the passed index from the database. Since indexes can be massive, it deletes the index in multiple +// database transactions in order to keep memory usage to reasonable levels. It also marks the drop in progress so the +// drop can be resumed if it is stopped before it is done before the index can be used again. +func dropIndex(db database.DB, idxKey []byte, idxName string, interrupt <-chan struct{}) (e error) { + // Nothing to do if the index doesn't already exist. + var needsDelete bool + e = db.View( + func(dbTx database.Tx) (e error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + if indexesBucket != nil && indexesBucket.Get(idxKey) != nil { + needsDelete = true + } + return nil + }, + ) + if E.Chk(e) { + return + } + if !needsDelete { + W.F("not dropping %s because it does not exist", idxName) + return + } + // Mark that the index is in the process of being dropped so that it can be resumed on the next start if interrupted + // before the process is complete. + I.F("dropping all %s entries. This might take a while...", idxName) + e = db.Update( + func(dbTx database.Tx) (e error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + return indexesBucket.Put(indexDropKey(idxKey), idxKey) + }, + ) + if e != nil { + return e + } + // Since the indexes can be so large, attempting to simply delete the bucket in a single database transaction would + // result in massive memory usage and likely crash many systems due to ulimits. In order to avoid this, use a cursor + // to delete a maximum number of entries out of the bucket at a time. Recurse buckets depth-first to delete any + // sub-buckets. + const maxDeletions = 2000000 + var totalDeleted uint64 + // Recurse through all buckets in the index, cataloging each for later deletion. + var subBuckets [][][]byte + var subBucketClosure func(database.Tx, []byte, [][]byte) error + subBucketClosure = func( + dbTx database.Tx, + subBucket []byte, tlBucket [][]byte, + ) (e error) { + // Get full bucket name and append to subBuckets for later deletion. + var bucketName [][]byte + if (tlBucket == nil) || (len(tlBucket) == 0) { + bucketName = append(bucketName, subBucket) + } else { + bucketName = append(tlBucket, subBucket) + } + subBuckets = append(subBuckets, bucketName) + // Recurse sub-buckets to append to subBuckets slice. + bucket := dbTx.Metadata() + for _, subBucketName := range bucketName { + bucket = bucket.Bucket(subBucketName) + } + return bucket.ForEachBucket( + func(k []byte) (e error) { + return subBucketClosure(dbTx, k, bucketName) + }, + ) + } + // Call subBucketClosure with top-level bucket. + e = db.View( + func(dbTx database.Tx) (e error) { + return subBucketClosure(dbTx, idxKey, nil) + }, + ) + if e != nil { + return nil + } + // Iterate through each sub-bucket in reverse, deepest-first, deleting all keys inside them and then dropping the + // buckets themselves. + for i := range subBuckets { + bucketName := subBuckets[len(subBuckets)-1-i] + // Delete maxDeletions key/value pairs at a time. + for numDeleted := maxDeletions; numDeleted == maxDeletions; { + numDeleted = 0 + e = db.Update( + func(dbTx database.Tx) (e error) { + subBucket := dbTx.Metadata() + for _, subBucketName := range bucketName { + subBucket = subBucket.Bucket(subBucketName) + } + cursor := subBucket.Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() && + numDeleted < maxDeletions { + if e := cursor.Delete(); E.Chk(e) { + return e + } + numDeleted++ + } + return nil + }, + ) + if e != nil { + return e + } + if numDeleted > 0 { + totalDeleted += uint64(numDeleted) + I.F( + "deleted %d keys (%d total) from %s", numDeleted, + totalDeleted, idxName, + ) + } + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Drop the bucket itself. + e = db.Update( + func(dbTx database.Tx) (e error) { + bucket := dbTx.Metadata() + for j := 0; j < len(bucketName)-1; j++ { + bucket = bucket.Bucket(bucketName[j]) + } + return bucket.DeleteBucket(bucketName[len(bucketName)-1]) + }, + ) + if e != nil { + } + } + // Call extra index specific deinitialization for the transaction index. + if idxName == txIndexName { + if e = dropBlockIDIndex(db); E.Chk(e) { + return e + } + } + // Remove the index tip, index bucket, and in-progress drop flag now that all index entries have been removed. + e = db.Update( + func(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + indexesBucket := meta.Bucket(indexTipsBucketName) + if e := indexesBucket.Delete(idxKey); E.Chk(e) { + return e + } + return indexesBucket.Delete(indexDropKey(idxKey)) + }, + ) + if e != nil { + return e + } + I.Ln("dropped", idxName) + return nil +} diff --git a/pkg/indexers/index/txindex.go b/pkg/indexers/index/txindex.go new file mode 100644 index 0000000..5ec69b8 --- /dev/null +++ b/pkg/indexers/index/txindex.go @@ -0,0 +1,435 @@ +package index + +import ( + "errors" + "fmt" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // txIndexName is the human-readable name for the index. + txIndexName = "transaction index" +) + +var ( + // txIndexKey is the key of the transaction index and the db bucket used + // to house it. + txIndexKey = []byte("txbyhashidx") + // idByHashIndexBucketName is the name of the db bucket used to house the + // block id -> block hash index. + idByHashIndexBucketName = []byte("idbyhashidx") + // hashByIDIndexBucketName is the name of the db bucket used to house the + // block hash -> block id index. + hashByIDIndexBucketName = []byte("hashbyididx") + // errNoBlockIDEntry is an error that indicates a requested entry does + // not exist in the block ID index. + errNoBlockIDEntry = errors.New("no entry in the block ID index") +) + +// The transaction index consists of an entry for every transaction in the main chain. In order to significantly +// optimize the space requirements a separate index which provides an internal mapping between each block that has been +// indexed and a unique ID for use within the hash to location mappings. The ID is simply a sequentially incremented +// uint32. +// +// This is useful because it is only 4 bytes versus 32 bytes hashes and thus saves a ton of space in the index. There +// are three buckets used in total. +// +// The first bucket maps the hash of each transaction to the specific block location. The second bucket maps the hash of +// each block to the unique ID and the third maps that ID back to the block hash. +// +// NOTE: Although it is technically possible for multiple transactions to have the same hash as long as the previous +// transaction with the same hash is fully spent, this code only stores the most recent one because doing otherwise +// would add a non-trivial amount of space and overhead for something that will realistically never happen per the +// probability and even if it did, the old one must be fully spent and so the most likely transaction a caller would +// want for a given hash is the most recent one anyways. +// +// The serialized format for keys and values in the block hash to ID bucket is: +// +// = +// Field Type Size +// hash chainhash.Hash 32 bytes +// ID uint32 4 bytes +// ----- +// Total: 36 bytes +// +// The serialized format for keys and values in the ID to block hash bucket is: +// +// = +// Field Type Size +// ID uint32 4 bytes +// hash chainhash.Hash 32 bytes +// ----- +// Total: 36 bytes +// The serialized format for the keys and values in the tx index bucket is: +// = +// Field Type Size +// txhash chainhash.Hash 32 bytes +// block id uint32 4 bytes +// start offset uint32 4 bytes +// tx length uint32 4 bytes +// ----- +// Total: 44 bytes +// +// dbPutBlockIDIndexEntry uses an existing database transaction to update or add the index entries for the hash to id +// and id to hash mappings for the provided values. +func dbPutBlockIDIndexEntry(dbTx database.Tx, hash *chainhash.Hash, id uint32) (e error) { + // Serialize the height for use in the index entries. + var serializedID [4]byte + byteOrder.PutUint32(serializedID[:], id) + // Add the block hash to ID mapping to the index. + meta := dbTx.Metadata() + hashIndex := meta.Bucket(idByHashIndexBucketName) + if e := hashIndex.Put(hash[:], serializedID[:]); E.Chk(e) { + return e + } + // Add the block ID to hash mapping to the index. + idIndex := meta.Bucket(hashByIDIndexBucketName) + return idIndex.Put(serializedID[:], hash[:]) +} + +// dbRemoveBlockIDIndexEntry uses an existing database transaction remove index entries from the hash to id and id to +// hash mappings for the provided hash. +func dbRemoveBlockIDIndexEntry(dbTx database.Tx, hash *chainhash.Hash) (e error) { + // Remove the block hash to ID mapping. + meta := dbTx.Metadata() + hashIndex := meta.Bucket(idByHashIndexBucketName) + serializedID := hashIndex.Get(hash[:]) + if serializedID == nil { + return nil + } + if e := hashIndex.Delete(hash[:]); E.Chk(e) { + return e + } + // Remove the block ID to hash mapping. + idIndex := meta.Bucket(hashByIDIndexBucketName) + return idIndex.Delete(serializedID) +} + +// dbFetchBlockIDByHash uses an existing database transaction to retrieve the block id for the provided hash from the +// index. +func dbFetchBlockIDByHash(dbTx database.Tx, hash *chainhash.Hash) (uint32, error) { + hashIndex := dbTx.Metadata().Bucket(idByHashIndexBucketName) + serializedID := hashIndex.Get(hash[:]) + if serializedID == nil { + return 0, errNoBlockIDEntry + } + return byteOrder.Uint32(serializedID), nil +} + +// dbFetchBlockHashBySerializedID uses an existing database transaction to retrieve the hash for the provided serialized +// block id from the index. +func dbFetchBlockHashBySerializedID(dbTx database.Tx, serializedID []byte) (*chainhash.Hash, error) { + idIndex := dbTx.Metadata().Bucket(hashByIDIndexBucketName) + hashBytes := idIndex.Get(serializedID) + if hashBytes == nil { + return nil, errNoBlockIDEntry + } + var hash chainhash.Hash + copy(hash[:], hashBytes) + return &hash, nil +} + +// dbFetchBlockHashByID uses an existing database transaction to retrieve the hash for the provided block id from the +// index. +func dbFetchBlockHashByID(dbTx database.Tx, id uint32) (*chainhash.Hash, error) { + var serializedID [4]byte + byteOrder.PutUint32(serializedID[:], id) + return dbFetchBlockHashBySerializedID(dbTx, serializedID[:]) +} + +// putTxIndexEntry serializes the provided values according to the format described about for a transaction index entry. +// The target byte slice must be at least large enough to handle the number of bytes defined by the txEntrySize constant +// or it will panic. +func putTxIndexEntry(target []byte, blockID uint32, txLoc wire.TxLoc) { + byteOrder.PutUint32(target, blockID) + byteOrder.PutUint32(target[4:], uint32(txLoc.TxStart)) + byteOrder.PutUint32(target[8:], uint32(txLoc.TxLen)) +} + +// dbPutTxIndexEntry uses an existing database transaction to update the transaction index given the provided serialized +// data that is expected to have been serialized putTxIndexEntry. +func dbPutTxIndexEntry(dbTx database.Tx, txHash *chainhash.Hash, serializedData []byte) (e error) { + txIndex := dbTx.Metadata().Bucket(txIndexKey) + return txIndex.Put(txHash[:], serializedData) +} + +// dbFetchTxIndexEntry uses an existing database transaction to fetch the block region for the provided transaction hash +// from the transaction index. When there is no entry for the provided hash, nil will be returned for the both the +// region and the error. +func dbFetchTxIndexEntry(dbTx database.Tx, txHash *chainhash.Hash) (*database.BlockRegion, error) { + // Load the record from the database and return now if it doesn't exist. + txIndex := dbTx.Metadata().Bucket(txIndexKey) + serializedData := txIndex.Get(txHash[:]) + if len(serializedData) == 0 { + return nil, nil + } + // Ensure the serialized data has enough bytes to properly deserialize. + if len(serializedData) < 12 { + return nil, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "corrupt transaction index "+ + "entry for %s", txHash, + ), + } + } + // Load the block hash associated with the block ID. + hash, e := dbFetchBlockHashBySerializedID(dbTx, serializedData[0:4]) + if e != nil { + return nil, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "corrupt transaction index "+ + "entry for %s: %v", txHash, e, + ), + } + } + // Deserialize the final entry. + region := database.BlockRegion{Hash: &chainhash.Hash{}} + copy(region.Hash[:], hash[:]) + region.Offset = byteOrder.Uint32(serializedData[4:8]) + region.Len = byteOrder.Uint32(serializedData[8:12]) + return ®ion, nil +} + +// dbAddTxIndexEntries uses an existing database transaction to add a transaction index entry for every transaction in +// the passed block. +func dbAddTxIndexEntries(dbTx database.Tx, block *block.Block, blockID uint32) (e error) { + // The offset and length of the transactions within the serialized block. + txLocs, e := block.TxLoc() + if e != nil { + return e + } + // As an optimization, allocate a single slice big enough to hold all of the serialized transaction index entries + // for the block and serialize them directly into the slice. Then, pass the appropriate subslice to the database to + // be written. This approach significantly cuts down on the number of required allocations. + offset := 0 + serializedValues := make([]byte, len(block.Transactions())*txEntrySize) + for i, tx := range block.Transactions() { + putTxIndexEntry(serializedValues[offset:], blockID, txLocs[i]) + endOffset := offset + txEntrySize + e := dbPutTxIndexEntry( + dbTx, tx.Hash(), + serializedValues[offset:endOffset:endOffset], + ) + if e != nil { + return e + } + offset += txEntrySize + } + return nil +} + +// dbRemoveTxIndexEntry uses an existing database transaction to remove the most recent transaction index entry for the +// given hash. +func dbRemoveTxIndexEntry(dbTx database.Tx, txHash *chainhash.Hash) (e error) { + txIndex := dbTx.Metadata().Bucket(txIndexKey) + serializedData := txIndex.Get(txHash[:]) + if len(serializedData) == 0 { + return fmt.Errorf( + "can't remove non-existent transaction %s "+ + "from the transaction index", txHash, + ) + } + return txIndex.Delete(txHash[:]) +} + +// dbRemoveTxIndexEntries uses an existing database transaction to remove the latest transaction entry for every +// transaction in the passed block. +func dbRemoveTxIndexEntries(dbTx database.Tx, block *block.Block) (e error) { + for _, tx := range block.Transactions() { + e := dbRemoveTxIndexEntry(dbTx, tx.Hash()) + if e != nil { + return e + } + } + return nil +} + +// TxIndex implements a transaction by hash index. That is to say, it supports querying all transactions by their hash. +type TxIndex struct { + db database.DB + curBlockID uint32 +} + +// Ensure the TxIndex type implements the Indexer interface. +var _ Indexer = (*TxIndex)(nil) + +// Init initializes the hash-based transaction index. In particular, it finds the highest used block ID and stores it +// for later use when connecting or disconnecting blocks. This is part of the Indexer interface. +func (idx *TxIndex) Init() (e error) { + // Find the latest known block id field for the internal block id index and initialize it. This is done because it's + // a lot more efficient to do a single search at initialize time than it is to write another value to the database + // on every update. + e = idx.db.View( + func(dbTx database.Tx) (e error) { + // Scan forward in large gaps to find a block id that doesn't exist yet to serve as an upper bound for the + // binary search below. + var highestKnown, nextUnknown uint32 + testBlockID := uint32(1) + increment := uint32(100000) + for { + _, e := dbFetchBlockHashByID(dbTx, testBlockID) + if e != nil { + // F.Ln(err) + nextUnknown = testBlockID + break + } + highestKnown = testBlockID + testBlockID += increment + } + T.F("forward scan (highest known %d, next unknown %d)", highestKnown, nextUnknown) + // No used block IDs due to new database. + if nextUnknown == 1 { + return nil + } + // Use a binary search to find the final highest used block id. This will take at most ceil(log_2(increment)) + // attempts. + for { + testBlockID = (highestKnown + nextUnknown) / 2 + _, e := dbFetchBlockHashByID(dbTx, testBlockID) + if e != nil { + // F.Ln(err) + nextUnknown = testBlockID + } else { + highestKnown = testBlockID + } + T.F("binary scan (highest known %d, next unknown %d)", highestKnown, nextUnknown) + if highestKnown+1 == nextUnknown { + break + } + } + idx.curBlockID = highestKnown + return nil + }, + ) + if e != nil { + return e + } + F.Ln("current internal block ID:", idx.curBlockID) + return nil +} + +// Key returns the database key to use for the index as a byte slice. This is part of the Indexer interface. +func (idx *TxIndex) Key() []byte { + return txIndexKey +} + +// Name returns the human-readable name of the index. This is part of the Indexer interface. +func (idx *TxIndex) Name() string { + return txIndexName +} + +// Create is invoked when the indexer manager determines the index needs to be created for the first time. It creates +// the buckets for the hash-based transaction index and the internal block ID indexes. This is part of the Indexer +// interface. +func (idx *TxIndex) Create(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + if _, e = meta.CreateBucket(idByHashIndexBucketName); E.Chk(e) { + return e + } + if _, e = meta.CreateBucket(hashByIDIndexBucketName); E.Chk(e) { + return e + } + _, e = meta.CreateBucket(txIndexKey) + return e +} + +// ConnectBlock is invoked by the index manager when a new block has been connected to the main chain. This indexer adds +// a hash-to-transaction mapping for every transaction in the passed block. This is part of the Indexer interface. +func (idx *TxIndex) ConnectBlock(dbTx database.Tx, block *block.Block, stxos []blockchain.SpentTxOut) (e error) { + // Increment the internal block ID to use for the block being connected and add all of the transactions in the block + // to the index. + newBlockID := idx.curBlockID + 1 + if e = dbAddTxIndexEntries(dbTx, block, newBlockID); E.Chk(e) { + return e + } + // Add the new block ID index entry for the block being connected and update the current internal block ID + // accordingly. + e = dbPutBlockIDIndexEntry(dbTx, block.Hash(), newBlockID) + if e != nil { + return e + } + idx.curBlockID = newBlockID + return nil +} + +// DisconnectBlock is invoked by the index manager when a block has been disconnected from the main chain. +// +// This indexer removes the hash-to-transaction mapping for every transaction in the block. +// +// This is part of the Indexer interface. +func (idx *TxIndex) DisconnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // Remove all of the transactions in the block from the index. + if e = dbRemoveTxIndexEntries(dbTx, block); E.Chk(e) { + return e + } + // Remove the block ID index entry for the block being disconnected and decrement the current internal block ID to + // account for it. + if e = dbRemoveBlockIDIndexEntry(dbTx, block.Hash()); E.Chk(e) { + return e + } + idx.curBlockID-- + return nil +} + +// TxBlockRegion returns the block region for the provided transaction hash from the transaction index. +// +// The block region can in turn be used to load the raw transaction bytes. +// +// When there is no entry for the provided hash, nil will be returned for the both the entry and the error. +// +// This function is safe for concurrent access. +func (idx *TxIndex) TxBlockRegion(hash *chainhash.Hash) (region *database.BlockRegion, e error) { + e = idx.db.View( + func(dbTx database.Tx) (e error) { + region, e = dbFetchTxIndexEntry(dbTx, hash) + return e + }, + ) + return region, e +} + +// NewTxIndex returns a new instance of an indexer that is used to create a mapping of the hashes of all transactions in +// the blockchain to the respective block, location within the block, and size of the transaction. +// +// It implements the Indexer interface which plugs into the IndexManager that in turn is used by the blockchain package. +// +// This allows the index to be seamlessly maintained along with the chain. +func NewTxIndex(db database.DB) *TxIndex { + return &TxIndex{db: db} +} + +// dropBlockIDIndex drops the internal block id index. +func dropBlockIDIndex(db database.DB) (e error) { + return db.Update( + func(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + e = meta.DeleteBucket(idByHashIndexBucketName) + if e != nil { + return e + } + return meta.DeleteBucket(hashByIDIndexBucketName) + }, + ) +} + +// DropTxIndex drops the transaction index from the provided database if it exists. Since the address index relies on +// it, the address index will also be dropped when it exists. +func DropTxIndex(db database.DB, interrupt qu.C) (e error) { + e = dropIndex(db, addrIndexKey, addrIndexName, interrupt) + if e != nil { + return e + } + return dropIndex(db, txIndexKey, txIndexName, interrupt) +} diff --git a/pkg/indexers/log.go b/pkg/indexers/log.go new file mode 100644 index 0000000..7e04ebc --- /dev/null +++ b/pkg/indexers/log.go @@ -0,0 +1,43 @@ +package indexers + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/indexers/manager.go b/pkg/indexers/manager.go new file mode 100644 index 0000000..8598c2b --- /dev/null +++ b/pkg/indexers/manager.go @@ -0,0 +1,647 @@ +package indexers + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" +) + +var ( + // indexTipsBucketName is the name of the db bucket used to house the current tip of each index. + indexTipsBucketName = []byte("idxtips") +) + +// The index manager tracks the current tip of each index by using a parent bucket that contains an entry for index. +// +// The serialized format for an index tip is: +// +// [],... +// Field Type Size +// block hash chainhash.Hash chainhash.HashSize +// block height uint32 4 bytes + +// dbPutIndexerTip uses an existing database transaction to update or add the current tip for the given index to the +// provided values. +func dbPutIndexerTip(dbTx database.Tx, idxKey []byte, hash *chainhash.Hash, height int32) (e error) { + serialized := make([]byte, chainhash.HashSize+4) + copy(serialized, hash[:]) + byteOrder.PutUint32(serialized[chainhash.HashSize:], uint32(height)) + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + return indexesBucket.Put(idxKey, serialized) +} + +// dbFetchIndexerTip uses an existing database transaction to retrieve the hash and height of the current tip for the +// provided index. +func dbFetchIndexerTip(dbTx database.Tx, idxKey []byte) (*chainhash.Hash, int32, error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + serialized := indexesBucket.Get(idxKey) + if len(serialized) < chainhash.HashSize+4 { + return nil, 0, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "unexpected end of data for "+ + "index %q tip", string(idxKey), + ), + } + } + var hash chainhash.Hash + copy(hash[:], serialized[:chainhash.HashSize]) + height := int32(byteOrder.Uint32(serialized[chainhash.HashSize:])) + return &hash, height, nil +} + +// dbIndexConnectBlock adds all of the index entries associated with the given block using the provided indexer and +// updates the tip of the indexer accordingly. An error will be returned if the current tip for the indexer is not the +// previous block for the passed block. +func dbIndexConnectBlock( + dbTx database.Tx, indexer Indexer, block *block.Block, + stxo []blockchain.SpentTxOut, +) (e error) { + // Assert that the block being connected properly connects to the current tip of the index. + idxKey := indexer.Key() + var curTipHash *chainhash.Hash + if curTipHash, _, e = dbFetchIndexerTip(dbTx, idxKey); E.Chk(e) { + return e + } + if !curTipHash.IsEqual(&block.WireBlock().Header.PrevBlock) { + return AssertError( + fmt.Sprintf( + "dbIndexConnectBlock must be "+ + "called with a block that extends the current index "+ + "tip (%s, tip %s, block %s)", indexer.Name(), + curTipHash, block.Hash(), + ), + ) + } + // Notify the indexer with the connected block so it can index it. + if e := indexer.ConnectBlock(dbTx, block, stxo); E.Chk(e) { + return e + } + // Update the current index tip. + return dbPutIndexerTip(dbTx, idxKey, block.Hash(), block.Height()) +} + +// dbIndexDisconnectBlock removes all of the index entries associated with the given block using the provided indexer +// and updates the tip of the indexer accordingly. An error will be returned if the current tip for the indexer is not +// the passed block. +func dbIndexDisconnectBlock( + dbTx database.Tx, indexer Indexer, block *block.Block, + stxo []blockchain.SpentTxOut, +) (e error) { + // Assert that the block being disconnected is the current tip of the index. + idxKey := indexer.Key() + var curTipHash *chainhash.Hash + if curTipHash, _, e = dbFetchIndexerTip(dbTx, idxKey); E.Chk(e) { + return e + } + if !curTipHash.IsEqual(block.Hash()) { + return AssertError( + fmt.Sprintf( + "dbIndexDisconnectBlock must "+ + "be called with the block at the current index tip "+ + "(%s, tip %s, block %s)", indexer.Name(), + curTipHash, block.Hash(), + ), + ) + } + // Notify the indexer with the disconnected block so it can remove all of + // the appropriate entries. + if e := indexer.DisconnectBlock(dbTx, block, stxo); E.Chk(e) { + return e + } + // Update the current index tip. + prevHash := &block.WireBlock().Header.PrevBlock + return dbPutIndexerTip(dbTx, idxKey, prevHash, block.Height()-1) +} + +// Manager defines an index manager that manages multiple optional indexes and implements the blockchain. IndexManager +// interface so it can be seamlessly plugged into normal chain processing. +type Manager struct { + db database.DB + enabledIndexes []Indexer +} + +// Ensure the Manager type implements the blockchain.IndexManager interface. +var _ blockchain.IndexManager = (*Manager)(nil) + +// indexDropKey returns the key for an index which indicates it is in the process of being dropped. +func indexDropKey(idxKey []byte) []byte { + dropKey := make([]byte, len(idxKey)+1) + dropKey[0] = 'd' + copy(dropKey[1:], idxKey) + return dropKey +} + +// maybeFinishDrops determines if each of the enabled indexes are in the middle of being dropped and finishes dropping +// them when the are. This is necessary because dropping and index has to be done in several atomic steps rather than +// one big atomic step due to the massive number of entries. +func (m *Manager) maybeFinishDrops(interrupt <-chan struct{}) (e error) { + indexNeedsDrop := make([]bool, len(m.enabledIndexes)) + if e = m.db.View( + func(dbTx database.Tx) (e error) { + // None of the indexes needs to be dropped if the index tips bucket hasn't been created yet. + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + if indexesBucket == nil { + return nil + } + // Mark the indexer as requiring a drop if one is already in progress. + for i, indexer := range m.enabledIndexes { + dropKey := indexDropKey(indexer.Key()) + if indexesBucket.Get(dropKey) != nil { + indexNeedsDrop[i] = true + } + } + return nil + }, + ); E.Chk(e) { + return e + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Finish dropping any of the enabled indexes that are already in the + // middle of being dropped. + for i, indexer := range m.enabledIndexes { + if !indexNeedsDrop[i] { + continue + } + I.C( + func() string { + return fmt.Sprintf("Resuming %s drop", indexer.Name()) + }, + ) + if e = dropIndex(m.db, indexer.Key(), indexer.Name(), interrupt); E.Chk(e) { + return e + } + } + return nil +} + +// maybeCreateIndexes determines if each of the enabled indexes have already been created and creates them if not. +func (m *Manager) maybeCreateIndexes(dbTx database.Tx) (e error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + for _, indexer := range m.enabledIndexes { + // Nothing to do if the index tip already exists. + idxKey := indexer.Key() + if indexesBucket.Get(idxKey) != nil { + continue + } + // The tip for the index does not exist, so create it and invoke the create callback for the index so it can + // perform any one-time initialization it requires. + if e := indexer.Create(dbTx); E.Chk(e) { + return e + } + // Set the tip for the index to values which represent an uninitialized index. + e := dbPutIndexerTip(dbTx, idxKey, &chainhash.Hash{}, -1) + if e != nil { + return e + } + } + return nil +} + +// Init initializes the enabled indexes. This is called during chain initialization and primarily consists of catching +// up all indexes to the current best chain tip. This is necessary since each index can be disabled and re-enabled at +// any time and attempting to catch-up indexes at the same time new blocks are being downloaded would lead to an overall +// longer time to catch up due to the I/O contention. This is part of the blockchain.IndexManager interface. +func (m *Manager) Init(chain *blockchain.BlockChain, interrupt <-chan struct{}) (e error) { + // Nothing to do when no indexes are enabled. + if len(m.enabledIndexes) == 0 { + return nil + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Finish and drops that were previously interrupted. + if e = m.maybeFinishDrops(interrupt); E.Chk(e) { + return e + } + // Create the initial state for the indexes as needed. + e = m.db.Update( + func(dbTx database.Tx) (e error) { + // Create the bucket for the current tips as needed. + meta := dbTx.Metadata() + if _, e = meta.CreateBucketIfNotExists(indexTipsBucketName); E.Chk(e) { + return e + } + return m.maybeCreateIndexes(dbTx) + }, + ) + if E.Chk(e) { + return e + } + // Initialize each of the enabled indexes. + for _, indexer := range m.enabledIndexes { + if e = indexer.Init(); E.Chk(e) { + return e + } + } + // Rollback indexes to the main chain if their tip is an orphaned fork. This is fairly unlikely, but it can happen + // if the chain is reorganized while the index is disabled. This has to be done in reverse order because later + // indexes can depend on earlier ones. + var height int32 + var hash *chainhash.Hash + for i := len(m.enabledIndexes); i > 0; i-- { + indexer := m.enabledIndexes[i-1] + // Fetch the current tip for the index. + e = m.db.View( + func(dbTx database.Tx) (e error) { + idxKey := indexer.Key() + hash, height, e = dbFetchIndexerTip(dbTx, idxKey) + return e + }, + ) + if e != nil { + return e + } + // Nothing to do if the index does not have any entries yet. + if height == -1 { + continue + } + // Loop until the tip is a block that exists in the main chain. + initialHeight := height + for !chain.MainChainHasBlock(hash) { + // At this point the index tip is orphaned, so load the orphaned block from the database directly and + // disconnect it from the index. The block has to be loaded directly since it is no longer in the main chain + // and thus the chain. BlockByHash function would error. + var blk *block.Block + e = m.db.View( + func(dbTx database.Tx) (e error) { + var blockBytes []byte + blockBytes, e = dbTx.FetchBlock(hash) + if e != nil { + return e + } + blk, e = block.NewFromBytes(blockBytes) + if e != nil { + return e + } + blk.SetHeight(height) + return e + }, + ) + if e != nil { + return e + } + // We'll also grab the set of outputs spent by this block so we can remove them from the index. + var spentTxos []blockchain.SpentTxOut + spentTxos, e = chain.FetchSpendJournal(blk) + if e != nil { + return e + } + // With the block and stxo set for that block retrieved, we can now update the index itself. + e = m.db.Update( + func(dbTx database.Tx) (e error) { + // Remove all of the index entries associated with the block and update the indexer tip. + e = dbIndexDisconnectBlock( + dbTx, indexer, blk, spentTxos, + ) + if e != nil { + return e + } + // Update the tip to the previous block. + hash = &blk.WireBlock().Header.PrevBlock + height-- + return nil + }, + ) + if e != nil { + return e + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + } + if initialHeight != height { + I.F( + "removed %d orphaned blocks from %s (heights %d to %d)", + initialHeight-height, + indexer.Name(), + height+1, + initialHeight, + ) + } + } + // Fetch the current tip heights for each index along with tracking the lowest one so the catchup code only needs to + // start at the earliest block and is able to skip connecting the block for the indexes that don't need it. + bestHeight := chain.BestSnapshot().Height + lowestHeight := bestHeight + indexerHeights := make([]int32, len(m.enabledIndexes)) + e = m.db.View( + func(dbTx database.Tx) (e error) { + for i, indexer := range m.enabledIndexes { + idxKey := indexer.Key() + _, height, e := dbFetchIndexerTip(dbTx, idxKey) + if e != nil { + return e + } + T.F( + "current %s tip (height %d, hash %v)", + indexer.Name(), + height, + hash, + ) + indexerHeights[i] = height + if height < lowestHeight { + lowestHeight = height + } + } + return nil + }, + ) + if e != nil { + return e + } + // Nothing to index if all of the indexes are caught up. + if lowestHeight == bestHeight { + return nil + } + // Create a progress logger for the indexing process below. + progressLogger := newBlockProgressLogger( + "Indexed", + // log.L, + ) + // At this point, one or more indexes are behind the current best chain tip and need to be caught up, so log the + // details and loop through each block that needs to be indexed. + I.F( + "catching up indexes from height %d to %d", + lowestHeight, + bestHeight, + ) + for height := lowestHeight + 1; height <= bestHeight; height++ { + // Load the block for the height since it is required to index it. + block, e := chain.BlockByHeight(height) + if e != nil { + return e + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Connect the block for all indexes that need it. + var spentTxos []blockchain.SpentTxOut + for i, indexer := range m.enabledIndexes { + // Skip indexes that don't need to be updated with this block. + if indexerHeights[i] >= height { + continue + } + // When the index requires all of the referenced txouts and they haven't been loaded yet, they need to be + // retrieved from the spend journal. + if spentTxos == nil && indexNeedsInputs(indexer) { + spentTxos, e = chain.FetchSpendJournal(block) + if e != nil { + return e + } + } + e := m.db.Update( + func(dbTx database.Tx) (e error) { + return dbIndexConnectBlock( + dbTx, indexer, block, spentTxos, + ) + }, + ) + if e != nil { + return e + } + indexerHeights[i] = height + } + // Log indexing progress. + progressLogger.LogBlockHeight(block) + if interruptRequested(interrupt) { + return errInterruptRequested + } + } + I.Ln("indexes caught up to height", bestHeight) + return nil +} + +// indexNeedsInputs returns whether or not the index needs access to the txouts referenced by the transaction inputs +// being indexed. +func indexNeedsInputs(index Indexer) bool { + if idx, ok := index.(NeedsInputser); ok { + return idx.NeedsInputs() + } + return false +} + +// // dbFetchTx looks up the passed transaction hash in the transaction index +// and loads it from the database. +// func dbFetchTx(// dbTx database.Tx, hash *chainhash.Hash) (*wire.MsgTx, +// error) { +// // Look up the location of the transaction. +// blockRegion, e := dbFetchTxIndexEntry(dbTx, hash) +// if e != nil { +// DB// return nil, e +// } +// if blockRegion == nil { +// return nil, fmt.Errorf("transaction %v not found", hash) +// } +// // Load the raw transaction bytes from the database. +// txBytes, e := dbTx.FetchBlockRegion(blockRegion) +// if e != nil { +// DB// return nil, e +// } +// // Deserialize the transaction. +// var msgTx wire.MsgTx +// e = msgTx.Deserialize(bytes.NewReader(txBytes)) +// if e != nil { +// DB// return nil, e +// } +// return &msgTx, nil +// } + +// ConnectBlock must be invoked when a block is extending the main chain. It keeps track of the state of each index it +// is managing, performs some sanity checks, and invokes each indexer. This is part of the blockchain.IndexManager +// interface. +func (m *Manager) ConnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // Call each of the currently active optional indexes with the block being connected so they can update accordingly. + for _, index := range m.enabledIndexes { + e := dbIndexConnectBlock(dbTx, index, block, stxos) + if e != nil { + return e + } + } + return nil +} + +// DisconnectBlock must be invoked when a block is being disconnected from the end of the main chain. It keeps track of +// the state of each index it is managing, performs some sanity checks, and invokes each indexer to remove the index +// entries associated with the block. This is part of the blockchain.IndexManager interface. +func (m *Manager) DisconnectBlock( + dbTx database.Tx, block *block.Block, + stxo []blockchain.SpentTxOut, +) (e error) { + // Call each of the currently active optional indexes with the block being disconnected so they can update + // accordingly. + for _, index := range m.enabledIndexes { + e := dbIndexDisconnectBlock(dbTx, index, block, stxo) + if e != nil { + return e + } + } + return nil +} + +// NewManager returns a new index manager with the provided indexes enabled. The manager returned satisfies the +// blockchain. IndexManager interface and thus cleanly plugs into the normal blockchain processing path. +func NewManager(db database.DB, enabledIndexes []Indexer) *Manager { + return &Manager{ + db: db, + enabledIndexes: enabledIndexes, + } +} + +// dropIndex drops the passed index from the database. Since indexes can be massive, it deletes the index in multiple +// database transactions in order to keep memory usage to reasonable levels. It also marks the drop in progress so the +// drop can be resumed if it is stopped before it is done before the index can be used again. +func dropIndex(db database.DB, idxKey []byte, idxName string, interrupt <-chan struct{}) (e error) { + // Nothing to do if the index doesn't already exist. + var needsDelete bool + e = db.View( + func(dbTx database.Tx) (e error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + if indexesBucket != nil && indexesBucket.Get(idxKey) != nil { + needsDelete = true + } + return nil + }, + ) + if E.Chk(e) { + return + } + if !needsDelete { + W.F("not dropping %s because it does not exist", idxName) + return + } + // Mark that the index is in the process of being dropped so that it can be resumed on the next start if interrupted + // before the process is complete. + I.F("dropping all %s entries. This might take a while...", idxName) + e = db.Update( + func(dbTx database.Tx) (e error) { + indexesBucket := dbTx.Metadata().Bucket(indexTipsBucketName) + return indexesBucket.Put(indexDropKey(idxKey), idxKey) + }, + ) + if e != nil { + return e + } + // Since the indexes can be so large, attempting to simply delete the bucket in a single database transaction would + // result in massive memory usage and likely crash many systems due to ulimits. In order to avoid this, use a cursor + // to delete a maximum number of entries out of the bucket at a time. Recurse buckets depth-first to delete any + // sub-buckets. + const maxDeletions = 2000000 + var totalDeleted uint64 + // Recurse through all buckets in the index, cataloging each for later deletion. + var subBuckets [][][]byte + var subBucketClosure func(database.Tx, []byte, [][]byte) error + subBucketClosure = func( + dbTx database.Tx, + subBucket []byte, tlBucket [][]byte, + ) (e error) { + // Get full bucket name and append to subBuckets for later deletion. + var bucketName [][]byte + if (tlBucket == nil) || (len(tlBucket) == 0) { + bucketName = append(bucketName, subBucket) + } else { + bucketName = append(tlBucket, subBucket) + } + subBuckets = append(subBuckets, bucketName) + // Recurse sub-buckets to append to subBuckets slice. + bucket := dbTx.Metadata() + for _, subBucketName := range bucketName { + bucket = bucket.Bucket(subBucketName) + } + return bucket.ForEachBucket( + func(k []byte) (e error) { + return subBucketClosure(dbTx, k, bucketName) + }, + ) + } + // Call subBucketClosure with top-level bucket. + e = db.View( + func(dbTx database.Tx) (e error) { + return subBucketClosure(dbTx, idxKey, nil) + }, + ) + if e != nil { + return nil + } + // Iterate through each sub-bucket in reverse, deepest-first, deleting all keys inside them and then dropping the + // buckets themselves. + for i := range subBuckets { + bucketName := subBuckets[len(subBuckets)-1-i] + // Delete maxDeletions key/value pairs at a time. + for numDeleted := maxDeletions; numDeleted == maxDeletions; { + numDeleted = 0 + e = db.Update( + func(dbTx database.Tx) (e error) { + subBucket := dbTx.Metadata() + for _, subBucketName := range bucketName { + subBucket = subBucket.Bucket(subBucketName) + } + cursor := subBucket.Cursor() + for ok := cursor.First(); ok; ok = cursor.Next() && + numDeleted < maxDeletions { + if e := cursor.Delete(); E.Chk(e) { + return e + } + numDeleted++ + } + return nil + }, + ) + if e != nil { + return e + } + if numDeleted > 0 { + totalDeleted += uint64(numDeleted) + I.F( + "deleted %d keys (%d total) from %s", numDeleted, + totalDeleted, idxName, + ) + } + } + if interruptRequested(interrupt) { + return errInterruptRequested + } + // Drop the bucket itself. + e = db.Update( + func(dbTx database.Tx) (e error) { + bucket := dbTx.Metadata() + for j := 0; j < len(bucketName)-1; j++ { + bucket = bucket.Bucket(bucketName[j]) + } + return bucket.DeleteBucket(bucketName[len(bucketName)-1]) + }, + ) + if e != nil { + } + } + // Call extra index specific deinitialization for the transaction index. + if idxName == txIndexName { + if e = dropBlockIDIndex(db); E.Chk(e) { + return e + } + } + // Remove the index tip, index bucket, and in-progress drop flag now that all index entries have been removed. + e = db.Update( + func(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + indexesBucket := meta.Bucket(indexTipsBucketName) + if e := indexesBucket.Delete(idxKey); E.Chk(e) { + return e + } + return indexesBucket.Delete(indexDropKey(idxKey)) + }, + ) + if e != nil { + return e + } + I.Ln("dropped", idxName) + return nil +} diff --git a/pkg/indexers/txindex.go b/pkg/indexers/txindex.go new file mode 100644 index 0000000..e4ecdad --- /dev/null +++ b/pkg/indexers/txindex.go @@ -0,0 +1,435 @@ +package indexers + +import ( + "errors" + "fmt" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // txIndexName is the human-readable name for the index. + txIndexName = "transaction index" +) + +var ( + // txIndexKey is the key of the transaction index and the db bucket used + // to house it. + txIndexKey = []byte("txbyhashidx") + // idByHashIndexBucketName is the name of the db bucket used to house the + // block id -> block hash index. + idByHashIndexBucketName = []byte("idbyhashidx") + // hashByIDIndexBucketName is the name of the db bucket used to house the + // block hash -> block id index. + hashByIDIndexBucketName = []byte("hashbyididx") + // errNoBlockIDEntry is an error that indicates a requested entry does + // not exist in the block ID index. + errNoBlockIDEntry = errors.New("no entry in the block ID index") +) + +// The transaction index consists of an entry for every transaction in the main chain. In order to significantly +// optimize the space requirements a separate index which provides an internal mapping between each block that has been +// indexed and a unique ID for use within the hash to location mappings. The ID is simply a sequentially incremented +// uint32. +// +// This is useful because it is only 4 bytes versus 32 bytes hashes and thus saves a ton of space in the index. There +// are three buckets used in total. +// +// The first bucket maps the hash of each transaction to the specific block location. The second bucket maps the hash of +// each block to the unique ID and the third maps that ID back to the block hash. +// +// NOTE: Although it is technically possible for multiple transactions to have the same hash as long as the previous +// transaction with the same hash is fully spent, this code only stores the most recent one because doing otherwise +// would add a non-trivial amount of space and overhead for something that will realistically never happen per the +// probability and even if it did, the old one must be fully spent and so the most likely transaction a caller would +// want for a given hash is the most recent one anyways. +// +// The serialized format for keys and values in the block hash to ID bucket is: +// +// = +// Field Type Size +// hash chainhash.Hash 32 bytes +// ID uint32 4 bytes +// ----- +// Total: 36 bytes +// +// The serialized format for keys and values in the ID to block hash bucket is: +// +// = +// Field Type Size +// ID uint32 4 bytes +// hash chainhash.Hash 32 bytes +// ----- +// Total: 36 bytes +// The serialized format for the keys and values in the tx index bucket is: +// = +// Field Type Size +// txhash chainhash.Hash 32 bytes +// block id uint32 4 bytes +// start offset uint32 4 bytes +// tx length uint32 4 bytes +// ----- +// Total: 44 bytes +// +// dbPutBlockIDIndexEntry uses an existing database transaction to update or add the index entries for the hash to id +// and id to hash mappings for the provided values. +func dbPutBlockIDIndexEntry(dbTx database.Tx, hash *chainhash.Hash, id uint32) (e error) { + // Serialize the height for use in the index entries. + var serializedID [4]byte + byteOrder.PutUint32(serializedID[:], id) + // Add the block hash to ID mapping to the index. + meta := dbTx.Metadata() + hashIndex := meta.Bucket(idByHashIndexBucketName) + if e := hashIndex.Put(hash[:], serializedID[:]); E.Chk(e) { + return e + } + // Add the block ID to hash mapping to the index. + idIndex := meta.Bucket(hashByIDIndexBucketName) + return idIndex.Put(serializedID[:], hash[:]) +} + +// dbRemoveBlockIDIndexEntry uses an existing database transaction remove index entries from the hash to id and id to +// hash mappings for the provided hash. +func dbRemoveBlockIDIndexEntry(dbTx database.Tx, hash *chainhash.Hash) (e error) { + // Remove the block hash to ID mapping. + meta := dbTx.Metadata() + hashIndex := meta.Bucket(idByHashIndexBucketName) + serializedID := hashIndex.Get(hash[:]) + if serializedID == nil { + return nil + } + if e := hashIndex.Delete(hash[:]); E.Chk(e) { + return e + } + // Remove the block ID to hash mapping. + idIndex := meta.Bucket(hashByIDIndexBucketName) + return idIndex.Delete(serializedID) +} + +// dbFetchBlockIDByHash uses an existing database transaction to retrieve the block id for the provided hash from the +// index. +func dbFetchBlockIDByHash(dbTx database.Tx, hash *chainhash.Hash) (uint32, error) { + hashIndex := dbTx.Metadata().Bucket(idByHashIndexBucketName) + serializedID := hashIndex.Get(hash[:]) + if serializedID == nil { + return 0, errNoBlockIDEntry + } + return byteOrder.Uint32(serializedID), nil +} + +// dbFetchBlockHashBySerializedID uses an existing database transaction to retrieve the hash for the provided serialized +// block id from the index. +func dbFetchBlockHashBySerializedID(dbTx database.Tx, serializedID []byte) (*chainhash.Hash, error) { + idIndex := dbTx.Metadata().Bucket(hashByIDIndexBucketName) + hashBytes := idIndex.Get(serializedID) + if hashBytes == nil { + return nil, errNoBlockIDEntry + } + var hash chainhash.Hash + copy(hash[:], hashBytes) + return &hash, nil +} + +// dbFetchBlockHashByID uses an existing database transaction to retrieve the hash for the provided block id from the +// index. +func dbFetchBlockHashByID(dbTx database.Tx, id uint32) (*chainhash.Hash, error) { + var serializedID [4]byte + byteOrder.PutUint32(serializedID[:], id) + return dbFetchBlockHashBySerializedID(dbTx, serializedID[:]) +} + +// putTxIndexEntry serializes the provided values according to the format described about for a transaction index entry. +// The target byte slice must be at least large enough to handle the number of bytes defined by the txEntrySize constant +// or it will panic. +func putTxIndexEntry(target []byte, blockID uint32, txLoc wire.TxLoc) { + byteOrder.PutUint32(target, blockID) + byteOrder.PutUint32(target[4:], uint32(txLoc.TxStart)) + byteOrder.PutUint32(target[8:], uint32(txLoc.TxLen)) +} + +// dbPutTxIndexEntry uses an existing database transaction to update the transaction index given the provided serialized +// data that is expected to have been serialized putTxIndexEntry. +func dbPutTxIndexEntry(dbTx database.Tx, txHash *chainhash.Hash, serializedData []byte) (e error) { + txIndex := dbTx.Metadata().Bucket(txIndexKey) + return txIndex.Put(txHash[:], serializedData) +} + +// dbFetchTxIndexEntry uses an existing database transaction to fetch the block region for the provided transaction hash +// from the transaction index. When there is no entry for the provided hash, nil will be returned for the both the +// region and the error. +func dbFetchTxIndexEntry(dbTx database.Tx, txHash *chainhash.Hash) (*database.BlockRegion, error) { + // Load the record from the database and return now if it doesn't exist. + txIndex := dbTx.Metadata().Bucket(txIndexKey) + serializedData := txIndex.Get(txHash[:]) + if len(serializedData) == 0 { + return nil, nil + } + // Ensure the serialized data has enough bytes to properly deserialize. + if len(serializedData) < 12 { + return nil, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "corrupt transaction index "+ + "entry for %s", txHash, + ), + } + } + // Load the block hash associated with the block ID. + hash, e := dbFetchBlockHashBySerializedID(dbTx, serializedData[0:4]) + if e != nil { + return nil, database.DBError{ + ErrorCode: database.ErrCorruption, + Description: fmt.Sprintf( + "corrupt transaction index "+ + "entry for %s: %v", txHash, e, + ), + } + } + // Deserialize the final entry. + region := database.BlockRegion{Hash: &chainhash.Hash{}} + copy(region.Hash[:], hash[:]) + region.Offset = byteOrder.Uint32(serializedData[4:8]) + region.Len = byteOrder.Uint32(serializedData[8:12]) + return ®ion, nil +} + +// dbAddTxIndexEntries uses an existing database transaction to add a transaction index entry for every transaction in +// the passed block. +func dbAddTxIndexEntries(dbTx database.Tx, block *block.Block, blockID uint32) (e error) { + // The offset and length of the transactions within the serialized block. + txLocs, e := block.TxLoc() + if e != nil { + return e + } + // As an optimization, allocate a single slice big enough to hold all of the serialized transaction index entries + // for the block and serialize them directly into the slice. Then, pass the appropriate subslice to the database to + // be written. This approach significantly cuts down on the number of required allocations. + offset := 0 + serializedValues := make([]byte, len(block.Transactions())*txEntrySize) + for i, tx := range block.Transactions() { + putTxIndexEntry(serializedValues[offset:], blockID, txLocs[i]) + endOffset := offset + txEntrySize + e := dbPutTxIndexEntry( + dbTx, tx.Hash(), + serializedValues[offset:endOffset:endOffset], + ) + if e != nil { + return e + } + offset += txEntrySize + } + return nil +} + +// dbRemoveTxIndexEntry uses an existing database transaction to remove the most recent transaction index entry for the +// given hash. +func dbRemoveTxIndexEntry(dbTx database.Tx, txHash *chainhash.Hash) (e error) { + txIndex := dbTx.Metadata().Bucket(txIndexKey) + serializedData := txIndex.Get(txHash[:]) + if len(serializedData) == 0 { + return fmt.Errorf( + "can't remove non-existent transaction %s "+ + "from the transaction index", txHash, + ) + } + return txIndex.Delete(txHash[:]) +} + +// dbRemoveTxIndexEntries uses an existing database transaction to remove the latest transaction entry for every +// transaction in the passed block. +func dbRemoveTxIndexEntries(dbTx database.Tx, block *block.Block) (e error) { + for _, tx := range block.Transactions() { + e := dbRemoveTxIndexEntry(dbTx, tx.Hash()) + if e != nil { + return e + } + } + return nil +} + +// TxIndex implements a transaction by hash index. That is to say, it supports querying all transactions by their hash. +type TxIndex struct { + db database.DB + curBlockID uint32 +} + +// Ensure the TxIndex type implements the Indexer interface. +var _ Indexer = (*TxIndex)(nil) + +// Init initializes the hash-based transaction index. In particular, it finds the highest used block ID and stores it +// for later use when connecting or disconnecting blocks. This is part of the Indexer interface. +func (idx *TxIndex) Init() (e error) { + // Find the latest known block id field for the internal block id index and initialize it. This is done because it's + // a lot more efficient to do a single search at initialize time than it is to write another value to the database + // on every update. + e = idx.db.View( + func(dbTx database.Tx) (e error) { + // Scan forward in large gaps to find a block id that doesn't exist yet to serve as an upper bound for the + // binary search below. + var highestKnown, nextUnknown uint32 + testBlockID := uint32(1) + increment := uint32(100000) + for { + _, e := dbFetchBlockHashByID(dbTx, testBlockID) + if e != nil { + // F.Ln(err) + nextUnknown = testBlockID + break + } + highestKnown = testBlockID + testBlockID += increment + } + T.F("forward scan (highest known %d, next unknown %d)", highestKnown, nextUnknown) + // No used block IDs due to new database. + if nextUnknown == 1 { + return nil + } + // Use a binary search to find the final highest used block id. This will take at most ceil(log_2(increment)) + // attempts. + for { + testBlockID = (highestKnown + nextUnknown) / 2 + _, e := dbFetchBlockHashByID(dbTx, testBlockID) + if e != nil { + // F.Ln(err) + nextUnknown = testBlockID + } else { + highestKnown = testBlockID + } + T.F("binary scan (highest known %d, next unknown %d)", highestKnown, nextUnknown) + if highestKnown+1 == nextUnknown { + break + } + } + idx.curBlockID = highestKnown + return nil + }, + ) + if e != nil { + return e + } + T.Ln("current internal block ID:", idx.curBlockID) + return nil +} + +// Key returns the database key to use for the index as a byte slice. This is part of the Indexer interface. +func (idx *TxIndex) Key() []byte { + return txIndexKey +} + +// Name returns the human-readable name of the index. This is part of the Indexer interface. +func (idx *TxIndex) Name() string { + return txIndexName +} + +// Create is invoked when the indexer manager determines the index needs to be created for the first time. It creates +// the buckets for the hash-based transaction index and the internal block ID indexes. This is part of the Indexer +// interface. +func (idx *TxIndex) Create(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + if _, e = meta.CreateBucket(idByHashIndexBucketName); E.Chk(e) { + return e + } + if _, e = meta.CreateBucket(hashByIDIndexBucketName); E.Chk(e) { + return e + } + _, e = meta.CreateBucket(txIndexKey) + return e +} + +// ConnectBlock is invoked by the index manager when a new block has been connected to the main chain. This indexer adds +// a hash-to-transaction mapping for every transaction in the passed block. This is part of the Indexer interface. +func (idx *TxIndex) ConnectBlock(dbTx database.Tx, block *block.Block, stxos []blockchain.SpentTxOut) (e error) { + // Increment the internal block ID to use for the block being connected and add all of the transactions in the block + // to the index. + newBlockID := idx.curBlockID + 1 + if e = dbAddTxIndexEntries(dbTx, block, newBlockID); E.Chk(e) { + return e + } + // Add the new block ID index entry for the block being connected and update the current internal block ID + // accordingly. + e = dbPutBlockIDIndexEntry(dbTx, block.Hash(), newBlockID) + if e != nil { + return e + } + idx.curBlockID = newBlockID + return nil +} + +// DisconnectBlock is invoked by the index manager when a block has been disconnected from the main chain. +// +// This indexer removes the hash-to-transaction mapping for every transaction in the block. +// +// This is part of the Indexer interface. +func (idx *TxIndex) DisconnectBlock( + dbTx database.Tx, block *block.Block, + stxos []blockchain.SpentTxOut, +) (e error) { + // Remove all of the transactions in the block from the index. + if e = dbRemoveTxIndexEntries(dbTx, block); E.Chk(e) { + return e + } + // Remove the block ID index entry for the block being disconnected and decrement the current internal block ID to + // account for it. + if e = dbRemoveBlockIDIndexEntry(dbTx, block.Hash()); E.Chk(e) { + return e + } + idx.curBlockID-- + return nil +} + +// TxBlockRegion returns the block region for the provided transaction hash from the transaction index. +// +// The block region can in turn be used to load the raw transaction bytes. +// +// When there is no entry for the provided hash, nil will be returned for the both the entry and the error. +// +// This function is safe for concurrent access. +func (idx *TxIndex) TxBlockRegion(hash *chainhash.Hash) (region *database.BlockRegion, e error) { + e = idx.db.View( + func(dbTx database.Tx) (e error) { + region, e = dbFetchTxIndexEntry(dbTx, hash) + return e + }, + ) + return region, e +} + +// NewTxIndex returns a new instance of an indexer that is used to create a mapping of the hashes of all transactions in +// the blockchain to the respective block, location within the block, and size of the transaction. +// +// It implements the Indexer interface which plugs into the IndexManager that in turn is used by the blockchain package. +// +// This allows the index to be seamlessly maintained along with the chain. +func NewTxIndex(db database.DB) *TxIndex { + return &TxIndex{db: db} +} + +// dropBlockIDIndex drops the internal block id index. +func dropBlockIDIndex(db database.DB) (e error) { + return db.Update( + func(dbTx database.Tx) (e error) { + meta := dbTx.Metadata() + e = meta.DeleteBucket(idByHashIndexBucketName) + if e != nil { + return e + } + return meta.DeleteBucket(hashByIDIndexBucketName) + }, + ) +} + +// DropTxIndex drops the transaction index from the provided database if it exists. Since the address index relies on +// it, the address index will also be dropped when it exists. +func DropTxIndex(db database.DB, interrupt qu.C) (e error) { + e = dropIndex(db, addrIndexKey, addrIndexName, interrupt) + if e != nil { + return e + } + return dropIndex(db, txIndexKey, txIndexName, interrupt) +} diff --git a/pkg/interrupt/README.md b/pkg/interrupt/README.md new file mode 100644 index 0000000..fc97615 --- /dev/null +++ b/pkg/interrupt/README.md @@ -0,0 +1,2 @@ +# interrupt +Handle shutdowns cleanly easy restarts (theoretically) diff --git a/pkg/interrupt/cmd/ctrlctest.go b/pkg/interrupt/cmd/ctrlctest.go new file mode 100644 index 0000000..f417198 --- /dev/null +++ b/pkg/interrupt/cmd/ctrlctest.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + + "github.com/p9c/p9/pkg/interrupt" +) + +func main() { + interrupt.AddHandler(func() { + fmt.Println("IT'S THE END OF THE WORLD!") + }) + <-interrupt.HandlersDone +} diff --git a/pkg/interrupt/log.go b/pkg/interrupt/log.go new file mode 100644 index 0000000..aea7f92 --- /dev/null +++ b/pkg/interrupt/log.go @@ -0,0 +1,8 @@ +package interrupt + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/pkg/interrupt/main.go b/pkg/interrupt/main.go new file mode 100644 index 0000000..a2f555f --- /dev/null +++ b/pkg/interrupt/main.go @@ -0,0 +1,187 @@ +package interrupt + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "runtime" + "strings" + "syscall" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/qu" + + "github.com/kardianos/osext" +) + +type HandlerWithSource struct { + Source string + Fn func() +} + +var ( + Restart bool // = true + requested uberatomic.Bool + // ch is used to receive SIGINT (Ctrl+C) signals. + ch chan os.Signal + // signals is the list of signals that cause the interrupt + signals = []os.Signal{os.Interrupt} + // ShutdownRequestChan is a channel that can receive shutdown requests + ShutdownRequestChan = qu.T() + // addHandlerChan is used to add an interrupt handler to the list of + // handlers to be invoked on SIGINT (Ctrl+C) signals. + addHandlerChan = make(chan HandlerWithSource) + // HandlersDone is closed after all interrupt handlers run the first time + // an interrupt is signaled. + HandlersDone = make(qu.C) +) + +var interruptCallbacks []func() +var interruptCallbackSources []string + +// Listener listens for interrupt signals, registers interrupt callbacks, +// and responds to custom shutdown signals as required +func Listener() { + invokeCallbacks := func() { + D.Ln( + "running interrupt callbacks", + len(interruptCallbacks), + strings.Repeat(" ", 48), + interruptCallbackSources, + ) + // run handlers in LIFO order. + for i := range interruptCallbacks { + idx := len(interruptCallbacks) - 1 - i + D.Ln("running callback", idx, interruptCallbackSources[idx]) + interruptCallbacks[idx]() + } + D.Ln("interrupt handlers finished") + HandlersDone.Q() + if Restart { + var file string + var e error + file, e = osext.Executable() + if e != nil { + E.Ln(e) + return + } + D.Ln("restarting") + if runtime.GOOS != "windows" { + e = syscall.Exec(file, os.Args, os.Environ()) + if e != nil { + F.Ln(e) + } + } else { + D.Ln("doing windows restart") + + // procAttr := new(os.ProcAttr) + // procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr} + // os.StartProcess(os.Args[0], os.Args[1:], procAttr) + + var s []string + // s = []string{"cmd.exe", "/C", "start"} + s = append(s, os.Args[0]) + // s = append(s, "--delaystart") + s = append(s, os.Args[1:]...) + cmd := exec.Command(s[0], s[1:]...) + D.Ln("windows restart done") + if e = cmd.Start(); E.Chk(e) { + } + // // select{} + // os.Exit(0) + } + } + // time.Sleep(time.Second * 3) + // os.Exit(1) + // close(HandlersDone) + } +out: + for { + select { + case sig := <-ch: + // if !requested { + // L.Printf("\r>>> received signal (%s)\n", sig) + D.Ln("received interrupt signal", sig) + requested.Store(true) + invokeCallbacks() + // pprof.Lookup("goroutine").WriteTo(os.Stderr, 2) + // } + break out + case <-ShutdownRequestChan.Wait(): + // if !requested { + W.Ln("received shutdown request - shutting down...") + requested.Store(true) + invokeCallbacks() + break out + // } + case handler := <-addHandlerChan: + // if !requested { + // D.Ln("adding handler") + interruptCallbacks = append(interruptCallbacks, handler.Fn) + interruptCallbackSources = append(interruptCallbackSources, handler.Source) + // } + case <-HandlersDone.Wait(): + break out + } + } +} + +// AddHandler adds a handler to call when a SIGINT (Ctrl+C) is received. +func AddHandler(handler func()) { + // Create the channel and start the main interrupt handler which invokes all other callbacks and exits if not + // already done. + _, loc, line, _ := runtime.Caller(1) + msg := fmt.Sprintf("%s:%d", loc, line) + D.Ln("handler added by:", msg) + if ch == nil { + ch = make(chan os.Signal) + signal.Notify(ch, signals...) + go Listener() + } + addHandlerChan <- HandlerWithSource{ + msg, handler, + } +} + +// Request programmatically requests a shutdown +func Request() { + _, f, l, _ := runtime.Caller(1) + D.Ln("interrupt requested", f, l, requested.Load()) + if requested.Load() { + D.Ln("requested again") + return + } + requested.Store(true) + ShutdownRequestChan.Q() + // qu.PrintChanState() + var ok bool + select { + case _, ok = <-ShutdownRequestChan: + default: + } + D.Ln("shutdownrequestchan", ok) + if ok { + close(ShutdownRequestChan) + } +} + +// GoroutineDump returns a string with the current goroutine dump in order to show what's going on in case of timeout. +func GoroutineDump() string { + buf := make([]byte, 1<<18) + n := runtime.Stack(buf, true) + return string(buf[:n]) +} + +// RequestRestart sets the reset flag and requests a restart +func RequestRestart() { + Restart = true + D.Ln("requesting restart") + Request() +} + +// Requested returns true if an interrupt has been requested +func Requested() bool { + return requested.Load() +} diff --git a/pkg/interrupt/sigterm.go b/pkg/interrupt/sigterm.go new file mode 100644 index 0000000..4e547cc --- /dev/null +++ b/pkg/interrupt/sigterm.go @@ -0,0 +1,13 @@ +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package interrupt + +import ( + "os" + "syscall" +) + +func init() { + + signals = []os.Signal{os.Interrupt, syscall.SIGTERM} +} diff --git a/pkg/log/LICENSE b/pkg/log/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/log/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/log/logg.go b/pkg/log/logg.go new file mode 100644 index 0000000..c7832e3 --- /dev/null +++ b/pkg/log/logg.go @@ -0,0 +1,577 @@ +package log + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/gookit/color" + uberatomic "go.uber.org/atomic" +) + +const ( + _Off = iota + _Fatal + _Error + _Chek + _Warn + _Info + _Debug + _Trace +) + +type ( + // LevelPrinter defines a set of terminal printing primitives that output with + // extra data, time, log logLevelList, and code location + LevelPrinter struct { + // Ln prints lists of interfaces with spaces in between + Ln func(a ...interface{}) + // F prints like fmt.Println surrounded by log details + F func(format string, a ...interface{}) + // S prints a spew.Sdump for an interface slice + S func(a ...interface{}) + // C accepts a function so that the extra computation can be avoided if it is + // not being viewed + C func(closure func() string) + // Chk is a shortcut for printing if there is an error, or returning true + Chk func(e error) bool + } + logLevelList struct { + Off, Fatal, Error, Check, Warn, Info, Debug, Trace int32 + } + LevelSpec struct { + ID int32 + Name string + Colorizer func(format string, a ...interface{}) string + } + + // Entry is a log entry to be printed as json to the log file + Entry struct { + Time time.Time + Level string + Package string + CodeLocation string + Text string + } +) + +var ( + logger_started = time.Now() + App = " pod" + AppColorizer = color.White.Sprint + // sep is just a convenient shortcut for this very longwinded expression + sep = string(os.PathSeparator) + currentLevel = uberatomic.NewInt32(logLevels.Info) + // writer can be swapped out for any io.*writer* that you want to use instead of + // stdout. + writer io.Writer = os.Stderr + // allSubsystems stores all of the package subsystem names found in the current + // application + allSubsystems []string + // highlighted is a text that helps visually distinguish a log entry by category + highlighted = make(map[string]struct{}) + // logFilter specifies a set of packages that will not pr logs + logFilter = make(map[string]struct{}) + // mutexes to prevent concurrent map accesses + highlightMx, _logFilterMx sync.Mutex + // logLevels is a shorthand access that minimises possible Name collisions in the + // dot import + logLevels = logLevelList{ + Off: _Off, + Fatal: _Fatal, + Error: _Error, + Check: _Chek, + Warn: _Warn, + Info: _Info, + Debug: _Debug, + Trace: _Trace, + } + // LevelSpecs specifies the id, string name and color-printing function + LevelSpecs = []LevelSpec{ + {logLevels.Off, "off ", color.Bit24(0, 0, 0, false).Sprintf}, + {logLevels.Fatal, "fatal", color.Bit24(128, 0, 0, false).Sprintf}, + {logLevels.Error, "error", color.Bit24(255, 0, 0, false).Sprintf}, + {logLevels.Check, "check", color.Bit24(255, 255, 0, false).Sprintf}, + {logLevels.Warn, "warn ", color.Bit24(0, 255, 0, false).Sprintf}, + {logLevels.Info, "info ", color.Bit24(255, 255, 0, false).Sprintf}, + {logLevels.Debug, "debug", color.Bit24(0, 128, 255, false).Sprintf}, + {logLevels.Trace, "trace", color.Bit24(128, 0, 255, false).Sprintf}, + } + Levels = []string{ + Off, + Fatal, + Error, + Check, + Warn, + Info, + Debug, + Trace, + } + LogChanDisabled = uberatomic.NewBool(true) + LogChan chan Entry +) + +const ( + Off = "off" + Fatal = "fatal" + Error = "error" + Warn = "warn" + Info = "info" + Check = "check" + Debug = "debug" + Trace = "trace" +) + +// AddLogChan adds a channel that log entries are sent to +func AddLogChan() (ch chan Entry) { + LogChanDisabled.Store(false) + if LogChan != nil { + panic("warning warning") + } + // L.Writer.Write.Store( false + LogChan = make(chan Entry) + return LogChan +} + +// GetLogPrinterSet returns a set of LevelPrinter with their subsystem preloaded +func GetLogPrinterSet(subsystem string) (Fatal, Error, Warn, Info, Debug, Trace LevelPrinter) { + return _getOnePrinter(_Fatal, subsystem), + _getOnePrinter(_Error, subsystem), + _getOnePrinter(_Warn, subsystem), + _getOnePrinter(_Info, subsystem), + _getOnePrinter(_Debug, subsystem), + _getOnePrinter(_Trace, subsystem) +} + +func _getOnePrinter(level int32, subsystem string) LevelPrinter { + return LevelPrinter{ + Ln: _ln(level, subsystem), + F: _f(level, subsystem), + S: _s(level, subsystem), + C: _c(level, subsystem), + Chk: _chk(level, subsystem), + } +} + +// SetLogLevel sets the log level via a string, which can be truncated down to +// one character, similar to nmcli's argument processor, as the first letter is +// unique. This could be used with a linter to make larger command sets. +func SetLogLevel(l string) { + if l == "" { + l = "info" + } + // fmt.Fprintln(os.Stderr, "setting log level", l) + lvl := logLevels.Info + for i := range LevelSpecs { + if LevelSpecs[i].Name[:1] == l[:1] { + lvl = LevelSpecs[i].ID + } + } + currentLevel.Store(lvl) +} + +// SetLogWriter atomically changes the log io.Writer interface +func SetLogWriter(wr io.Writer) { + // w := unsafe.Pointer(writer) + // c := unsafe.Pointer(wr) + // atomic.SwapPointer(&w, c) + writer = wr +} + +func SetLogWriteToFile(path, appName string) (e error) { + // copy existing log file to dated log file as we will truncate it per + // session + path = filepath.Join(path, "log"+appName) + if _, e = os.Stat(path); e == nil { + var b []byte + b, e = ioutil.ReadFile(path) + if e == nil { + ioutil.WriteFile(path+fmt.Sprint(time.Now().Unix()), b, 0600) + } + } + var fileWriter *os.File + if fileWriter, e = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0600); e != nil { + fmt.Fprintln(os.Stderr, "unable to write log to", path, "error:", e) + return + } + mw := io.MultiWriter(os.Stderr, fileWriter) + fileWriter.Write([]byte("logging to file '" + path + "'\n")) + mw.Write([]byte("logging to file '" + path + "'\n")) + SetLogWriter(mw) + return +} + +// SortSubsystemsList sorts the list of subsystems, to keep the data read-only, +// call this function right at the top of the main, which runs after +// declarations and main/init. Really this is just here to alert the reader. +func SortSubsystemsList() { + sort.Strings(allSubsystems) + // fmt.Fprintln( + // os.Stderr, + // spew.Sdump(allSubsystems), + // spew.Sdump(highlighted), + // spew.Sdump(logFilter), + // ) +} + +// AddLoggerSubsystem adds a subsystem to the list of known subsystems and returns the +// string so it is nice and neat in the package logg.go file +func AddLoggerSubsystem(pathBase string) (subsystem string) { + // var split []string + var ok bool + var file string + _, file, _, ok = runtime.Caller(1) + if ok { + r := strings.Split(file, pathBase) + // fmt.Fprintln(os.Stderr, version.PathBase, r) + fromRoot := filepath.Base(file) + if len(r) > 1 { + fromRoot = r[1] + } + split := strings.Split(fromRoot, "/") + // fmt.Fprintln(os.Stderr, version.PathBase, "file", file, r, fromRoot, split) + subsystem = strings.Join(split[:len(split)-1], "/") + // fmt.Fprintln(os.Stderr, "adding subsystem", subsystem) + allSubsystems = append(allSubsystems, subsystem) + } + return +} + +// StoreHighlightedSubsystems sets the list of subsystems to highlight +func StoreHighlightedSubsystems(highlights []string) (found bool) { + highlightMx.Lock() + highlighted = make(map[string]struct{}, len(highlights)) + for i := range highlights { + highlighted[highlights[i]] = struct{}{} + } + highlightMx.Unlock() + return +} + +// LoadHighlightedSubsystems returns a copy of the map of highlighted subsystems +func LoadHighlightedSubsystems() (o []string) { + highlightMx.Lock() + o = make([]string, len(logFilter)) + var counter int + for i := range logFilter { + o[counter] = i + counter++ + } + highlightMx.Unlock() + sort.Strings(o) + return +} + +// StoreSubsystemFilter sets the list of subsystems to filter +func StoreSubsystemFilter(filter []string) { + _logFilterMx.Lock() + logFilter = make(map[string]struct{}, len(filter)) + for i := range filter { + logFilter[filter[i]] = struct{}{} + } + _logFilterMx.Unlock() +} + +// LoadSubsystemFilter returns a copy of the map of filtered subsystems +func LoadSubsystemFilter() (o []string) { + _logFilterMx.Lock() + o = make([]string, len(logFilter)) + var counter int + for i := range logFilter { + o[counter] = i + counter++ + } + _logFilterMx.Unlock() + sort.Strings(o) + return +} + +// _isHighlighted returns true if the subsystem is in the list to have attention +// getters added to them +func _isHighlighted(subsystem string) (found bool) { + highlightMx.Lock() + _, found = highlighted[subsystem] + highlightMx.Unlock() + return +} + +// AddHighlightedSubsystem adds a new subsystem Name to the highlighted list +func AddHighlightedSubsystem(hl string) struct{} { + highlightMx.Lock() + highlighted[hl] = struct{}{} + highlightMx.Unlock() + return struct{}{} +} + +// _isSubsystemFiltered returns true if the subsystem should not pr logs +func _isSubsystemFiltered(subsystem string) (found bool) { + _logFilterMx.Lock() + _, found = logFilter[subsystem] + _logFilterMx.Unlock() + return +} + +// AddFilteredSubsystem adds a new subsystem Name to the highlighted list +func AddFilteredSubsystem(hl string) struct{} { + _logFilterMx.Lock() + logFilter[hl] = struct{}{} + _logFilterMx.Unlock() + return struct{}{} +} + +func getTimeText(level int32) string { + // since := time.Now().Sub(logger_started).Round(time.Millisecond).String() + // diff := 12 - len(since) + // if diff > 0 { + // since = strings.Repeat(" ", diff) + since + " " + // } + return color.Bit24(99, 99, 99, false).Sprint(time.Now(). + Format(time.StampMilli)) +} + +func _ln(level int32, subsystem string) func(a ...interface{}) { + return func(a ...interface{}) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer(joinStrings(" ", a...)), + ), + ) + } + } +} + +func _f(level int32, subsystem string) func(format string, a ...interface{}) { + return func(format string, a ...interface{}) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer(fmt.Sprintf(format, a...)), + ), + ) + } + } +} + +func _s(level int32, subsystem string) func(a ...interface{}) { + return func(a ...interface{}) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%s%s%s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer( + " spew:", + ), + fmt.Sprint( + color.Bit24(20, 20, 20, true).Sprint("\n\n"+spew.Sdump(a)), + "\n", + ), + ), + ) + } + } +} + +func _c(level int32, subsystem string) func(closure func() string) { + return func(closure func() string) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer(closure()), + ), + ) + } + } +} + +func _chk(level int32, subsystem string) func(e error) bool { + return func(e error) bool { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + if e != nil { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + LevelSpecs[level].Colorizer(joinStrings(" ", e.Error())), + ), + ) + return true + } + } + return false + } +} + +// joinStrings constructs a string from an slice of interface same as Println but +// without the terminal newline +func joinStrings(sep string, a ...interface{}) (o string) { + for i := range a { + o += fmt.Sprint(a[i]) + if i < len(a)-1 { + o += sep + } + } + return +} + +// getLoc calls runtime.Caller and formats as expected by source code editors +// for terminal hyperlinks +// +// Regular expressions and the substitution texts to make these clickable in +// Tilix and other RE hyperlink configurable terminal emulators: +// +// This matches the shortened paths generated in this command and printed at +// the very beginning of the line as this logger prints: +// +// ^((([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+)) +// +// goland --line $5 $GOPATH/src/github.com/p9c/matrjoska/$2 +// +// I have used a shell variable there but tilix doesn't expand them, +// so put your GOPATH in manually, and obviously change the repo subpath. +// +// +// Change the path to use with another repository's logging output ( +// someone with more time on their hands could probably come up with +// something, but frankly the custom links feature of Tilix has the absolute +// worst UX I have encountered since the 90s... +// Maybe in the future this library will be expanded with a tool that more +// intelligently sets the path, ie from CWD or other cleverness. +// +// This matches full paths anywhere on the commandline delimited by spaces: +// +// ([/](([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+)) +// +// goland --line $5 /$2 +// +// Adapt the invocation to open your preferred editor if it has the capability, +// the above is for Jetbrains Goland +// +func getLoc(skip int, level int32, subsystem string) (output string) { + _, file, line, _ := runtime.Caller(skip) + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "getloc panic on subsystem", subsystem, file) + } + }() + split := strings.Split(file, subsystem) + if len(split) < 2 { + output = fmt.Sprint( + color.White.Sprint(subsystem), + color.Gray.Sprint( + file, ":", line, + ), + ) + } else { + output = fmt.Sprint( + color.White.Sprint(subsystem), + color.Gray.Sprint( + split[1], ":", line, + ), + ) + } + return +} + +// DirectionString is a helper function that returns a string that represents the direction of a connection (inbound or outbound). +func DirectionString(inbound bool) string { + if inbound { + return "inbound" + } + return "outbound" +} + +func PickNoun(n int, singular, plural string) string { + if n == 1 { + return singular + } + return plural +} + +func FileExists(filePath string) bool { + _, e := os.Stat(filePath) + return e == nil +} + +func Caller(comment string, skip int) string { + _, file, line, _ := runtime.Caller(skip + 1) + o := fmt.Sprintf("%s: %s:%d", comment, file, line) + // L.Debug(o) + return o +} diff --git a/pkg/log/readme.md b/pkg/log/readme.md new file mode 100644 index 0000000..2c585ce --- /dev/null +++ b/pkg/log/readme.md @@ -0,0 +1,23 @@ +# logg + +This is a very simple, but practical library for +logging in applications. Currenty in process of +superseding the logi library, once the pipe logger +is converted, as it is integrated into the block +sync progress logger. + +To use it, create a file in a package you want +to add the logger to with the name `log.go`, and then: + +```bash +go run ./pkg/logg/deploy/. +``` + +from the root of the github.com/p9c/p9 repository +and it replicates its template with the altered +package name to match the folder name - so you need +to adhere to the rule that package names and the folder +name are the same. + +The library includes functions to toggle the filtering, +highlight and filtering sets while it is running. \ No newline at end of file diff --git a/pkg/logo/LICENSE b/pkg/logo/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/logo/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/logo/README.md b/pkg/logo/README.md new file mode 100644 index 0000000..505b1b2 --- /dev/null +++ b/pkg/logo/README.md @@ -0,0 +1,2 @@ +# logo +Authoritative repository of ParallelCoin branding diff --git a/pkg/logo/logo.svg b/pkg/logo/logo.svg new file mode 100644 index 0000000..edbc2a3 --- /dev/null +++ b/pkg/logo/logo.svg @@ -0,0 +1,77 @@ + +image/svg+xml + + + + + + + + + + diff --git a/pkg/logo/logo1024x1024.png b/pkg/logo/logo1024x1024.png new file mode 100644 index 0000000..f015502 Binary files /dev/null and b/pkg/logo/logo1024x1024.png differ diff --git a/pkg/logo/logo_nobg.svg b/pkg/logo/logo_nobg.svg new file mode 100644 index 0000000..a0f3af4 --- /dev/null +++ b/pkg/logo/logo_nobg.svg @@ -0,0 +1,68 @@ + +image/svg+xml + + + + + + + + diff --git a/pkg/logo/logo_nobg1024x1024.png b/pkg/logo/logo_nobg1024x1024.png new file mode 100644 index 0000000..bcb33ed Binary files /dev/null and b/pkg/logo/logo_nobg1024x1024.png differ diff --git a/pkg/logo/logo_nocircle.svg b/pkg/logo/logo_nocircle.svg new file mode 100644 index 0000000..81d4533 --- /dev/null +++ b/pkg/logo/logo_nocircle.svg @@ -0,0 +1,75 @@ + +image/svg+xml + + + + + + + + + diff --git a/pkg/logo/logo_nocircle1024x1024.png b/pkg/logo/logo_nocircle1024x1024.png new file mode 100644 index 0000000..30508a6 Binary files /dev/null and b/pkg/logo/logo_nocircle1024x1024.png differ diff --git a/pkg/logo/logo_nocircle128x128.png b/pkg/logo/logo_nocircle128x128.png new file mode 100644 index 0000000..fd5ded1 Binary files /dev/null and b/pkg/logo/logo_nocircle128x128.png differ diff --git a/pkg/logo/logo_nocircle128x128.svg b/pkg/logo/logo_nocircle128x128.svg new file mode 100644 index 0000000..bc9434f --- /dev/null +++ b/pkg/logo/logo_nocircle128x128.svg @@ -0,0 +1,79 @@ + +image/svg+xml + + + + + + + + + diff --git a/pkg/mempool/README.md b/pkg/mempool/README.md new file mode 100755 index 0000000..a8ae151 --- /dev/null +++ b/pkg/mempool/README.md @@ -0,0 +1,106 @@ +# mempool + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/mempool) + +Package mempool provides a policy-enforced pool of unmined bitcoin transactions. + +A key responsbility of the bitcoin network is mining user-generated transactions +into blocks. In order to facilitate this, the mining process relies on having a +readily-available source of transactions to include in a block that is being +solved. + +At a high level, this package satisfies that requirement by providing an +in-memory pool of fully validated transactions that can also optionally be +further filtered based upon a configurable policy. + +One of the policy configuration options controls whether or not "standard" +transactions are accepted. In essence, a " +standard" transaction is one that satisfies a fairly strict set of requirements +that are largely intended to help provide fair use of the system to all users. +It is important to note that what is considered a "standard" transaction changes +over time. For some insight, at the time of this writing, an example of _some_ +of the criteria that are required for a transaction to be considered standard +are that it is of the most-recently supported version, finalized, does not +exceed a specific size, and only consists of specific script forms. + +Since this package does not deal with other bitcoin specifics such as network +communication and transaction relay, it returns a list of transactions that were +accepted which gives the caller a high level of flexibility in how they want to +proceed. Typically, this will involve things such as relaying the transactions +to other peers on the network and notifying the mining process that new +transactions are available. + +This package has intentionally been designed so it can be used as a standalone +package for any projects needing the ability create an in-memory pool of bitcoin +transactions that are not only valid by consensus rules, but also adhere to a +configurable policy. + +## Feature Overview + +The following is a quick overview of the major features. It is not intended to +be an exhaustive list. + +- Maintain a pool of fully validated transactions + + - Reject non-fully-spent duplicate transactions + + - Reject coinbase transactions + + - Reject double spends (both from the chain and other transactions in pool) + + - Reject invalid transactions according to the network consensus rules + + - Full script execution and validation with signature cache support + + - Individual transaction query support + +- Orphan transaction support (transactions that spend from unknown outputs) + + - Configurable limits (see transaction acceptance policy) + + - Automatic addition of orphan transactions that are no longer orphans as + new transactions are added to the pool + + - Individual orphan transaction query support + +- Configurable transaction acceptance policy + + - Option to accept or reject standard transactions + + - Option to accept or reject transactions based on priority calculations + + - Rate limiting of low-fee and free transactions + + - Non-zero fee threshold + + - Max signature operations per transaction + + - Max orphan transaction size + + - Max number of orphan transactions allowed + +- Additional metadata tracking for each transaction + + - Timestamp when the transaction was added to the pool + + - Most recent block height when the transaction was added to the pool + + - The fee the transaction pays + + - The starting priority for the transaction + +- Manual control of transaction removal + + - Recursive removal of all dependent transactions + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/mempool +``` + +## License + +Package mempool is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/mempool/doc.go b/pkg/mempool/doc.go new file mode 100644 index 0000000..1def1b5 --- /dev/null +++ b/pkg/mempool/doc.go @@ -0,0 +1,67 @@ +/* +Package mempool provides a policy-enforced pool of unmined bitcoin transactions. + +A key responsibility of the bitcoin network is mining user-generated transactions into blocks. In order to facilitate +this, the mining process relies on having a readily-available source of transactions to include in a block that is being +solved. + +At a high level, this package satisfies that requirement by providing an in-memory pool of fully validated transactions +that can also optionally be further filtered based upon a configurable policy. One of the policy configuration options +controls whether or not "standard" transactions are accepted. In essence, a "standard" transaction is one that satisfies +a fairly strict set of requirements that are largely intended to help provide fair use of the system to all users. + +It is important to note that what is considered a "standard" transaction changes over time. For some insight, at the +time of this writing, an example of SOME of the criteria that are required for a transaction to be considered standard +are that it is of the most-recently supported version, finalized, does not exceed a specific size, and only consists of +specific script forms. + +Since this package does not deal with other bitcoin specifics such as network communication and transaction relay, it +returns a list of transactions that were accepted which gives the caller a high level of flexibility in how they want to +proceed. Typically, this will involve things such as relaying the transactions to other peers on the network and +notifying the mining process that new transactions are available. + +Feature Overview + +The following is a quick overview of the major features. It is not intended to be an exhaustive list. + + - Maintain a pool of fully validated transactions + - Reject non-fully-spent duplicate transactions + - Reject coinbase transactions + - Reject double spends (both from the chain and other transactions in pool) + - Reject invalid transactions according to the network consensus rules + - Full script execution and validation with signature cache support + - Individual transaction query support + - Orphan transaction support (transactions that spend from unknown outputs) + - Configurable limits (see transaction acceptance policy) + - Automatic addition of orphan transactions that are no longer orphans as new transactions are added to the pool + - Individual orphan transaction query support + - Configurable transaction acceptance policy + - Option to accept or reject standard transactions + - Option to accept or reject transactions based on priority calculations + - Rate limiting of low-fee and free transactions + - Non-zero fee threshold + - Max signature operations per transaction + - Max orphan transaction size + - Max number of orphan transactions allowed + - Additional metadata tracking for each transaction + - Timestamp when the transaction was added to the pool + - Most recent block height when the transaction was added to the pool + - The fee the transaction pays + - The starting priority for the transaction + - Manual control of transaction removal + - Recursive removal of all dependent transactions + +Errors + +Errors returned by this package are either the raw errors provided by underlying calls or of type mempool.RuleError. +Since there are two classes of rules (mempool acceptance rules and blockchain (consensus) acceptance rules), the +mempool.RuleError type contains a single Err field which will, in turn, either be a mempool.TxRuleError or a +blockchain.RuleError. + +The first indicates a violation of mempool acceptance rules while the latter indicates a violation of consensus +acceptance rules. This allows the caller to easily differentiate between unexpected errors, such as database errors, +versus errors due to rule violations through type assertions. In addition, callers can programmatically determine the +specific rule violation by type asserting the Err field to one of the aforementioned types and examining their +underlying ErrorCode field. +*/ +package mempool diff --git a/pkg/mempool/error.go b/pkg/mempool/error.go new file mode 100644 index 0000000..9993c13 --- /dev/null +++ b/pkg/mempool/error.go @@ -0,0 +1,110 @@ +package mempool + +import ( + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/wire" +) + +// RuleError identifies a rule violation. It is used to indicate that processing of a transaction failed due to one of +// the many validation rules. The caller can use type assertions to determine if a failure was specifically due to a +// rule violation and use the Err field to access the underlying error, which will be either a TxRuleError or a +// blockchain. RuleError. +type RuleError struct { + Err error +} + +// Error satisfies the error interface and prints human-readable errors. +func (e RuleError) Error() string { + if e.Err == nil { + return "" + } + return e.Err.Error() +} + +// TxRuleError identifies a rule violation. It is used to indicate that processing of a transaction failed due to one of +// the many validation rules. The caller can use type assertions to determine if a failure was specifically due to a +// rule violation and access the ErrorCode field to ascertain the specific reason for the rule violation. +type TxRuleError struct { + RejectCode wire.RejectCode // The code to send with reject messages + Description string // Human readable description of the issue +} + +// Error satisfies the error interface and prints human-readable errors. +func (e TxRuleError) Error() string { + return e.Description +} + +// txRuleError creates an underlying TxRuleError with the given a set of arguments and returns a RuleError that +// encapsulates it. +func txRuleError(c wire.RejectCode, desc string) RuleError { + return RuleError{ + Err: TxRuleError{RejectCode: c, Description: desc}, + } +} + +// chainRuleError returns a RuleError that encapsulates the given blockchain. RuleError. +func chainRuleError(chainErr blockchain.RuleError) RuleError { + return RuleError{ + Err: chainErr, + } +} + +// extractRejectCode attempts to return a relevant reject code for a given error by examining the error for known types. +// It will return true if a code was successfully extracted. +func extractRejectCode(e error) (wire.RejectCode, bool) { + // Pull the underlying error out of a RuleError. + if rerr, ok := e.(RuleError); ok { + e = rerr.Err + } + switch er := e.(type) { + case blockchain.RuleError: + // Convert the chain error to a reject code. + var code wire.RejectCode + switch er.ErrorCode { + // Rejected due to duplicate. + case blockchain.ErrDuplicateBlock: + code = wire.RejectDuplicate + // Rejected due to obsolete version. + case blockchain.ErrBlockVersionTooOld: + code = wire.RejectObsolete + // Rejected due to checkpoint. + case blockchain.ErrCheckpointTimeTooOld: + fallthrough + case blockchain.ErrDifficultyTooLow: + fallthrough + case blockchain.ErrBadCheckpoint: + fallthrough + case blockchain.ErrForkTooOld: + code = wire.RejectCheckpoint + // Everything else is due to the block or transaction being invalid. + default: + code = wire.RejectInvalid + } + return code, true + case TxRuleError: + return er.RejectCode, true + case nil: + return wire.RejectInvalid, false + } + return wire.RejectInvalid, false +} + +// ErrToRejectErr examines the underlying type of the error and returns a reject code and string appropriate to be sent +// in a wire.MsgReject message. +func ErrToRejectErr(e error) (wire.RejectCode, string) { + // Return the reject code along with the error text if it can be + // extracted from the error. + rejectCode, found := extractRejectCode(e) + if found { + return rejectCode, e.Error() + } + // Return a generic rejected string if there is no error. This really should not happen unless the code elsewhere is + // not setting an error as it should be but it's best to be safe and simply return a generic string rather than + // allowing the following code that dereferences the err to panic. + if e == nil { + return wire.RejectInvalid, "rejected" + } + // When the underlying error is not one of the above cases, just return wire.RejectInvalid with a generic rejected + // string plus the error text. + return wire.RejectInvalid, "rejected: " + e.Error() +} diff --git a/pkg/mempool/estimatefee.go b/pkg/mempool/estimatefee.go new file mode 100644 index 0000000..691a829 --- /dev/null +++ b/pkg/mempool/estimatefee.go @@ -0,0 +1,718 @@ +package mempool + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/block" + "io" + "math" + "math/rand" + "sort" + "strings" + "sync" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/mining" + "github.com/p9c/p9/pkg/util" +) + +// DUOPerKilobyte is number with units of parallelcoins per kilobyte. +type DUOPerKilobyte float64 + +// FeeEstimator manages the data necessary to create fee estimations. It is safe for concurrent access. +type FeeEstimator struct { + maxRollback uint32 + binSize int32 + // The maximum number of replacements that can be made in a single bin per block. Default is + // estimateFeeMaxReplacements + maxReplacements int32 + // The minimum number of blocks that can be registered with the fee estimator before it will provide answers. + minRegisteredBlocks uint32 + // The last known height. + lastKnownHeight int32 + // The number of blocks that have been registered. + numBlocksRegistered uint32 + mtx sync.RWMutex + observed map[chainhash.Hash]*observedTransaction + bin [estimateFeeDepth][]*observedTransaction + // The cached estimates. + cached []SatoshiPerByte + // Transactions that have been removed from the bins. This allows us to revert in case of an orphaned block. + dropped []*registeredBlock +} + +// FeeEstimatorState represents a saved FeeEstimator that can be restored with data from an earlier session of the +// program. +type FeeEstimatorState []byte + +// SatoshiPerByte is number with units of satoshis per byte. +type SatoshiPerByte float64 + +// estimateFeeSet is a set of txs that can that is sorted by the fee per kb rate. +type estimateFeeSet struct { + feeRate []SatoshiPerByte + bin [estimateFeeDepth]uint32 +} + +// observedTransaction represents an observed transaction and some additional data required for the fee estimation +// algorithm. +type observedTransaction struct { + // A transaction hash. + hash chainhash.Hash + // The fee per byte of the transaction in satoshis. + feeRate SatoshiPerByte + // The block height when it was observed. + observed int32 + // The height of the block in which it was mined. If the transaction has not yet been mined, it is zero. + mined int32 +} + +// observedTxSet is a set of txs that can that is sorted by hash. It exists for serialization purposes so that a +// serialized state always comes out the same. +type observedTxSet []*observedTransaction + +// registeredBlock has the hash of a block and the list of transactions it mined which had been previously observed by +// the FeeEstimator. It is used if Rollback is called to reverse the effect of registering a block. +type registeredBlock struct { + hash chainhash.Hash + transactions []*observedTransaction +} + +// TODO incorporate Alex Morcos' modifications to Gavin's initial model +// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2014-October/006824.html +const ( + // estimateFeeDepth is the maximum number of blocks before a transaction is confirmed that we want to track. + estimateFeeDepth = 25 + // estimateFeeBinSize is the number of txs stored in each bin. + estimateFeeBinSize = 100 + // estimateFeeMaxReplacements is the max number of replacements that can be made by the txs found in a given block. + estimateFeeMaxReplacements = 10 + // DefaultEstimateFeeMaxRollback is the default number of rollbacks allowed by the fee estimator for orphaned + // blocks. + DefaultEstimateFeeMaxRollback = 2 + // DefaultEstimateFeeMinRegisteredBlocks is the default minimum number of blocks which must be observed by the fee + // estimator before will provide fee estimations. + DefaultEstimateFeeMinRegisteredBlocks = 3 + bytePerKb = 1000 + duoPerSatoshi = 1e-8 +) + +// In case the format for the serialized version of the FeeEstimator changes, we use a version number. If the version +// number changes, it does not make sense to try to upgrade a previous version to a new version. Instead, just start fee +// estimation over. +const estimateFeeSaveVersion = 1 + +var ( + // EstimateFeeDatabaseKey is the key that we use to store the fee estimator in the database. + EstimateFeeDatabaseKey = []byte("estimatefee") +) + +// EstimateFee estimates the fee per byte to have a tx confirmed a given number of blocks from now. +func (ef *FeeEstimator) EstimateFee(numBlocks uint32) (DUOPerKilobyte, error) { + ef.mtx.Lock() + defer ef.mtx.Unlock() + // If the number of registered blocks is below the minimum, return an error. + if ef.numBlocksRegistered < ef.minRegisteredBlocks { + return -1, errors.New("not enough blocks have been observed") + } + if numBlocks == 0 { + return -1, errors.New("cannot confirm transaction in zero blocks") + } + if numBlocks > estimateFeeDepth { + return -1, fmt.Errorf( + "can only estimate fees for up to %d blocks from now", + estimateFeeBinSize, + ) + } + // If there are no cached results, generate them. + if ef.cached == nil { + ef.cached = ef.estimates() + } + return ef.cached[int(numBlocks)-1].ToBtcPerKb(), nil +} + +func // LastKnownHeight returns the height of the last block which was +// registered. +(ef *FeeEstimator) LastKnownHeight() int32 { + ef.mtx.Lock() + defer ef.mtx.Unlock() + return ef.lastKnownHeight +} + +// ObserveTransaction is called when a new transaction is observed in the mempool. +func (ef *FeeEstimator) ObserveTransaction( + t *TxDesc, +) { + ef.mtx.Lock() + defer ef.mtx.Unlock() + // If we haven't seen a block yet we don't know when this one arrived, so we ignore it. + if ef.lastKnownHeight == mining.UnminedHeight { + return + } + hash := *t.Tx.Hash() + if _, ok := ef.observed[hash]; !ok { + size := uint32(GetTxVirtualSize(t.Tx)) + ef.observed[hash] = &observedTransaction{ + hash: hash, + feeRate: NewSatoshiPerByte(amt.Amount(t.Fee), size), + observed: t.Height, + mined: mining.UnminedHeight, + } + } +} + +// RegisterBlock informs the fee estimator of a new block to take into account. +func (ef *FeeEstimator) RegisterBlock( + block *block.Block, +) (e error) { + ef.mtx.Lock() + defer ef.mtx.Unlock() + // The previous sorted list is invalid, so delete it. + ef.cached = nil + height := block.Height() + if height != ef.lastKnownHeight+1 && ef.lastKnownHeight != mining.UnminedHeight { + return fmt.Errorf( + "intermediate block not recorded; current height is %d; new height is %d", + ef.lastKnownHeight, height, + ) + } + // Update the last known height. + ef.lastKnownHeight = height + ef.numBlocksRegistered++ + // Randomly order txs in block. + transactions := make(map[*util.Tx]struct{}) + for _, t := range block.Transactions() { + transactions[t] = struct{}{} + } + // Count the number of replacements we make per bin so that we don't replace too many. + var replacementCounts [estimateFeeDepth]int + // Keep track of which txs were dropped in case of an orphan block. + dropped := ®isteredBlock{ + hash: *block.Hash(), + transactions: make([]*observedTransaction, 0, 100), + } + // Go through the txs in the block. + for t := range transactions { + hash := *t.Hash() + // Have we observed this tx in the mempool? + o, ok := ef.observed[hash] + if !ok { + continue + } + // Put the observed tx in the appropriate bin. + blocksToConfirm := height - o.observed - 1 + // This shouldn't happen if the fee estimator works correctly, but return an error if it does. + if o.mined != mining.UnminedHeight { + E.Ln("Estimate fee: transaction ", hash, " has already been mined") + return errors.New("transaction has already been mined") + } + // This shouldn't happen but check just in case to avoid an out-of -bounds array index later. + if blocksToConfirm >= estimateFeeDepth { + continue + } + // Make sure we do not replace too many transactions per min. + if replacementCounts[blocksToConfirm] == int(ef.maxReplacements) { + continue + } + o.mined = height + replacementCounts[blocksToConfirm]++ + bin := ef.bin[blocksToConfirm] + // Remove a random element and replace it with this new tx. + if len(bin) == int(ef.binSize) { + // Don't drop transactions we have just added from this same block. + l := int(ef.binSize) - replacementCounts[blocksToConfirm] + drop := rand.Intn(l) + dropped.transactions = append(dropped.transactions, bin[drop]) + bin[drop] = bin[l-1] + bin[l-1] = o + } else { + bin = append(bin, o) + } + ef.bin[blocksToConfirm] = bin + } + // Go through the mempool for txs that have been in too long. + for hash, o := range ef.observed { + if o.mined == mining.UnminedHeight && height-o.observed >= estimateFeeDepth { + delete(ef.observed, hash) + } + } + // Add dropped list to history. + if ef.maxRollback == 0 { + return nil + } + if uint32(len(ef.dropped)) == ef.maxRollback { + ef.dropped = append(ef.dropped[1:], dropped) + } else { + ef.dropped = append(ef.dropped, dropped) + } + return nil +} + +// Rollback unregisters a recently registered block from the FeeEstimator. This can be used to reverse the effect of an +// orphaned block on the fee estimator. The maximum number of rollbacks allowed is given by maxRollbacks. Note: not +// everything can be rolled back because some transactions are deleted if they have been observed too long ago. That +// means the result of Rollback won't always be exactly the same as if the last block had not happened, but it should be +// close enough. +func (ef *FeeEstimator) Rollback(hash *chainhash.Hash) (e error) { + ef.mtx.Lock() + defer ef.mtx.Unlock() + // Find this block in the stack of recent registered blocks. + var n int + for n = 1; n <= len(ef.dropped); n++ { + if ef.dropped[len(ef.dropped)-n].hash.IsEqual(hash) { + break + } + } + if n > len(ef.dropped) { + return errors.New("no such block was recently registered") + } + for i := 0; i < n; i++ { + ef.rollback() + } + return nil +} + +// Save records the current state of the FeeEstimator to a []byte that can be restored later. +func (ef *FeeEstimator) Save() FeeEstimatorState { + ef.mtx.Lock() + defer ef.mtx.Unlock() + // TODO figure out what the capacity should be. + w := bytes.NewBuffer(make([]byte, 0)) + e := binary.Write( + w, binary.BigEndian, uint32(estimateFeeSaveVersion), + ) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + // Insert basic parameters. + e = binary.Write(w, binary.BigEndian, &ef.maxRollback) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + e = binary.Write(w, binary.BigEndian, &ef.binSize) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + e = binary.Write(w, binary.BigEndian, &ef.maxReplacements) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + e = binary.Write(w, binary.BigEndian, &ef.minRegisteredBlocks) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + e = binary.Write(w, binary.BigEndian, &ef.lastKnownHeight) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + e = binary.Write(w, binary.BigEndian, &ef.numBlocksRegistered) + if e != nil { + F.Ln("failed to write fee estimates", e) + } + // Put all the observed transactions in a sorted list. + var txCount uint32 + ots := make([]*observedTransaction, len(ef.observed)) + for hash := range ef.observed { + ots[txCount] = ef.observed[hash] + txCount++ + } + sort.Sort(observedTxSet(ots)) + txCount = 0 + observed := make(map[*observedTransaction]uint32) + e = binary.Write(w, binary.BigEndian, uint32(len(ef.observed))) + if e != nil { + F.Ln("failed to write:", e) + } + for _, ot := range ots { + ot.Serialize(w) + observed[ot] = txCount + txCount++ + } + // Save all the right bins. + for _, list := range ef.bin { + e = binary.Write(w, binary.BigEndian, uint32(len(list))) + if e != nil { + F.Ln("failed to write:", e) + } + for _, o := range list { + e = binary.Write(w, binary.BigEndian, observed[o]) + if e != nil { + F.Ln("failed to write:", e) + } + } + } + // Dropped transactions. + e = binary.Write(w, binary.BigEndian, uint32(len(ef.dropped))) + if e != nil { + F.Ln("failed to write:", e) + } + for _, registered := range ef.dropped { + registered.serialize(w, observed) + } + // Commit the tx and return. + return w.Bytes() +} + +// estimates returns the set of all fee estimates from 1 to estimateFeeDepth confirmations from now. +func (ef *FeeEstimator) estimates() []SatoshiPerByte { + set := ef.newEstimateFeeSet() + estimates := make([]SatoshiPerByte, estimateFeeDepth) + for i := 0; i < estimateFeeDepth; i++ { + estimates[i] = set.estimateFee(i + 1) + } + return estimates +} + +// newEstimateFeeSet creates a temporary data structure that can be used to find all fee estimates. +func (ef *FeeEstimator) newEstimateFeeSet() *estimateFeeSet { + set := &estimateFeeSet{} + capacity := 0 + for i, b := range ef.bin { + l := len(b) + set.bin[i] = uint32(l) + capacity += l + } + set.feeRate = make([]SatoshiPerByte, capacity) + i := 0 + for _, b := range ef.bin { + for _, o := range b { + set.feeRate[i] = o.feeRate + i++ + } + } + sort.Sort(set) + return set +} + +// rollback rolls back the effect of the last block in the stack of registered blocks. +func (ef *FeeEstimator) rollback() { + // The previous sorted list is invalid, so delete it. + ef.cached = nil + // pop the last list of dropped txs from the stack. + last := len(ef.dropped) - 1 + if last == -1 { + // Cannot really happen because the exported calling function only rolls back a block already known to be in the + // list of dropped transactions. + return + } + dropped := ef.dropped[last] + // where we are in each bin as we replace txs? + var replacementCounters [estimateFeeDepth]int + // Go through the txs in the dropped block. + for _, o := range dropped.transactions { + // Which bin was this tx in? + blocksToConfirm := o.mined - o.observed - 1 + bin := ef.bin[blocksToConfirm] + var counter = replacementCounters[blocksToConfirm] + // Continue to go through that bin where we left off. + for { + if counter >= len(bin) { + // Panic, as we have entered an unrecoverable invalid state. + panic( + errors.New( + "illegal state: cannot rollback dropped transaction", + ), + ) + } + prev := bin[counter] + if prev.mined == ef.lastKnownHeight { + prev.mined = mining.UnminedHeight + bin[counter] = o + counter++ + break + } + counter++ + } + replacementCounters[blocksToConfirm] = counter + } + // Continue going through bins to find other txs to remove which did not replace any other when they were entered. + for i, j := range replacementCounters { + for { + l := len(ef.bin[i]) + if j >= l { + break + } + prev := ef.bin[i][j] + if prev.mined == ef.lastKnownHeight { + prev.mined = mining.UnminedHeight + newBin := append(ef.bin[i][0:j], ef.bin[i][j+1:l]...) + // TODO This line should prevent an unintentional memory leak + // but it causes a panic when it is uncommented. + // ef.bin[i][j] = nil + ef.bin[i] = newBin + continue + } + j++ + } + } + ef.dropped = ef.dropped[0:last] + // The number of blocks the fee estimator has seen is decremented. + ef.numBlocksRegistered-- + ef.lastKnownHeight-- +} +func (b *estimateFeeSet) Len() int { return len(b.feeRate) } +func (b *estimateFeeSet) Less(i, j int) bool { return b.feeRate[i] > b.feeRate[j] } +func (b *estimateFeeSet) Swap(i, j int) { + b.feeRate[i], b.feeRate[j] = b.feeRate[j], b.feeRate[i] +} + +// estimateFee returns the estimated fee for a transaction to confirm in confirmations blocks from now, given the data +// set we have collected. +func (b *estimateFeeSet) estimateFee(confirmations int) SatoshiPerByte { + if confirmations <= 0 { + return SatoshiPerByte(math.Inf(1)) + } + if confirmations > estimateFeeDepth { + return 0 + } + // We don't have any transactions! + if len(b.feeRate) == 0 { + return 0 + } + var min, max int + for i := 0; i < confirmations-1; i++ { + min += int(b.bin[i]) + } + max = min + int(b.bin[confirmations-1]) - 1 + if max < min { + max = min + } + feeIndex := (min + max) / 2 + if feeIndex >= len(b.feeRate) { + feeIndex = len(b.feeRate) - 1 + } + return b.feeRate[feeIndex] +} +func (o *observedTransaction) Serialize(w io.Writer) { + e := binary.Write(w, binary.BigEndian, o.hash) + if e != nil { + F.Ln("failed to serialize observed transaction:", e) + } + e = binary.Write(w, binary.BigEndian, o.feeRate) + if e != nil { + F.Ln("failed to serialize observed transaction:", e) + } + e = binary.Write(w, binary.BigEndian, o.observed) + if e != nil { + F.Ln("failed to serialize observed transaction:", e) + } + e = binary.Write(w, binary.BigEndian, o.mined) + if e != nil { + F.Ln("failed to serialize observed transaction:", e) + } +} + +func (rb *registeredBlock) serialize( + w io.Writer, + txs map[*observedTransaction]uint32, +) { + e := binary.Write(w, binary.BigEndian, rb.hash) + if e != nil { + F.Ln("failed to write:", e) + } + e = binary.Write(w, binary.BigEndian, uint32(len(rb.transactions))) + if e != nil { + F.Ln("failed to write:", e) + } + for _, o := range rb.transactions { + e = binary.Write(w, binary.BigEndian, txs[o]) + if e != nil { + F.Ln("failed to write:", e) + } + } +} + +// Fee returns the fee for a transaction of a given size for the given fee rate. +func (rate SatoshiPerByte) Fee(size uint32) amt.Amount { + // If our rate is the error value, return that. + if rate == SatoshiPerByte(-1) { + return amt.Amount(-1) + } + return amt.Amount(float64(rate) * float64(size)) +} + +// ToBtcPerKb returns a float value that represents the given SatoshiPerByte converted to satoshis per kb. +func (rate SatoshiPerByte) ToBtcPerKb() DUOPerKilobyte { + // If our rate is the error value, return that. + if rate == SatoshiPerByte(-1.0) { + return -1.0 + } + return DUOPerKilobyte(float64(rate) * bytePerKb * duoPerSatoshi) +} + +func (q observedTxSet) Len() int { return len(q) } +func (q observedTxSet) Less(i, j int) bool { + return strings.Compare(q[i].hash.String(), q[j].hash.String()) < 0 +} +func (q observedTxSet) Swap(i, j int) { q[i], q[j] = q[j], q[i] } + +// NewFeeEstimator creates a FeeEstimator for which at most maxRollback blocks can be unregistered and which returns an +// error unless minRegisteredBlocks have been registered with it. +func NewFeeEstimator(maxRollback, minRegisteredBlocks uint32) *FeeEstimator { + return &FeeEstimator{ + maxRollback: maxRollback, + minRegisteredBlocks: minRegisteredBlocks, + lastKnownHeight: mining.UnminedHeight, + binSize: estimateFeeBinSize, + maxReplacements: estimateFeeMaxReplacements, + observed: make(map[chainhash.Hash]*observedTransaction), + dropped: make([]*registeredBlock, 0, maxRollback), + } +} + +// NewSatoshiPerByte creates a SatoshiPerByte from an Amount and a size in bytes. +func NewSatoshiPerByte(fee amt.Amount, size uint32) SatoshiPerByte { + return SatoshiPerByte(float64(fee) / float64(size)) +} + +// RestoreFeeEstimator takes a FeeEstimatorState that was previously returned by Save and restores it to a FeeEstimator +func RestoreFeeEstimator(data FeeEstimatorState) (*FeeEstimator, error) { + r := bytes.NewReader(data) + // Chk version + var version uint32 + e := binary.Read(r, binary.BigEndian, &version) + if e != nil { + return nil, e + } + if version != estimateFeeSaveVersion { + return nil, fmt.Errorf( + "incorrect version: expected %d found %d", + estimateFeeSaveVersion, version, + ) + } + ef := &FeeEstimator{ + observed: make(map[chainhash.Hash]*observedTransaction), + } + // Read basic parameters. + e = binary.Read(r, binary.BigEndian, &ef.maxRollback) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &ef.binSize) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &ef.maxReplacements) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &ef.minRegisteredBlocks) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &ef.lastKnownHeight) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &ef.numBlocksRegistered) + if e != nil { + F.Ln("failed to read", e) + } + // Read transactions. + var numObserved uint32 + observed := make(map[uint32]*observedTransaction) + e = binary.Read(r, binary.BigEndian, &numObserved) + if e != nil { + F.Ln("failed to read", e) + } + for i := uint32(0); i < numObserved; i++ { + var ot *observedTransaction + ot, e = deserializeObservedTransaction(r) + if e != nil { + return nil, e + } + observed[i] = ot + ef.observed[ot.hash] = ot + } + // Read bins. + for i := 0; i < estimateFeeDepth; i++ { + var numTransactions uint32 + e = binary.Read(r, binary.BigEndian, &numTransactions) + if e != nil { + F.Ln("failed to read", e) + } + bin := make([]*observedTransaction, numTransactions) + for j := uint32(0); j < numTransactions; j++ { + var index uint32 + e = binary.Read(r, binary.BigEndian, &index) + if e != nil { + F.Ln("failed to read", e) + } + var exists bool + bin[j], exists = observed[index] + if !exists { + return nil, fmt.Errorf( + "invalid transaction reference %d", + index, + ) + } + } + ef.bin[i] = bin + } + // Read dropped transactions. + var numDropped uint32 + e = binary.Read(r, binary.BigEndian, &numDropped) + if e != nil { + F.Ln("failed to read", e) + } + ef.dropped = make([]*registeredBlock, numDropped) + for i := uint32(0); i < numDropped; i++ { + var e error + ef.dropped[int(i)], e = deserializeRegisteredBlock(r, observed) + if e != nil { + return nil, e + } + } + return ef, nil +} +func deserializeObservedTransaction(r io.Reader) (*observedTransaction, error) { + ot := observedTransaction{} + // The first 32 bytes should be a hash. + e := binary.Read(r, binary.BigEndian, &ot.hash) + if e != nil { + F.Ln("failed to read", e) + } + // The next 8 are SatoshiPerByte + e = binary.Read(r, binary.BigEndian, &ot.feeRate) + if e != nil { + F.Ln("failed to read", e) + } + // And next there are two uint32's. + e = binary.Read(r, binary.BigEndian, &ot.observed) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &ot.mined) + if e != nil { + F.Ln("failed to read", e) + } + return &ot, nil +} +func deserializeRegisteredBlock( + r io.Reader, + txs map[uint32]*observedTransaction, +) (*registeredBlock, error) { + var lenTransactions uint32 + rb := ®isteredBlock{} + e := binary.Read(r, binary.BigEndian, &rb.hash) + if e != nil { + F.Ln("failed to read", e) + } + e = binary.Read(r, binary.BigEndian, &lenTransactions) + if e != nil { + F.Ln("failed to read", e) + } + rb.transactions = make([]*observedTransaction, lenTransactions) + for i := uint32(0); i < lenTransactions; i++ { + var index uint32 + e = binary.Read(r, binary.BigEndian, &index) + if e != nil { + F.Ln("failed to read", e) + } + rb.transactions[i] = txs[index] + } + return rb, nil +} diff --git a/pkg/mempool/estimatefee_test.go b/pkg/mempool/estimatefee_test.go new file mode 100644 index 0000000..9f51ee1 --- /dev/null +++ b/pkg/mempool/estimatefee_test.go @@ -0,0 +1,403 @@ +package mempool + +import ( + "bytes" + "github.com/p9c/p9/pkg/amt" + block2 "github.com/p9c/p9/pkg/block" + "math/rand" + "testing" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/mining" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// estimateFeeTester interacts with the FeeEstimator to keep track of its expected state. +type estimateFeeTester struct { + ef *FeeEstimator + t *testing.T + version int32 + height int32 + last *lastBlock +} + +// lastBlock is a linked list of the block hashes which have been processed by the test FeeEstimator. +type lastBlock struct { + hash *chainhash.Hash + prev *lastBlock +} + +func (eft *estimateFeeTester) checkSaveAndRestore( + previousEstimates [estimateFeeDepth]DUOPerKilobyte, +) { + // Get the save state. + save := eft.ef.Save() + // Save and restore database. + var e error + eft.ef, e = RestoreFeeEstimator(save) + if e != nil { + eft.t.Fatalf("Could not restore database: %s", e) + } + // Save again and check that it matches the previous one. + redo := eft.ef.Save() + if !bytes.Equal(save, redo) { + eft.t.Fatalf("Restored states do not match: %v %v", save, redo) + } + // Chk that the results match. + newEstimates := eft.estimates() + for i, prev := range previousEstimates { + if prev != newEstimates[i] { + eft.t.Error( + "Mismatch in estimate ", i, " after restore; got ", + newEstimates[i], " but expected ", prev, + ) + } + } +} + +func (eft *estimateFeeTester) estimates() [estimateFeeDepth]DUOPerKilobyte { + // Generate estimates + var estimates [estimateFeeDepth]DUOPerKilobyte + for i := 0; i < estimateFeeDepth; i++ { + estimates[i], _ = eft.ef.EstimateFee(uint32(i + 1)) + } + // Chk that all estimated fee results go in descending order. + for i := 1; i < estimateFeeDepth; i++ { + if estimates[i] > estimates[i-1] { + eft.t.Error( + "Estimates not in descending order; got ", + estimates[i], " for estimate ", i, " and ", estimates[i-1], + " for ", i-1, + ) + panic("invalid state.") + } + } + return estimates +} + +func (eft *estimateFeeTester) newBlock(txs []*wire.MsgTx) { + eft.height++ + block := block2.NewBlock(&wire.Block{Transactions: txs}) + block.SetHeight(eft.height) + eft.last = &lastBlock{block.Hash(), eft.last} + e := eft.ef.RegisterBlock(block) + if e != nil { + W.Ln("failed to register block:", e) + } +} + +func (eft *estimateFeeTester) rollback() { + if eft.last == nil { + return + } + e := eft.ef.Rollback(eft.last.hash) + if e != nil { + eft.t.Errorf("Could not rollback: %v", e) + } + eft.height-- + eft.last = eft.last.prev +} + +func (eft *estimateFeeTester) round( + txHistory [][]*TxDesc, + estimateHistory [][estimateFeeDepth]DUOPerKilobyte, txPerRound, + txPerBlock uint32, +) ([][]*TxDesc, [][estimateFeeDepth]DUOPerKilobyte) { + // generate new txs. + var newTxs []*TxDesc + for i := uint32(0); i < txPerRound; i++ { + newTx := eft.testTx(amt.Amount(rand.Intn(1000000))) + eft.ef.ObserveTransaction(newTx) + newTxs = append(newTxs, newTx) + } + // Generate mempool. + mempool := make(map[*observedTransaction]*TxDesc) + for _, h := range txHistory { + for _, t := range h { + if o, exists := eft.ef.observed[*t.Tx.Hash()]; exists && o. + mined == mining.UnminedHeight { + mempool[o] = t + } + } + } + // generate new block, with no duplicates. + i := uint32(0) + newBlockList := make([]*wire.MsgTx, 0, txPerBlock) + for _, t := range mempool { + newBlockList = append(newBlockList, t.TxDesc.Tx.MsgTx()) + i++ + if i == txPerBlock { + break + } + } + // Register a new block. + eft.newBlock(newBlockList) + // return results. + estimates := eft.estimates() + // Return results + return append(txHistory, newTxs), append(estimateHistory, estimates) +} + +func ( +eft *estimateFeeTester, +) testTx( + fee amt.Amount, +) *TxDesc { + eft.version++ + return &TxDesc{ + TxDesc: mining.TxDesc{ + Tx: util.NewTx( + &wire.MsgTx{ + Version: eft.version, + }, + ), + Height: eft.height, + Fee: int64(fee), + }, + StartingPriority: 0, + } +} + +// TestSave tests saving and restoring to a []byte. +func TestDatabase(t *testing.T) { + txPerRound := uint32(7) + txPerBlock := uint32(5) + binSize := uint32(6) + maxReplacements := uint32(4) + rounds := 8 + eft := estimateFeeTester{ef: newTestFeeEstimator(binSize, maxReplacements, uint32(rounds)+1), t: t} + var txHistory [][]*TxDesc + estimateHistory := [][estimateFeeDepth]DUOPerKilobyte{eft.estimates()} + for round := 0; round < rounds; round++ { + eft.checkSaveAndRestore(estimateHistory[len(estimateHistory)-1]) + // Go forward one step. + txHistory, estimateHistory = + eft.round(txHistory, estimateHistory, txPerRound, txPerBlock) + } + // Reverse the process and try again. + for round := 1; round <= rounds; round++ { + eft.rollback() + eft.checkSaveAndRestore(estimateHistory[len(estimateHistory)-round-1]) + } +} + +// TestEstimateFee tests basic functionality in the FeeEstimator. +func TestEstimateFee(t *testing.T) { + ef := newTestFeeEstimator(5, 3, 1) + eft := estimateFeeTester{ef: ef, t: t} + // Try with no txs and get zero for all queries. + expected := DUOPerKilobyte(0.0) + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f when estimator is"+ + " empty; got %f", expected, estimated, + ) + } + } + // Now insert a tx. + tx := eft.testTx(1000000) + ef.ObserveTransaction(tx) + // Expected should still be zero because this is still in the mempool. + expected = DUOPerKilobyte(0.0) + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f when estimator has one"+ + " tx in mempool; got %f", expected, estimated, + ) + } + } + // Change minRegisteredBlocks to make sure that works. Error return value expected. + ef.minRegisteredBlocks = 1 + expected = DUOPerKilobyte(-1.0) + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f before any blocks have"+ + " been registered; got %f", expected, estimated, + ) + } + } + // Record a block with the new tx. + eft.newBlock([]*wire.MsgTx{tx.Tx.MsgTx()}) + expected = expectedFeePerKilobyte(tx) + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f when one tx is binned"+ + "; got %f", expected, estimated, + ) + } + } + // Roll back the last block; this was an orphan block. + ef.minRegisteredBlocks = 0 + eft.rollback() + expected = DUOPerKilobyte(0.0) + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f after rolling back"+ + " block; got %f", expected, estimated, + ) + } + } + // Record an empty block and then a block with the new tx. This test was made because of a bug that only appeared + // when there were no transactions in the first bin. + eft.newBlock([]*wire.MsgTx{}) + eft.newBlock([]*wire.MsgTx{tx.Tx.MsgTx()}) + expected = expectedFeePerKilobyte(tx) + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f when one tx is binned"+ + "; got %f", expected, estimated, + ) + } + } + // Create some more transactions. + txA := eft.testTx(500000) + txB := eft.testTx(2000000) + txC := eft.testTx(4000000) + ef.ObserveTransaction(txA) + ef.ObserveTransaction(txB) + ef.ObserveTransaction(txC) + // Record 7 empty blocks. + for i := 0; i < 7; i++ { + eft.newBlock([]*wire.MsgTx{}) + } + // Mine the first tx. + eft.newBlock([]*wire.MsgTx{txA.Tx.MsgTx()}) + // Now the estimated amount should depend on the value of the argument to estimate fee. + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + if i > 2 { + expected = expectedFeePerKilobyte(txA) + } else { + expected = expectedFeePerKilobyte(tx) + } + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f on round %d; got %f", + expected, i, estimated, + ) + } + } + // Record 5 more empty blocks. + for i := 0; i < 5; i++ { + eft.newBlock([]*wire.MsgTx{}) + } + // Mine the next tx. + eft.newBlock([]*wire.MsgTx{txB.Tx.MsgTx()}) + // Now the estimated amount should depend on the value of the argument to estimate fee. + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + switch { + case i <= 2: + expected = expectedFeePerKilobyte(txB) + case i <= 8: + expected = expectedFeePerKilobyte(tx) + default: + expected = expectedFeePerKilobyte(txA) + } + if estimated != expected { + t.Errorf( + "Estimate fee error: expected %f on round %d; got %f", + expected, i, estimated, + ) + } + } + // Record 9 more empty blocks. + for i := 0; i < 10; i++ { + eft.newBlock([]*wire.MsgTx{}) + } + // Mine txC. + eft.newBlock([]*wire.MsgTx{txC.Tx.MsgTx()}) + // This should have no effect on the outcome because too many blocks have been mined for txC to be recorded. + for i := uint32(1); i <= estimateFeeDepth; i++ { + estimated, _ := ef.EstimateFee(i) + switch { + case i <= 2: + expected = expectedFeePerKilobyte(txC) + case i <= 8: + expected = expectedFeePerKilobyte(txB) + case i <= 8+6: + expected = expectedFeePerKilobyte(tx) + default: + expected = expectedFeePerKilobyte(txA) + } + if estimated != expected { + t.Errorf("Estimate fee error: expected %f on round %d; got %f", expected, i, estimated) + } + } +} + +// TestEstimateFeeRollback tests the rollback function, which undoes the effect of a adding a new block. +func TestEstimateFeeRollback(t *testing.T) { + txPerRound := uint32(7) + txPerBlock := uint32(5) + binSize := uint32(6) + maxReplacements := uint32(4) + stepsBack := 2 + rounds := 30 + eft := estimateFeeTester{ + ef: newTestFeeEstimator( + binSize, + maxReplacements, uint32(stepsBack), + ), t: t, + } + var txHistory [][]*TxDesc + estimateHistory := [][estimateFeeDepth]DUOPerKilobyte{eft.estimates()} + for round := 0; round < rounds; round++ { + // Go forward a few rounds. + for step := 0; step <= stepsBack; step++ { + txHistory, estimateHistory = + eft.round(txHistory, estimateHistory, txPerRound, txPerBlock) + } + // Now go back. + for step := 0; step < stepsBack; step++ { + eft.rollback() + // After rolling back we should have the same estimated fees as before. + expected := estimateHistory[len(estimateHistory)-step-2] + estimates := eft.estimates() + // Ensure that these are both the same. + for i := 0; i < estimateFeeDepth; i++ { + if expected[i] != estimates[i] { + t.Errorf( + "Rollback value mismatch. Expected %f, got %f. ", + expected[i], estimates[i], + ) + return + } + } + } + // Erase history. + txHistory = txHistory[0 : len(txHistory)-stepsBack] + estimateHistory = estimateHistory[0 : len(estimateHistory)-stepsBack] + } +} +func expectedFeePerKilobyte(t *TxDesc) DUOPerKilobyte { + size := float64(t.TxDesc.Tx.MsgTx().SerializeSize()) + fee := float64(t.TxDesc.Fee) + return SatoshiPerByte(fee / size).ToBtcPerKb() +} + +// newTestFeeEstimator creates a feeEstimator with some different parameters for testing purposes. +func newTestFeeEstimator(binSize, maxReplacements, maxRollback uint32) *FeeEstimator { + return &FeeEstimator{ + maxRollback: maxRollback, + lastKnownHeight: 0, + binSize: int32(binSize), + minRegisteredBlocks: 0, + maxReplacements: int32(maxReplacements), + observed: make(map[chainhash.Hash]*observedTransaction), + dropped: make([]*registeredBlock, 0, maxRollback), + } +} diff --git a/pkg/mempool/log.go b/pkg/mempool/log.go new file mode 100644 index 0000000..5108bf1 --- /dev/null +++ b/pkg/mempool/log.go @@ -0,0 +1,43 @@ +package mempool + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/mempool/mempool.go b/pkg/mempool/mempool.go new file mode 100644 index 0000000..22dc267 --- /dev/null +++ b/pkg/mempool/mempool.go @@ -0,0 +1,1038 @@ +package mempool + +import ( + "container/list" + "errors" + "fmt" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/constant" + "github.com/p9c/p9/pkg/log" + "math" + "sync" + "sync/atomic" + "time" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/hardfork" + "github.com/p9c/p9/pkg/indexers" + "github.com/p9c/p9/pkg/mining" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/wire" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/util" +) + +// Config is a descriptor containing the memory pool configuration. +type Config struct { + // Policy defines the various mempool configuration options related to policy. + Policy Policy + // ChainParams identifies which chain parameters the txpool is associated with. + ChainParams *chaincfg.Params + // FetchUtxoView defines the function to use to fetch unspent transaction output information. + FetchUtxoView func(*util.Tx) (*blockchain.UtxoViewpoint, error) + // BestHeight defines the function to use to access the block height of the current best chain. + BestHeight func() int32 + // MedianTimePast defines the function to use in order to access the median time past calculated from the + // point-of-view of the current chain tip within the best chain. + MedianTimePast func() time.Time + // // CalcSequenceLock defines the function to use in order to generate the current sequence lock for the given + // // transaction using the passed utxo view. + // CalcSequenceLock func(*util.Tx, *blockchain.UtxoViewpoint) (*blockchain.SequenceLock, error) + // IsDeploymentActive returns true if the target deploymentID is active, and false otherwise. The mempool uses + // this function to gauge if transactions using new to be soft-forked rules should be allowed into the mempool + // or not. + IsDeploymentActive func(deploymentID uint32) (bool, error) + // SigCache defines a signature cache to use. + SigCache *txscript.SigCache + // HashCache defines the transaction hash mid-state cache to use. + HashCache *txscript.HashCache + // AddrIndex defines the optional address index instance to use for indexing the unconfirmed transactions in the + // memory pool. This can be nil if the address index is not enabled. + AddrIndex *indexers.AddrIndex + // FeeEstimatator provides a feeEstimator. If it is not nil, the mempool records all new transactions it + // observes into the feeEstimator. + FeeEstimator *FeeEstimator + // UpdateHook is a function that is called when transactions are added or + // removed from the mempool + UpdateHook func() +} + +// Policy houses the policy (configuration parameters) that is used to control the mempool. +type Policy struct { + // MaxTxVersion is the transaction version that the mempool should accept. All transactions above this version are + // rejected as non-standard. + MaxTxVersion int32 + // DisableRelayPriority defines whether to relay free or low-fee transactions that do not have enough priority to be + // relayed. + DisableRelayPriority bool + // AcceptNonStd defines whether to accept non-standard transactions. If true, non-standard transactions will be + // accepted into the mempool. Otherwise, all non-standard transactions will be rejected. + AcceptNonStd bool + // FreeTxRelayLimit defines the given amount in thousands of bytes per minute that transactions with no fee are rate + // limited to. + FreeTxRelayLimit float64 + // MaxOrphanTxs is the maximum number of orphan transactions that can be queued. + MaxOrphanTxs int + // MaxOrphanTxSize is the maximum size allowed for orphan transactions. This helps prevent memory exhaustion attacks + // from sending a lot of of big orphans. + MaxOrphanTxSize int + // MaxSigOpCostPerTx is the cumulative maximum cost of all the signature operations in a single transaction we will + // relay or mine. It is a fraction of the max signature operations for a block. + MaxSigOpCostPerTx int + // MinRelayTxFee defines the minimum transaction fee in DUO/kB to be considered a non-zero fee. + MinRelayTxFee amt.Amount +} + +// Tag represents an identifier to use for tagging orphan transactions. The caller may choose any scheme it desires +// however it is common to use peer IDs so that orphans can be identified by which peer first relayed them. +type Tag uint64 + +// TxDesc is a descriptor containing a transaction in the mempool along with additional metadata. +type TxDesc struct { + mining.TxDesc + // StartingPriority is the priority of the transaction when it was added to the pool. + StartingPriority float64 +} + +// TxPool is used as a source of transactions that need to be mined into blocks and relayed to other peers. It is safe +// for concurrent access from multiple peers. +type TxPool struct { + // The following variables must only be used atomically. + lastUpdated int64 // last time pool was updated + mtx sync.RWMutex + cfg Config + pool map[chainhash.Hash]*TxDesc + orphans map[chainhash.Hash]*orphanTx + orphansByPrev map[wire.OutPoint]map[chainhash.Hash]*util.Tx + outpoints map[wire.OutPoint]*util.Tx + pennyTotal float64 // exponentially decaying total for penny spends. + lastPennyUnix int64 // unix time of last ``penny spend'' + // nextExpireScan is the time after which the orphan pool will be scanned in order to evict orphans. This is NOT + // a hard deadline as the scan will only run when an orphan is added to the pool as opposed to on an + // unconditional timer. + nextExpireScan time.Time + updateHook func() +} + +// orphanTx is normal transaction that references an ancestor transaction that is not yet available. It also contains +// additional information related to it such as an expiration time to help prevent caching the orphan forever. +type orphanTx struct { + tx *util.Tx + tag Tag + expiration time.Time +} + +const ( + // orphanTTL is the maximum amount of time an orphan is allowed to stay in the orphan pool before it expires and is + // evicted during the next scan. + orphanTTL = time.Minute * 15 + // orphanExpireScanInterval is the minimum amount of time in between + // scans of the orphan pool to evict expired transactions. + orphanExpireScanInterval = time.Minute * 5 +) + +var // Ensure the TxPool type implements the mining.TxSource interface. + _ mining.TxSource = (*TxPool)(nil) + +// CheckSpend checks whether the passed outpoint is already spent by a transaction in the mempool. If that's the case +// the spending transaction will be returned, if not nil will be returned. +func (mp *TxPool) CheckSpend(op wire.OutPoint) *util.Tx { + mp.mtx.RLock() + txR := mp.outpoints[op] + mp.mtx.RUnlock() + return txR +} + +// Count returns the number of transactions in the main pool. It does not include the orphan pool. This function is safe +// for concurrent access. +func (mp *TxPool) Count() int { + mp.mtx.RLock() + count := len(mp.pool) + mp.mtx.RUnlock() + return count +} + +// FetchTransaction returns the requested transaction from the transaction pool. This only fetches from the main +// transaction pool and does not include orphans. This function is safe for concurrent access. +func (mp *TxPool) FetchTransaction(txHash *chainhash.Hash) (*util.Tx, error) { + // Protect concurrent access. + mp.mtx.RLock() + txDesc, exists := mp.pool[*txHash] + mp.mtx.RUnlock() + if exists { + return txDesc.Tx, nil + } + return nil, fmt.Errorf("transaction is not in the pool") +} + +// HaveTransaction returns whether or not the passed transaction already exists in the main pool or in the orphan pool. +// This function is safe for concurrent access. +func (mp *TxPool) HaveTransaction(hash *chainhash.Hash) bool { + // Protect concurrent access. + mp.mtx.RLock() + haveTx := mp.haveTransaction(hash) + mp.mtx.RUnlock() + return haveTx +} + +// IsOrphanInPool returns whether or not the passed transaction already exists in the orphan pool. This function is safe +// for concurrent access. +func (mp *TxPool) IsOrphanInPool(hash *chainhash.Hash) bool { + // Protect concurrent access. + mp.mtx.RLock() + inPool := mp.isOrphanInPool(hash) + mp.mtx.RUnlock() + return inPool +} + +// IsTransactionInPool returns whether or not the passed transaction already exists in the main pool. This function is +// safe for concurrent access. +func (mp *TxPool) IsTransactionInPool(hash *chainhash.Hash) bool { + // Protect concurrent access. + mp.mtx.RLock() + inPool := mp.isTransactionInPool(hash) + mp.mtx.RUnlock() + return inPool +} + +// LastUpdated returns the last time a transaction was added to or removed from the main pool. It does not include the +// orphan pool. This function is safe for concurrent access. +func (mp *TxPool) LastUpdated() time.Time { + return time.Unix(atomic.LoadInt64(&mp.lastUpdated), 0) +} + +// MaybeAcceptTransaction is the main workhorse for handling insertion of new free-standing transactions into a memory +// pool. It includes functionality such as rejecting duplicate transactions, ensuring transactions follow all rules, +// detecting orphan transactions, and insertion into the memory pool. If the transaction is an orphan ( missing parent +// transactions) the transaction is NOT added to the orphan pool but each unknown referenced parent is returned. Use +// ProcessTransaction instead if new orphans should be added to the orphan pool. This function is safe for concurrent +// access. +func (mp *TxPool) MaybeAcceptTransaction( + b *blockchain.BlockChain, + tx *util.Tx, isNew, rateLimit bool, +) (hashes []*chainhash.Hash, txD *TxDesc, e error) { + // Protect concurrent access. + mp.mtx.Lock() + hashes, txD, e = mp.maybeAcceptTransaction(b, tx, isNew, rateLimit, true) + mp.mtx.Unlock() + return hashes, txD, e +} + +// MiningDescs returns a slice of mining descriptors for all the transactions in the pool. This is part of the mining. +// TxSource interface implementation and is safe for concurrent access as required by the interface contract. +func (mp *TxPool) MiningDescs() []*mining.TxDesc { + mp.mtx.RLock() + descs := make([]*mining.TxDesc, len(mp.pool)) + i := 0 + for _, desc := range mp.pool { + descs[i] = &desc.TxDesc + i++ + } + mp.mtx.RUnlock() + return descs +} + +// ProcessOrphans determines if there are any orphans which depend on the passed transaction hash (it is possible that +// they are no longer orphans) and potentially accepts them to the memory pool. It repeats the process for the newly +// accepted transactions (to detect further orphans which may no longer be orphans) until there are no more. It returns +// a slice of transactions added to the mempool. A nil slice means no transactions were moved from the orphan pool to +// the mempool. This function is safe for concurrent access. +func (mp *TxPool) ProcessOrphans(b *blockchain.BlockChain, acceptedTx *util.Tx) []*TxDesc { + mp.mtx.Lock() + acceptedTxns := mp.processOrphans(b, acceptedTx) + mp.mtx.Unlock() + return acceptedTxns +} + +// ProcessTransaction is the main workhorse for handling insertion of new free-standing transactions into the memory +// pool. It includes functionality such as rejecting duplicate transactions, ensuring transactions follow all rules, +// orphan transaction handling, and insertion into the memory pool. It returns a slice of transactions added to the +// mempool. When the error is nil the list will include the passed transaction itself along with any additional orphan +// transactions that were added as a result of the passed one being accepted. This function is safe for concurrent +// access. +func (mp *TxPool) ProcessTransaction( + b *blockchain.BlockChain, tx *util.Tx, + allowOrphan, rateLimit bool, tag Tag, +) ([]*TxDesc, error) { + D.Ln("processing transaction", tx.Hash()) + // Protect concurrent access. + mp.mtx.Lock() + defer mp.mtx.Unlock() + // Potentially accept the transaction to the memory pool. + missingParents, txD, e := mp.maybeAcceptTransaction( + b, tx, true, + rateLimit, true, + ) + if e != nil { + return nil, e + } + if len(missingParents) == 0 { + // Accept any orphan transactions that depend on this transaction ( they may no longer be orphans if all inputs + // are now available) and repeat for those accepted transactions until there are no more. + newTxs := mp.processOrphans(b, tx) + acceptedTxs := make([]*TxDesc, len(newTxs)+1) + // Add the parent transaction first so remote nodes do not add orphans. + acceptedTxs[0] = txD + copy(acceptedTxs[1:], newTxs) + return acceptedTxs, nil + } + // The transaction is an orphan (has inputs missing). Reject it if the flag to allow orphans is not set. + if !allowOrphan { + // Only use the first missing parent transaction in the error message. NOTE: RejectDuplicate is really not an + // accurate reject code here, but it matches the reference implementation and there isn't a better choice due to + // the limited number of reject codes. Missing inputs is assumed to mean they are already spent which is not + // really always the case. + str := fmt.Sprintf( + "orphan transaction %v references outputs of"+ + " unknown or fully-spent transaction %v", tx.Hash(), + missingParents[0], + ) + return nil, txRuleError(wire.RejectDuplicate, str) + } + // Potentially add the orphan transaction to the orphan pool. + e = mp.maybeAddOrphan(tx, tag) + return nil, e +} + +// RawMempoolVerbose returns all of the entries in the mempool as a fully populated json result. This function is safe +// for concurrent access. +func (mp *TxPool) RawMempoolVerbose() map[string]*btcjson.GetRawMempoolVerboseResult { + mp.mtx.RLock() + defer mp.mtx.RUnlock() + result := make(map[string]*btcjson.GetRawMempoolVerboseResult, len(mp.pool)) + bestHeight := mp.cfg.BestHeight() + for _, desc := range mp.pool { + // Calculate the current priority based on the inputs to the transaction. Use zero if one or more of the input + // transactions can't be found for some reason. + tx := desc.Tx + var currentPriority float64 + utxos, e := mp.fetchInputUtxos(tx) + if e == nil { + currentPriority = mining.CalcPriority( + tx.MsgTx(), utxos, + bestHeight+1, + ) + } + mpd := &btcjson.GetRawMempoolVerboseResult{ + Size: int32(tx.MsgTx().SerializeSize()), + VSize: int32(GetTxVirtualSize(tx)), + Fee: amt.Amount(desc.Fee).ToDUO(), + Time: desc.Added.Unix(), + Height: int64(desc.Height), + StartingPriority: desc.StartingPriority, + CurrentPriority: currentPriority, + Depends: make([]string, 0), + } + for _, txIn := range tx.MsgTx().TxIn { + hash := &txIn.PreviousOutPoint.Hash + if mp.haveTransaction(hash) { + mpd.Depends = append( + mpd.Depends, + hash.String(), + ) + } + } + result[tx.Hash().String()] = mpd + } + return result +} + +// RemoveDoubleSpends removes all transactions which spend outputs spent by the passed transaction from the memory pool. +// Removing those transactions then leads to removing all transactions which rely on them, recursively. This is +// necessary when a block is connected to the main chain because the block may contain transactions which were +// previously unknown to the memory pool. This function is safe for concurrent access. +func (mp *TxPool) RemoveDoubleSpends(tx *util.Tx) { + // Protect concurrent access. + mp.mtx.Lock() + for _, txIn := range tx.MsgTx().TxIn { + if txRedeemer, ok := mp.outpoints[txIn.PreviousOutPoint]; ok { + if !txRedeemer.Hash().IsEqual(tx.Hash()) { + mp.removeTransaction(txRedeemer, true) + } + } + } + mp.mtx.Unlock() +} + +// RemoveOrphan removes the passed orphan transaction from the orphan pool and previous orphan index. This function is +// safe for concurrent access. +func (mp *TxPool) RemoveOrphan(tx *util.Tx) { + mp.mtx.Lock() + mp.removeOrphan(tx, false) + mp.mtx.Unlock() +} + +// RemoveOrphansByTag removes all orphan transactions tagged with the provided identifier. This function is safe for +// concurrent access. +func (mp *TxPool) RemoveOrphansByTag(tag Tag) uint64 { + var numEvicted uint64 + mp.mtx.Lock() + for _, otx := range mp.orphans { + if otx.tag == tag { + mp.removeOrphan(otx.tx, true) + numEvicted++ + } + } + mp.mtx.Unlock() + return numEvicted +} + +// RemoveTransaction removes the passed transaction from the mempool. When the removeRedeemers flag is set any +// transactions that redeem outputs from the removed transaction will also be removed recursively from the mempool, as +// they would otherwise become orphans. This function is safe for concurrent access. +func (mp *TxPool) RemoveTransaction(tx *util.Tx, removeRedeemers bool) { + // Protect concurrent access. + mp.mtx.Lock() + mp.removeTransaction(tx, removeRedeemers) + mp.mtx.Unlock() +} + +// TxDescs returns a slice of descriptors for all the transactions in the pool. The descriptors are to be treated as +// read only. This function is safe for concurrent access. +func (mp *TxPool) TxDescs() []*TxDesc { + mp.mtx.RLock() + descs := make([]*TxDesc, len(mp.pool)) + i := 0 + for _, desc := range mp.pool { + descs[i] = desc + i++ + } + mp.mtx.RUnlock() + return descs +} + +// TxHashes returns a slice of hashes for all of the transactions in the memory pool. This function is safe for +// concurrent access. +func (mp *TxPool) TxHashes() []*chainhash.Hash { + mp.mtx.RLock() + hashes := make([]*chainhash.Hash, len(mp.pool)) + i := 0 + for hash := range mp.pool { + hashCopy := hash + hashes[i] = &hashCopy + i++ + } + mp.mtx.RUnlock() + return hashes +} + +// addOrphan adds an orphan transaction to the orphan pool. This function MUST be called with the mempool lock held (for +// writes). +func (mp *TxPool) addOrphan(tx *util.Tx, tag Tag) { + // Nothing to do if no orphans are allowed. + if mp.cfg.Policy.MaxOrphanTxs <= 0 { + return + } + // Limit the number orphan transactions to prevent memory exhaustion. This will periodically remove any expired + // orphans and evict a random orphan if space is still needed. + e := mp.limitNumOrphans() + if e != nil { + W.Ln("failed to set orphan limit", e) + } + mp.orphans[*tx.Hash()] = &orphanTx{ + tx: tx, + tag: tag, + expiration: time.Now().Add(orphanTTL), + } + for _, txIn := range tx.MsgTx().TxIn { + if _, exists := mp.orphansByPrev[txIn.PreviousOutPoint]; !exists { + mp.orphansByPrev[txIn.PreviousOutPoint] = + make(map[chainhash.Hash]*util.Tx) + } + mp.orphansByPrev[txIn.PreviousOutPoint][*tx.Hash()] = tx + } + D.Ln("stored orphan transaction", tx.Hash(), "(total:", len(mp.orphans), ")") +} + +// addTransaction adds the passed transaction to the memory pool. It should not be called directly as it doesn't perform +// any validation. This is a helper for maybeAcceptTransaction. This function MUST be called with the mempool lock held +// (for writes). +func (mp *TxPool) addTransaction(utxoView *blockchain.UtxoViewpoint, tx *util.Tx, height int32, fee int64) *TxDesc { + // Add the transaction to the pool and mark the referenced outpoints as spent by the pool. + txD := &TxDesc{ + TxDesc: mining.TxDesc{ + Tx: tx, + Added: time.Now(), + Height: height, + Fee: fee, + FeePerKB: fee * 1000 / GetTxVirtualSize(tx), + }, + StartingPriority: mining.CalcPriority(tx.MsgTx(), utxoView, height), + } + mp.pool[*tx.Hash()] = txD + for _, txIn := range tx.MsgTx().TxIn { + mp.outpoints[txIn.PreviousOutPoint] = tx + } + atomic.StoreInt64(&mp.lastUpdated, time.Now().Unix()) + if mp.updateHook != nil { + mp.updateHook() + } + // Add unconfirmed address index entries associated with the transaction if enabled. + if mp.cfg.AddrIndex != nil { + mp.cfg.AddrIndex.AddUnconfirmedTx(tx, utxoView) + } + // Record this tx for fee estimation if enabled. + if mp.cfg.FeeEstimator != nil { + mp.cfg.FeeEstimator.ObserveTransaction(txD) + } + return txD +} + +// checkPoolDoubleSpend checks whether or not the passed transaction is attempting to spend coins already spent by other +// transactions in the pool. Note it does not check for double spends against transactions already in the main chain. +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) checkPoolDoubleSpend(tx *util.Tx) (e error) { + for _, txIn := range tx.MsgTx().TxIn { + if txR, exists := mp.outpoints[txIn.PreviousOutPoint]; exists { + str := fmt.Sprintf( + "output %v already spent by "+ + "transaction %v in the memory pool", + txIn.PreviousOutPoint, txR.Hash(), + ) + return txRuleError(wire.RejectDuplicate, str) + } + } + return nil +} + +// fetchInputUtxos loads utxo details about the input transactions referenced by the passed transaction. First it loads +// the details form the viewpoint of the main chain, then it adjusts them based upon the contents of the transaction +// pool. This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) fetchInputUtxos(tx *util.Tx) (*blockchain.UtxoViewpoint, error) { + utxoView, e := mp.cfg.FetchUtxoView(tx) + if e != nil { + return nil, e + } + // Attempt to populate any missing inputs from the transaction pool. + for _, txIn := range tx.MsgTx().TxIn { + prevOut := &txIn.PreviousOutPoint + entry := utxoView.LookupEntry(*prevOut) + if entry != nil && !entry.IsSpent() { + continue + } + if poolTxDesc, exists := mp.pool[prevOut.Hash]; exists { + // AddTxOut ignores out of range index values, + // so it is safe to call without bounds checking here. + utxoView.AddTxOut( + poolTxDesc.Tx, prevOut.Index, + mining.UnminedHeight, + ) + } + } + return utxoView, nil +} + +// haveTransaction returns whether or not the passed transaction already exists in the main pool or in the orphan pool. +// This function MUST be called with the mempool lock held (for reads). +func (mp *TxPool) haveTransaction( + hash *chainhash.Hash, +) bool { + return mp.isTransactionInPool(hash) || mp.isOrphanInPool(hash) +} + +// isOrphanInPool returns whether or not the passed transaction already exists in the orphan pool. This function MUST be +// called with the mempool lock held (for reads). +func (mp *TxPool) isOrphanInPool(hash *chainhash.Hash) bool { + if _, exists := mp.orphans[*hash]; exists { + return true + } + return false +} + +// isTransactionInPool returns whether or not the passed transaction already exists in the main pool. This function MUST +// be called with the mempool lock held (for reads). +func (mp *TxPool) isTransactionInPool(hash *chainhash.Hash) bool { + if _, exists := mp.pool[*hash]; exists { + return true + } + return false +} + +// limitNumOrphans limits the number of orphan transactions by evicting a random orphan if adding a new one would cause +// it to overflow the max allowed. This function MUST be called with the mempool lock held (for writes). +func (mp *TxPool) limitNumOrphans() (e error) { + // Scan through the orphan pool and remove any expired orphans when it's time. This is done for efficiency so the + // scan only happens periodically instead of on every orphan added to the pool. + if now := time.Now(); now.After(mp.nextExpireScan) { + origNumOrphans := len(mp.orphans) + for _, otx := range mp.orphans { + if now.After(otx.expiration) { + // Remove redeemers too because the missing parents are very unlikely to ever materialize since the + // orphan has already been around more than long enough for them to be delivered. + mp.removeOrphan(otx.tx, true) + } + } + // Set next expiration scan to occur after the scan interval. + mp.nextExpireScan = now.Add(orphanExpireScanInterval) + numOrphans := len(mp.orphans) + if numExpired := origNumOrphans - numOrphans; numExpired > 0 { + D.F( + "Expired %d %s (remaining: %d)", + numExpired, log.PickNoun(numExpired, "orphan", "orphans"), + numOrphans, + ) + } + } + // Nothing to do if adding another orphan will not cause the pool to exceed the limit. + if len(mp.orphans)+1 <= mp.cfg.Policy.MaxOrphanTxs { + return nil + } + // Remove a random entry from the map. For most compilers, Go's range statement iterates starting at a random item + // although that is not 100% guaranteed by the spec. The iteration order is not important here because an adversary + // would have to be able to pull off preimage attacks on the hashing function in order to target eviction of + // specific entries anyways. + for _, otx := range mp.orphans { + // Don't remove redeemers in the case of a random eviction since it is quite possible it might be needed again + // shortly. + mp.removeOrphan(otx.tx, false) + break + } + return nil +} + +// maybeAcceptTransaction is the internal function which implements the public MaybeAcceptTransaction. See the comment +// for MaybeAcceptTransaction for more details. This function MUST be called with the mempool lock held (for writes). +func (mp *TxPool) maybeAcceptTransaction( + b *blockchain.BlockChain, tx *util.Tx, isNew, rateLimit, rejectDupOrphans bool, +) ([]*chainhash.Hash, *TxDesc, error) { + txHash := tx.Hash() + // // If a transaction has witness data, and segwit isn't active yet, If segwit isn't active yet, then we won't accept + // // it into the mempool as it can't be mined yet. + // if tx.MsgTx().HasWitness() { + // segwitActive, e := mp.cfg.IsDeploymentActive(chaincfg.DeploymentSegwit) + // if e != nil { + // General // return nil, nil, e + // } + // if !segwitActive { + // str := fmt.Sprintf("transaction %v has witness data, but segwit isn't active yet", txHash) + // return nil, nil, txRuleError(wire.RejectNonstandard, str) + // } + // } + if blockchain.ContainsBlacklisted(b, tx, hardfork.Blacklist) { + return nil, nil, errors.New("transaction contains blacklisted address") + } + // Don't accept the transaction if it already exists in the pool. This applies to orphan transactions as well when + // the reject duplicate orphans flag is set. This check is intended to be a quick check to weed out duplicates. + if mp.isTransactionInPool(txHash) || (rejectDupOrphans && + mp.isOrphanInPool(txHash)) { + str := fmt.Sprintf("already have transaction %v", txHash) + return nil, nil, txRuleError(wire.RejectDuplicate, str) + } + // Perform preliminary sanity checks on the transaction. This makes use of blockchain which contains the invariant + // rules for what transactions are allowed into blocks. + e := blockchain.CheckTransactionSanity(tx) + if e != nil { + if cErr, ok := e.(blockchain.RuleError); ok { + return nil, nil, chainRuleError(cErr) + } + return nil, nil, e + } + // A standalone transaction must not be a coinbase transaction. + if blockchain.IsCoinBase(tx) { + str := fmt.Sprintf( + "transaction %v is an individual coinbase", + txHash, + ) + return nil, nil, txRuleError(wire.RejectInvalid, str) + } + // Get the current height of the main chain. A standalone transaction will be mined into the next block at best, so + // its height is at least one more than the current height. + bestHeight := mp.cfg.BestHeight() + nextBlockHeight := bestHeight + 1 + medianTimePast := mp.cfg.MedianTimePast() + // Don't allow non-standard transactions if the network parameters forbid their acceptance. + if !mp.cfg.Policy.AcceptNonStd { + e = checkTransactionStandard( + tx, + nextBlockHeight, + medianTimePast, + mp.cfg.Policy.MinRelayTxFee, + mp.cfg.Policy.MaxTxVersion, + ) + if e != nil { + // Attempt to extract a reject code from the error so it can be retained. When not possible, fall back to a + // non standard error. + rejectCode, found := extractRejectCode(e) + if !found { + rejectCode = wire.RejectNonstandard + } + str := fmt.Sprintf( + "transaction %v is not standard: %v", + txHash, e, + ) + return nil, nil, txRuleError(rejectCode, str) + } + } + // The transaction may not use any of the same outputs as other transactions already in the pool as that would + // ultimately result in a double spend. This check is intended to be quick and therefore only detects double spends + // within the transaction pool itself. The transaction could still be double spending coins from the main chain at + // this point. There is a more in-depth check that happens later after fetching the referenced transaction inputs + // from the main chain which examines the actual spend data and prevents double spends. + e = mp.checkPoolDoubleSpend(tx) + if e != nil { + return nil, nil, e + } + // Fetch all of the unspent transaction outputs referenced by the inputs to this transaction. This function also + // attempts to fetch the transaction itself to be used for detecting a duplicate transaction without needing to do a + // separate lookup. + utxoView, e := mp.fetchInputUtxos(tx) + if e != nil { + if cErr, ok := e.(blockchain.RuleError); ok { + return nil, nil, chainRuleError(cErr) + } + return nil, nil, e + } + // Don't allow the transaction if it exists in the main chain and is not not already fully spent. + prevOut := wire.OutPoint{Hash: *txHash} + for txOutIdx := range tx.MsgTx().TxOut { + prevOut.Index = uint32(txOutIdx) + entry := utxoView.LookupEntry(prevOut) + if entry != nil && !entry.IsSpent() { + return nil, nil, txRuleError( + wire.RejectDuplicate, + "transaction already exists", + ) + } + utxoView.RemoveEntry(prevOut) + } + // Transaction is an orphan if any of the referenced transaction outputs don't exist or are already spent. Adding + // orphans to the orphan pool is not handled by this function, and the caller should use maybeAddOrphan if this + // behavior is desired. + var missingParents []*chainhash.Hash + for outpoint, entry := range utxoView.Entries() { + if entry == nil || entry.IsSpent() { + // Must make a copy of the hash here since the iterator is replaced and taking its address directly would + // result in all of the entries pointing to the same memory location and thus all be the final hash. + hashCopy := outpoint.Hash + missingParents = append(missingParents, &hashCopy) + } + } + if len(missingParents) > 0 { + return missingParents, nil, nil + } + // // Don't allow the transaction into the mempool unless its sequence lock is active, meaning that it'll be allowed + // // into the next block with respect to its defined relative lock times. + // sequenceLock, e := mp.cfg.CalcSequenceLock(tx, utxoView) + // if e != nil { + // General // if cErr, ok := err.(blockchain.RuleError); ok { + // return nil, nil, chainRuleError(cErr) + // } + // return nil, nil, e + // } + // if !blockchain.SequenceLockActive( + // sequenceLock, nextBlockHeight, + // medianTimePast, + // ) { + // return nil, nil, txRuleError( + // wire.RejectNonstandard, + // "transaction's sequence locks on inputs not met", + // ) + // } + // Perform several checks on the transaction inputs using the invariant rules in blockchain for what transactions + // are allowed into blocks. Also returns the fees associated with the transaction which will be used later. + txFee, e := blockchain.CheckTransactionInputs( + tx, nextBlockHeight, + utxoView, mp.cfg.ChainParams, + ) + if e != nil { + if cErr, ok := e.(blockchain.RuleError); ok { + return nil, nil, chainRuleError(cErr) + } + return nil, nil, e + } + // Don't allow transactions with non-standard inputs if the network parameters forbid their acceptance. + if !mp.cfg.Policy.AcceptNonStd { + e = checkInputsStandard(tx, utxoView) + if e != nil { + // Attempt to extract a reject code from the error so it can be retained. When not possible, fall back to a + // non standard error. + rejectCode, found := extractRejectCode(e) + if !found { + rejectCode = wire.RejectNonstandard + } + str := fmt.Sprintf( + "transaction %v has a non-standard "+ + "input: %v", txHash, e, + ) + return nil, nil, txRuleError(rejectCode, str) + } + } + // NOTE: if you modify this code to accept non-standard transactions, you should add code here to check that the + // transaction does a reasonable number of ECDSA signature verifications. Don't allow transactions with an excessive + // number of signature operations which would result in making it impossible to mine. Since the coinbase address + // itself can contain signature operations, the maximum allowed signature operations per transaction is less than + // the maximum allowed signature operations per block. TODO(roasbeef): last bool should be conditional on segwit + // activation + var sigOpCost int + sigOpCost, e = blockchain.GetSigOpCost(tx, false, utxoView, true) + if e != nil { + if cErr, ok := e.(blockchain.RuleError); ok { + return nil, nil, chainRuleError(cErr) + } + return nil, nil, e + } + if sigOpCost > mp.cfg.Policy.MaxSigOpCostPerTx { + str := fmt.Sprintf( + "transaction %v sigop cost is too high: %d > %d", + txHash, sigOpCost, mp.cfg.Policy.MaxSigOpCostPerTx, + ) + return nil, nil, txRuleError(wire.RejectNonstandard, str) + } + // Don't allow transactions with fees too low to get into a mined block. Most miners allow a free transaction area + // in blocks they mine to go alongside the area used for high-priority transactions as well as transactions with + // fees. A transaction size of up to 1000 bytes is considered safe to go into this section. Further, the minimum fee + // calculated below on its own would encourage several small transactions to avoid fees rather than one single + // larger transaction which is more desirable. Therefore as long as the size of the transaction does not exceed 1000 + // less than the reserved space for high-priority transactions, don't require a fee for it. + serializedSize := GetTxVirtualSize(tx) + minFee := calcMinRequiredTxRelayFee( + serializedSize, + mp.cfg.Policy.MinRelayTxFee, + ) + if serializedSize >= (constant.DefaultBlockPrioritySize-1000) && txFee < minFee { + str := fmt.Sprintf( + "transaction %v has %d fees which is under the required amount of %d", + txHash, txFee, minFee, + ) + return nil, nil, txRuleError(wire.RejectInsufficientFee, str) + } + // Require that free transactions have sufficient priority to be mined in the next block. Transactions which are + // being added back to the memory pool from blocks that have been disconnected during a reorg are exempted. + if isNew && !mp.cfg.Policy.DisableRelayPriority && txFee < minFee { + currentPriority := mining.CalcPriority( + tx.MsgTx(), utxoView, + nextBlockHeight, + ) + if currentPriority <= mining.MinHighPriority.ToDUO() { + str := fmt.Sprintf( + "transaction %v has insufficient "+ + "priority (%v <= %v)", txHash, + currentPriority, mining.MinHighPriority, + ) + return nil, nil, txRuleError(wire.RejectInsufficientFee, str) + } + } + // Free-to-relay transactions are rate limited here to prevent penny -flooding with tiny transactions as a form of + // attack. + if rateLimit && txFee < minFee { + nowUnix := time.Now().Unix() + // Decay passed data with an exponentially decaying ~10 minute window - matches bitcoind handling. + mp.pennyTotal *= math.Pow( + 1.0-1.0/600.0, + float64(nowUnix-mp.lastPennyUnix), + ) + mp.lastPennyUnix = nowUnix + // Are we still over the limit? + if mp.pennyTotal >= mp.cfg.Policy.FreeTxRelayLimit*10*1000 { + str := fmt.Sprintf( + "transaction %v has been rejected "+ + "by the rate limiter due to low fees", txHash, + ) + return nil, nil, txRuleError(wire.RejectInsufficientFee, str) + } + oldTotal := mp.pennyTotal + mp.pennyTotal += float64(serializedSize) + T.F( + "rate limit: curTotal %v, nextTotal: %v, limit %v", + oldTotal, + mp.pennyTotal, + mp.cfg.Policy.FreeTxRelayLimit*10*1000, + ) + } + // Verify crypto signatures for each input and reject the transaction if any don't verify. + e = blockchain.ValidateTransactionScripts( + b, tx, utxoView, + txscript.StandardVerifyFlags, mp.cfg.SigCache, + mp.cfg.HashCache, + ) + if e != nil { + if cErr, ok := e.(blockchain.RuleError); ok { + return nil, nil, chainRuleError(cErr) + } + return nil, nil, e + } + // Add to transaction pool. + txD := mp.addTransaction(utxoView, tx, bestHeight, txFee) + D.F( + "accepted transaction %v (pool size: %v) %s", + txHash, + len(mp.pool), + ) + return nil, txD, nil +} + +// maybeAddOrphan potentially adds an orphan to the orphan pool. This function MUST be called with the mempool lock held +// (for writes). +func (mp *TxPool) maybeAddOrphan(tx *util.Tx, tag Tag) (e error) { + // Ignore orphan transactions that are too large. This helps avoid a memory exhaustion attack based on sending a lot + // of really large orphans. In the case there is a valid transaction larger than this, it will ultimately be + // rebroadcast after the parent transactions have been mined or otherwise received. Note that the number of orphan + // transactions in the orphan pool is also limited, so this equates to a maximum memory used of mp.cfg.Policy. + // MaxOrphanTxSize * mp.cfg.Policy.MaxOrphanTxs ( which is ~5MB using the default values at the time this comment + // was written). + serializedLen := tx.MsgTx().SerializeSize() + if serializedLen > mp.cfg.Policy.MaxOrphanTxSize { + str := fmt.Sprintf( + "orphan transaction size of %d bytes is larger"+ + " than max allowed size of %d bytes", + serializedLen, mp.cfg.Policy.MaxOrphanTxSize, + ) + return txRuleError(wire.RejectNonstandard, str) + } + // Add the orphan if the none of the above disqualified it. + mp.addOrphan(tx, tag) + return nil +} + +// processOrphans is the internal function which implements the public ProcessOrphans. See the comment for +// ProcessOrphans for more details. This function MUST be called with the mempool lock held (for writes). +func (mp *TxPool) processOrphans(b *blockchain.BlockChain, acceptedTx *util.Tx) []*TxDesc { + var acceptedTxns []*TxDesc + // Start with processing at least the passed transaction. + processList := list.New() + processList.PushBack(acceptedTx) + for processList.Len() > 0 { + // Pop the transaction to process from the front of the list. + firstElement := processList.Remove(processList.Front()) + processItem := firstElement.(*util.Tx) + prevOut := wire.OutPoint{Hash: *processItem.Hash()} + for txOutIdx := range processItem.MsgTx().TxOut { + // Look up all orphans that redeem the output that is now available. This will typically only be one but it + // could be multiple if the orphan pool contains double spends. While it may seem odd that the orphan pool + // would allow this since there can only possibly ultimately be a single redeemer, it's important to track + // it this way to prevent malicious actors from being able to purposely constructing orphans that would + // otherwise make outputs unspendable. Skip to the next available output if there are none. + prevOut.Index = uint32(txOutIdx) + orphans, exists := mp.orphansByPrev[prevOut] + if !exists { + continue + } + // Potentially accept an orphan into the tx pool. + for _, tx := range orphans { + missing, txD, e := mp.maybeAcceptTransaction( + b, tx, true, true, false, + ) + if e != nil { + // The orphan is now invalid so there is no way any other orphans which redeem any of its outputs + // can be accepted. Remove them. + mp.removeOrphan(tx, true) + break + } + // Transaction is still an orphan. Try the next orphan which redeems this output. + if len(missing) > 0 { + continue + } + // Transaction was accepted into the main pool. Add it to the list of accepted transactions that are no + // longer orphans, remove it from the orphan pool, and add it to the list of transactions to process so + // any orphans that depend on it are handled too. + acceptedTxns = append(acceptedTxns, txD) + mp.removeOrphan(tx, false) + processList.PushBack(tx) + // Only one transaction for this outpoint can be accepted, so the rest are now double spends and are + // removed later. + break + } + } + } + // Recursively remove any orphans that also redeem any outputs redeemed by the accepted transactions since those are + // now definitive double spends. + mp.removeOrphanDoubleSpends(acceptedTx) + for _, txD := range acceptedTxns { + mp.removeOrphanDoubleSpends(txD.Tx) + } + return acceptedTxns +} + +// removeOrphan is the internal function which implements the public RemoveOrphan. See the comment for RemoveOrphan for +// more details. This function MUST be called with the mempool lock held (for writes). +func (mp *TxPool) removeOrphan(tx *util.Tx, removeRedeemers bool) { + // Nothing to do if passed tx is not an orphan. + txHash := tx.Hash() + otx, exists := mp.orphans[*txHash] + if !exists { + return + } + // Remove the reference from the previous orphan index. + for _, txIn := range otx.tx.MsgTx().TxIn { + orphans, exists := mp.orphansByPrev[txIn.PreviousOutPoint] + if exists { + delete(orphans, *txHash) + // Remove the map entry altogether if there are no longer any orphans which depend on it. + if len(orphans) == 0 { + delete(mp.orphansByPrev, txIn.PreviousOutPoint) + } + } + } + // Remove any orphans that redeem outputs from this one if requested. + if removeRedeemers { + prevOut := wire.OutPoint{Hash: *txHash} + for txOutIdx := range tx.MsgTx().TxOut { + prevOut.Index = uint32(txOutIdx) + for _, orphan := range mp.orphansByPrev[prevOut] { + mp.removeOrphan(orphan, true) + } + } + } + // Remove the transaction from the orphan pool. + delete(mp.orphans, *txHash) +} + +// removeOrphanDoubleSpends removes all orphans which spend outputs spent by the passed transaction from the orphan +// pool. Removing those orphans then leads to removing all orphans which rely on them, recursively. This is necessary +// when a transaction is added to the main pool because it may spend outputs orphans also spend. This function MUST be +// called with the mempool lock held (for writes). +func (mp *TxPool) removeOrphanDoubleSpends(tx *util.Tx) { + msgTx := tx.MsgTx() + for _, txIn := range msgTx.TxIn { + for _, orphan := range mp.orphansByPrev[txIn.PreviousOutPoint] { + mp.removeOrphan(orphan, true) + } + } +} + +// removeTransaction is the internal function which implements the public RemoveTransaction. See the comment for +// RemoveTransaction for more details. This function MUST be called with the mempool lock held (for writes). +func (mp *TxPool) removeTransaction(tx *util.Tx, removeRedeemers bool) { + txHash := tx.Hash() + if removeRedeemers { + // Remove any transactions which rely on this one. + for i := uint32(0); i < uint32(len(tx.MsgTx().TxOut)); i++ { + prevOut := wire.OutPoint{Hash: *txHash, Index: i} + if txRedeemer, exists := mp.outpoints[prevOut]; exists { + mp.removeTransaction(txRedeemer, true) + } + } + } + // Remove the transaction if needed. + if txDesc, exists := mp.pool[*txHash]; exists { + // Remove unconfirmed address index entries associated with the transaction if enabled. + if mp.cfg.AddrIndex != nil { + mp.cfg.AddrIndex.RemoveUnconfirmedTx(txHash) + } + // Mark the referenced outpoints as unspent by the pool. + for _, txIn := range txDesc.Tx.MsgTx().TxIn { + delete(mp.outpoints, txIn.PreviousOutPoint) + } + delete(mp.pool, *txHash) + atomic.StoreInt64(&mp.lastUpdated, time.Now().Unix()) + if mp.updateHook != nil { + mp.updateHook() + } + } +} + +// New returns a new memory pool for validating and storing standalone transactions until they are mined into a block. +func New(cfg *Config) *TxPool { + return &TxPool{ + cfg: *cfg, + pool: make(map[chainhash.Hash]*TxDesc), + orphans: make(map[chainhash.Hash]*orphanTx), + orphansByPrev: make(map[wire.OutPoint]map[chainhash.Hash]*util.Tx), + nextExpireScan: time.Now().Add(orphanExpireScanInterval), + outpoints: make(map[wire.OutPoint]*util.Tx), + updateHook: cfg.UpdateHook, + } +} diff --git a/pkg/mempool/mempool_test.go b/pkg/mempool/mempool_test.go new file mode 100644 index 0000000..259b9eb --- /dev/null +++ b/pkg/mempool/mempool_test.go @@ -0,0 +1,837 @@ +package mempool + +import ( + "encoding/hex" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "reflect" + "runtime" + "sync" + "testing" + "time" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// fakeChain is used by the pool harness to provide generated test utxos and a current faked chain height to the pool +// callbacks. This, in turn, allows transactions to appear as though they are spending completely valid utxos. +type fakeChain struct { + sync.RWMutex + utxos *blockchain.UtxoViewpoint + currentHeight int32 + medianTimePast time.Time +} + +// FetchUtxoView loads utxo details about the inputs referenced by the passed transaction from the point of view of the +// fake chain. It also attempts to fetch the utxos for the outputs of the transaction itself so the returned view can be +// examined for duplicate transactions. This function is safe for concurrent access however the returned view is NOT. +func (s *fakeChain) FetchUtxoView(tx *util.Tx) (*blockchain.UtxoViewpoint, error) { + s.RLock() + defer s.RUnlock() + // All entries are cloned to ensure modifications to the returned view do not affect the fake chain's view. Add an + // entry for the tx itself to the new view. + viewpoint := blockchain.NewUtxoViewpoint() + prevOut := wire.OutPoint{Hash: *tx.Hash()} + for txOutIdx := range tx.MsgTx().TxOut { + prevOut.Index = uint32(txOutIdx) + entry := s.utxos.LookupEntry(prevOut) + viewpoint.Entries()[prevOut] = entry.Clone() + } + // Add entries for all of the inputs to the tx to the new view. + for _, txIn := range tx.MsgTx().TxIn { + entry := s.utxos.LookupEntry(txIn.PreviousOutPoint) + viewpoint.Entries()[txIn.PreviousOutPoint] = entry.Clone() + } + return viewpoint, nil +} + +// BestHeight returns the current height associated with the fake chain instance. +func (s *fakeChain) BestHeight() int32 { + s.RLock() + height := s.currentHeight + s.RUnlock() + return height +} + +// SetHeight sets the current height associated with the fake chain instance. +func (s *fakeChain) SetHeight(height int32) { + s.Lock() + s.currentHeight = height + s.Unlock() +} + +// MedianTimePast returns the current median time past associated with the fake chain instance. +func (s *fakeChain) MedianTimePast() time.Time { + s.RLock() + mtp := s.medianTimePast + s.RUnlock() + return mtp +} + +// SetMedianTimePast sets the current median time past associated with the fake chain instance. +func (s *fakeChain) SetMedianTimePast(mtp time.Time) { + s.Lock() + s.medianTimePast = mtp + s.Unlock() +} + +// // CalcSequenceLock returns the current sequence lock for the passed transaction associated with the fake chain +// // instance. +// func (s *fakeChain) CalcSequenceLock(tx *util.Tx, +// view *blockchain.UtxoViewpoint) (*blockchain.SequenceLock, error) { +// return &blockchain.SequenceLock{ +// Seconds: -1, +// BlockHeight: -1, +// }, nil +// } + +// spendableOutput is a convenience type that houses a particular utxo and the amount associated with it. +type spendableOutput struct { + outPoint wire.OutPoint + amount amt.Amount +} + +// txOutToSpendableOut returns a spendable output given a transaction and index of the output to use. This is useful as +// a convenience when creating test transactions. +func txOutToSpendableOut(tx *util.Tx, outputNum uint32) spendableOutput { + return spendableOutput{ + outPoint: wire.OutPoint{Hash: *tx.Hash(), Index: outputNum}, + amount: amt.Amount(tx.MsgTx().TxOut[outputNum].Value), + } +} + +// poolHarness provides a harness that includes functionality for creating and signing transactions as well as a fake +// chain that provides utxos for use in generating valid transactions. +type poolHarness struct { + // signKey is the signing key used for creating transactions throughout the tests. payAddr is the p2sh address for + // the signing key and is used for the payment address throughout the tests. + signKey *ecc.PrivateKey + payAddr btcaddr.Address + payScript []byte + chainParams *chaincfg.Params + chain *fakeChain + txPool *TxPool +} + +// CreateCoinbaseTx returns a coinbase transaction with the requested number of outputs paying an appropriate subsidy +// based on the passed block height to the address associated with the harness. It automatically uses a standard +// signature script that starts with the block height that is required by version 2 blocks. +func (p *poolHarness) CreateCoinbaseTx(blockHeight int32, numOutputs uint32, version int32) (*util.Tx, error) { + // Create standard coinbase script. + extraNonce := int64(0) + coinbaseScript, e := txscript.NewScriptBuilder(). + AddInt64(int64(blockHeight)).AddInt64(extraNonce).Script() + if e != nil { + return nil, e + } + tx := wire.NewMsgTx(wire.TxVersion) + tx.AddTxIn( + &wire.TxIn{ + // Coinbase transactions have no inputs, so previous outpoint is zero hash and max index. + PreviousOutPoint: *wire.NewOutPoint( + &chainhash.Hash{}, + wire.MaxPrevOutIndex, + ), + SignatureScript: coinbaseScript, + Sequence: wire.MaxTxInSequenceNum, + }, + ) + totalInput := blockchain.CalcBlockSubsidy(blockHeight, p.chainParams, version) + amountPerOutput := totalInput / int64(numOutputs) + remainder := totalInput - amountPerOutput*int64(numOutputs) + for i := uint32(0); i < numOutputs; i++ { + // Ensure the final output accounts for any remainder that might be left from splitting the input amount. + amount := amountPerOutput + if i == numOutputs-1 { + amount = amountPerOutput + remainder + } + tx.AddTxOut( + &wire.TxOut{ + PkScript: p.payScript, + Value: amount, + }, + ) + } + return util.NewTx(tx), nil +} + +// CreateSignedTx creates a new signed transaction that consumes the provided inputs and generates the provided number +// of outputs by evenly splitting the total input amount. All outputs will be to the payment script associated with the +// harness and all inputs are assumed to do the same. +func (p *poolHarness) CreateSignedTx(inputs []spendableOutput, numOutputs uint32) (*util.Tx, error) { + // Calculate the total input amount and split it amongst the requested number of outputs. + var totalInput amt.Amount + for _, input := range inputs { + totalInput += input.amount + } + amountPerOutput := int64(totalInput) / int64(numOutputs) + remainder := int64(totalInput) - amountPerOutput*int64(numOutputs) + tx := wire.NewMsgTx(wire.TxVersion) + for _, input := range inputs { + tx.AddTxIn( + &wire.TxIn{ + PreviousOutPoint: input.outPoint, + SignatureScript: nil, + Sequence: wire.MaxTxInSequenceNum, + }, + ) + } + for i := uint32(0); i < numOutputs; i++ { + // Ensure the final output accounts for any remainder that might be left from splitting the input amount. + amount := amountPerOutput + if i == numOutputs-1 { + amount = amountPerOutput + remainder + } + tx.AddTxOut( + &wire.TxOut{ + PkScript: p.payScript, + Value: amount, + }, + ) + } + // Sign the new transaction. + for i := range tx.TxIn { + sigScript, e := txscript.SignatureScript( + tx, i, p.payScript, + txscript.SigHashAll, p.signKey, true, + ) + if e != nil { + return nil, e + } + tx.TxIn[i].SignatureScript = sigScript + } + return util.NewTx(tx), nil +} + +// CreateTxChain creates a chain of zero-fee transactions (each subsequent transaction spends the entire amount from the +// previous one) with the first one spending the provided outpoint. Each transaction spends the entire amount of the +// previous one and as such does not include any fees. +func (p *poolHarness) CreateTxChain(firstOutput spendableOutput, numTxns uint32) ([]*util.Tx, error) { + txChain := make([]*util.Tx, 0, numTxns) + prevOutPoint := firstOutput.outPoint + spendableAmount := firstOutput.amount + for i := uint32(0); i < numTxns; i++ { + // Create the transaction using the previous transaction output and paying the full amount to the payment + // address associated with the harness. + tx := wire.NewMsgTx(wire.TxVersion) + tx.AddTxIn( + &wire.TxIn{ + PreviousOutPoint: prevOutPoint, + SignatureScript: nil, + Sequence: wire.MaxTxInSequenceNum, + }, + ) + tx.AddTxOut( + &wire.TxOut{ + PkScript: p.payScript, + Value: int64(spendableAmount), + }, + ) + // Sign the new transaction. + sigScript, e := txscript.SignatureScript( + tx, 0, p.payScript, + txscript.SigHashAll, p.signKey, true, + ) + if e != nil { + return nil, e + } + tx.TxIn[0].SignatureScript = sigScript + txChain = append(txChain, util.NewTx(tx)) + // Next transaction uses outputs from this one. + prevOutPoint = wire.OutPoint{Hash: tx.TxHash(), Index: 0} + } + return txChain, nil +} + +// newPoolHarness returns a new instance of a pool harness initialized with a fake chain and a TxPool bound to it that +// is configured with a policy suitable for testing. Also the fake chain is populated with the returned spendable +// outputs so the caller can easily create new valid transactions which podbuild off of it. +func newPoolHarness(chainParams *chaincfg.Params) (*poolHarness, []spendableOutput, error) { + // Use a hard coded key pair for deterministic results. + keyBytes, e := hex.DecodeString( + "700868df1838811ffbdf918fb482c1f7e" + + "ad62db4b97bd7012c23e726485e577d", + ) + if e != nil { + return nil, nil, e + } + signKey, signPub := ecc.PrivKeyFromBytes(ecc.S256(), keyBytes) + // Generate associated pay-to-script-hash address and resulting payment script. + pubKeyBytes := signPub.SerializeCompressed() + payPubKeyAddr, e := btcaddr.NewPubKey(pubKeyBytes, chainParams) + if e != nil { + return nil, nil, e + } + payAddr := payPubKeyAddr.PubKeyHash() + pkScript, e := txscript.PayToAddrScript(payAddr) + if e != nil { + return nil, nil, e + } + // Create a new fake chain and harness bound to it. + chain := &fakeChain{utxos: blockchain.NewUtxoViewpoint()} + harness := poolHarness{ + signKey: signKey, + payAddr: payAddr, + payScript: pkScript, + chainParams: chainParams, + chain: chain, + txPool: New( + &Config{ + Policy: Policy{ + DisableRelayPriority: true, + FreeTxRelayLimit: 15.0, + MaxOrphanTxs: 5, + MaxOrphanTxSize: 1000, + MaxSigOpCostPerTx: blockchain.MaxBlockSigOpsCost / 4, + MinRelayTxFee: 1000, // 1 Satoshi per byte + MaxTxVersion: 1, + }, + ChainParams: chainParams, + FetchUtxoView: chain.FetchUtxoView, + BestHeight: chain.BestHeight, + MedianTimePast: chain.MedianTimePast, + // CalcSequenceLock: chain.CalcSequenceLock, + SigCache: nil, + AddrIndex: nil, + }, + ), + } + // Create a single coinbase transaction and add it to the harness chain's utxo set and set the harness chain height + // such that the coinbase will mature in the next block. This ensures the txpool accepts transactions which spend + // immature coinbases that will become mature in the next block. + numOutputs := uint32(1) + outputs := make([]spendableOutput, 0, numOutputs) + curHeight := harness.chain.BestHeight() + coinbase, e := harness.CreateCoinbaseTx(curHeight+1, numOutputs, 0) + if e != nil { + return nil, nil, e + } + harness.chain.utxos.AddTxOuts(coinbase, curHeight+1) + for i := uint32(0); i < numOutputs; i++ { + outputs = append(outputs, txOutToSpendableOut(coinbase, i)) + } + harness.chain.SetHeight(int32(chainParams.CoinbaseMaturity) + curHeight) + harness.chain.SetMedianTimePast(time.Now()) + return &harness, outputs, nil +} + +// testContext houses a test-related state that is useful to pass to helper functions as a single argument. +type testContext struct { + t *testing.T + harness *poolHarness +} + +// testPoolMembership tests the transaction pool associated with the provided test context to determine if the passed +// transaction matches the provided orphan pool and transaction pool status. It also further determines if it should be +// reported as available by the HaveTransaction function based upon the two flags and tests that condition as well. +func testPoolMembership(tc *testContext, tx *util.Tx, inOrphanPool, inTxPool bool) { + txHash := tx.Hash() + gotOrphanPool := tc.harness.txPool.IsOrphanInPool(txHash) + if inOrphanPool != gotOrphanPool { + _, file, line, _ := runtime.Caller(1) + tc.t.Fatalf( + "%s:%d -- IsOrphanInPool: want %v, got %v", file, + line, inOrphanPool, gotOrphanPool, + ) + } + gotTxPool := tc.harness.txPool.IsTransactionInPool(txHash) + if inTxPool != gotTxPool { + _, file, line, _ := runtime.Caller(1) + tc.t.Fatalf( + "%s:%d -- IsTransactionInPool: want %v, got %v", + file, line, inTxPool, gotTxPool, + ) + } + gotHaveTx := tc.harness.txPool.HaveTransaction(txHash) + wantHaveTx := inOrphanPool || inTxPool + if wantHaveTx != gotHaveTx { + _, file, line, _ := runtime.Caller(1) + tc.t.Fatalf( + "%s:%d -- HaveTransaction: want %v, got %v", file, + line, wantHaveTx, gotHaveTx, + ) + } +} + +// TestSimpleOrphanChain ensures that a simple chain of orphans is handled properly. In particular, it generates a chain +// of single input, single output transactions and inserts them while skipping the first linking transaction so they are +// all orphans. Finally, it adds the linking transaction and ensures the entire orphan chain is moved to the transaction +// pool. +func TestSimpleOrphanChain(t *testing.T) { + t.Parallel() + harness, spendableOuts, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + tc := &testContext{t, harness} + // Create a chain of transactions rooted with the first spendable output provided by the harness. + maxOrphans := uint32(harness.txPool.cfg.Policy.MaxOrphanTxs) + chainedTxns, e := harness.CreateTxChain(spendableOuts[0], maxOrphans+1) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + // Ensure the orphans are accepted (only up to the maximum allowed so none are evicted). + for _, tx := range chainedTxns[1 : maxOrphans+1] { + var acceptedTxns []*TxDesc + acceptedTxns, e = harness.txPool.ProcessTransaction( + nil, tx, true, + false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid "+ + "orphan %v", e, + ) + } + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted "+ + "transactions from what should be an orphan", + len(acceptedTxns), + ) + } + // Ensure the transaction is in the orphan pool, is not in the transaction pool, and is reported as available. + testPoolMembership(tc, tx, true, false) + } + // Add the transaction which completes the orphan chain and ensure they all get accepted. Notice the accept orphans + // flag is also false here to ensure it has no bearing on whether or not already existing orphans in the pool are + // linked. + acceptedTxns, e := harness.txPool.ProcessTransaction( + nil, chainedTxns[0], + false, false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid "+ + "orphan %v", e, + ) + } + if len(acceptedTxns) != len(chainedTxns) { + t.Fatalf( + "ProcessTransaction: reported accepted transactions "+ + "length does not match expected -- got %d, want %d", + len(acceptedTxns), len(chainedTxns), + ) + } + for _, txD := range acceptedTxns { + // Ensure the transaction is no longer in the orphan pool, is now in the transaction pool, and is reported as + // available. + testPoolMembership(tc, txD.Tx, false, true) + } +} + +// TestOrphanReject ensures that orphans are properly rejected when the allow orphans flag is not set on +// ProcessTransaction. +func TestOrphanReject(t *testing.T) { + t.Parallel() + harness, outputs, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + tc := &testContext{t, harness} + // Create a chain of transactions rooted with the first spendable output provided by the harness. + maxOrphans := uint32(harness.txPool.cfg.Policy.MaxOrphanTxs) + chainedTxns, e := harness.CreateTxChain(outputs[0], maxOrphans+1) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + // Ensure orphans are rejected when the allow orphans flag is not set. + for _, tx := range chainedTxns[1:] { + acceptedTxns, e := harness.txPool.ProcessTransaction( + nil, tx, false, + false, 0, + ) + if e == nil { + t.Fatalf( + "ProcessTransaction: did not fail on orphan "+ + "%v when allow orphans flag is false", tx.Hash(), + ) + } + expectedErr := RuleError{} + if reflect.TypeOf(e) != reflect.TypeOf(expectedErr) { + t.Fatalf( + "ProcessTransaction: wrong error got: <%T> %v, "+ + "want: <%T>", e, e, expectedErr, + ) + } + code, extracted := extractRejectCode(e) + if !extracted { + t.Fatalf( + "ProcessTransaction: failed to extract reject "+ + "code from error %q", e, + ) + } + if code != wire.RejectDuplicate { + t.Fatalf( + "ProcessTransaction: unexpected reject code "+ + "-- got %v, want %v", code, wire.RejectDuplicate, + ) + } + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted "+ + "transactions from failed orphan attempt", + len(acceptedTxns), + ) + } + // Ensure the transaction is not in the orphan pool, not in the transaction pool, and not reported as available + testPoolMembership(tc, tx, false, false) + } +} + +// TestOrphanEviction ensures that exceeding the maximum number of orphans evicts entries to make room for the new ones. +func TestOrphanEviction(t *testing.T) { + t.Parallel() + harness, outputs, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + tc := &testContext{t, harness} + // Create a chain of transactions rooted with the first spendable output provided by the harness that is long enough + // to be able to force several orphan evictions. + maxOrphans := uint32(harness.txPool.cfg.Policy.MaxOrphanTxs) + chainedTxns, e := harness.CreateTxChain(outputs[0], maxOrphans+5) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + // Add enough orphans to exceed the max allowed while ensuring they are all accepted. This will cause an eviction. + for _, tx := range chainedTxns[1:] { + acceptedTxns, e := harness.txPool.ProcessTransaction( + nil, tx, true, + false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid "+ + "orphan %v", e, + ) + } + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted "+ + "transactions from what should be an orphan", + len(acceptedTxns), + ) + } + // Ensure the transaction is in the orphan pool, is not in the transaction pool, and is reported as available. + testPoolMembership(tc, tx, true, false) + } + // Figure out which transactions were evicted and make sure the number evicted matches the expected number. + var evictedTxns []*util.Tx + for _, tx := range chainedTxns[1:] { + if !harness.txPool.IsOrphanInPool(tx.Hash()) { + evictedTxns = append(evictedTxns, tx) + } + } + expectedEvictions := len(chainedTxns) - 1 - int(maxOrphans) + if len(evictedTxns) != expectedEvictions { + t.Fatalf( + "unexpected number of evictions -- got %d, want %d", + len(evictedTxns), expectedEvictions, + ) + } + // Ensure none of the evicted transactions ended up in the transaction pool. + for _, tx := range evictedTxns { + testPoolMembership(tc, tx, false, false) + } +} + +// TestBasicOrphanRemoval ensure that orphan removal works as expected when an orphan that doesn't exist is removed both +// when there is another orphan that redeems it and when there is not. +func TestBasicOrphanRemoval(t *testing.T) { + t.Parallel() + const maxOrphans = 4 + harness, spendableOuts, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans + tc := &testContext{t, harness} + // Create a chain of transactions rooted with the first spendable output provided by the harness. + chainedTxns, e := harness.CreateTxChain(spendableOuts[0], maxOrphans+1) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + // Ensure the orphans are accepted (only up to the maximum allowed so none are evicted). + for _, tx := range chainedTxns[1 : maxOrphans+1] { + var acceptedTxns []*TxDesc + acceptedTxns, e = harness.txPool.ProcessTransaction( + nil, tx, true, + false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid "+ + "orphan %v", e, + ) + } + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted "+ + "transactions from what should be an orphan", + len(acceptedTxns), + ) + } + // Ensure the transaction is in the orphan pool, not in the transaction pool, and reported as available. + testPoolMembership(tc, tx, true, false) + } + // Attempt to remove an orphan that has no redeemers and is not present, and ensure the state of all other orphans + // are unaffected. + nonChainedOrphanTx, e := harness.CreateSignedTx( + []spendableOutput{ + { + amount: amt.Amount(5000000000), + outPoint: wire.OutPoint{Hash: chainhash.Hash{}, Index: 0}, + }, + }, 1, + ) + if e != nil { + t.Fatalf("unable to create signed tx: %v", e) + } + harness.txPool.RemoveOrphan(nonChainedOrphanTx) + testPoolMembership(tc, nonChainedOrphanTx, false, false) + for _, tx := range chainedTxns[1 : maxOrphans+1] { + testPoolMembership(tc, tx, true, false) + } + // Attempt to remove an orphan that has a existing redeemer but itself is not present and ensure the state of all + // other orphans (including the one that redeems it) are unaffected. + harness.txPool.RemoveOrphan(chainedTxns[0]) + testPoolMembership(tc, chainedTxns[0], false, false) + for _, tx := range chainedTxns[1 : maxOrphans+1] { + testPoolMembership(tc, tx, true, false) + } + // Remove each orphan one-by-one and ensure they are removed as expected. + for _, tx := range chainedTxns[1 : maxOrphans+1] { + harness.txPool.RemoveOrphan(tx) + testPoolMembership(tc, tx, false, false) + } +} + +// TestOrphanChainRemoval ensure that orphan chains (orphans that spend outputs from other orphans) are removed as +// expected. +func TestOrphanChainRemoval(t *testing.T) { + t.Parallel() + const maxOrphans = 10 + harness, spendableOuts, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans + tc := &testContext{t, harness} + // Create a chain of transactions rooted with the first spendable output provided by the harness. + chainedTxns, e := harness.CreateTxChain(spendableOuts[0], maxOrphans+1) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + // Ensure the orphans are accepted (only up to the maximum allowed so none are evicted). + for _, tx := range chainedTxns[1 : maxOrphans+1] { + acceptedTxns, e := harness.txPool.ProcessTransaction( + nil, tx, true, + false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid "+ + "orphan %v", e, + ) + } + // Ensure no transactions were reported as accepted. + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted "+ + "transactions from what should be an orphan", + len(acceptedTxns), + ) + } + // Ensure the transaction is in the orphan pool, not in the transaction pool, and reported as available. + testPoolMembership(tc, tx, true, false) + } + // Remove the first orphan that starts the orphan chain without the remove redeemer flag set and ensure that only + // the first orphan was removed. + harness.txPool.mtx.Lock() + harness.txPool.removeOrphan(chainedTxns[1], false) + harness.txPool.mtx.Unlock() + testPoolMembership(tc, chainedTxns[1], false, false) + for _, tx := range chainedTxns[2 : maxOrphans+1] { + testPoolMembership(tc, tx, true, false) + } + // Remove the first remaining orphan that starts the orphan chain with the remove redeemer flag set and ensure they + // are all removed. + harness.txPool.mtx.Lock() + harness.txPool.removeOrphan(chainedTxns[2], true) + harness.txPool.mtx.Unlock() + for _, tx := range chainedTxns[2 : maxOrphans+1] { + testPoolMembership(tc, tx, false, false) + } +} + +// TestMultiInputOrphanDoubleSpend ensures that orphans that spend from an output that is spend by another transaction +// entering the pool are removed. +func TestMultiInputOrphanDoubleSpend(t *testing.T) { + t.Parallel() + const maxOrphans = 4 + harness, outputs, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + harness.txPool.cfg.Policy.MaxOrphanTxs = maxOrphans + tc := &testContext{t, harness} + // Create a chain of transactions rooted with the first spendable output provided by the harness. + chainedTxns, e := harness.CreateTxChain(outputs[0], maxOrphans+1) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + // Start by adding the orphan transactions from the generated chain except the final one. + for _, tx := range chainedTxns[1:maxOrphans] { + var acceptedTxns []*TxDesc + acceptedTxns, e = harness.txPool.ProcessTransaction( + nil, tx, true, + false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid "+ + "orphan %v", e, + ) + } + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted transactions "+ + "from what should be an orphan", len(acceptedTxns), + ) + } + testPoolMembership(tc, tx, true, false) + } + // Ensure a transaction that contains a double spend of the same output as the second orphan that was just added as + // well as a valid spend from that last orphan in the chain generated above (and is not in the orphan pool) is + // accepted to the orphan pool. This must be allowed since it would otherwise be possible for a malicious actor to + // disrupt tx chains. + doubleSpendTx, e := harness.CreateSignedTx( + []spendableOutput{ + txOutToSpendableOut(chainedTxns[1], 0), + txOutToSpendableOut(chainedTxns[maxOrphans], 0), + }, 1, + ) + if e != nil { + t.Fatalf("unable to create signed tx: %v", e) + } + acceptedTxns, e := harness.txPool.ProcessTransaction( + nil, doubleSpendTx, + true, false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept valid orphan %v", + e, + ) + } + if len(acceptedTxns) != 0 { + t.Fatalf( + "ProcessTransaction: reported %d accepted transactions "+ + "from what should be an orphan", len(acceptedTxns), + ) + } + testPoolMembership(tc, doubleSpendTx, true, false) + // Add the transaction which completes the orphan chain and ensure the chain gets accepted. Notice the accept + // orphans flag is also false here to ensure it has no bearing on whether or not already existing orphans in the + // pool are linked. This will cause the shared output to become a concrete spend which will in turn must cause the + // double spending orphan to be removed. + acceptedTxns, e = harness.txPool.ProcessTransaction( + nil, chainedTxns[0], + false, false, 0, + ) + if e != nil { + t.Fatalf("ProcessTransaction: failed to accept valid tx %v", e) + } + if len(acceptedTxns) != maxOrphans { + t.Fatalf( + "ProcessTransaction: reported accepted transactions "+ + "length does not match expected -- got %d, want %d", + len(acceptedTxns), maxOrphans, + ) + } + for _, txD := range acceptedTxns { + // Ensure the transaction is no longer in the orphan pool, is in the transaction pool, and is reported as + // available. + testPoolMembership(tc, txD.Tx, false, true) + } + // Ensure the double spending orphan is no longer in the orphan pool and was not moved to the transaction pool. + testPoolMembership(tc, doubleSpendTx, false, false) +} + +// TestCheckSpend tests that CheckSpend returns the expected spends found in the mempool. +func TestCheckSpend(t *testing.T) { + t.Parallel() + harness, outputs, e := newPoolHarness(&chaincfg.MainNetParams) + if e != nil { + t.Fatalf("unable to create test pool: %v", e) + } + // The mempool is empty, so none of the spendable outputs should have a spend there. + for _, op := range outputs { + spend := harness.txPool.CheckSpend(op.outPoint) + if spend != nil { + t.Fatalf("Unexpeced spend found in pool: %v", spend) + } + } + // Create a chain of transactions rooted with the first spendable output provided by the harness. + const txChainLength = 5 + chainedTxns, e := harness.CreateTxChain(outputs[0], txChainLength) + if e != nil { + t.Fatalf("unable to create transaction chain: %v", e) + } + for _, tx := range chainedTxns { + _, e := harness.txPool.ProcessTransaction( + nil, tx, true, + false, 0, + ) + if e != nil { + t.Fatalf( + "ProcessTransaction: failed to accept "+ + "tx: %v", e, + ) + } + } + // The first tx in the chain should be the spend of the spendable output. + op := outputs[0].outPoint + spend := harness.txPool.CheckSpend(op) + if spend != chainedTxns[0] { + t.Fatalf( + "expected %v to be spent by %v, instead "+ + "got %v", op, chainedTxns[0], spend, + ) + } + // Now all but the last tx should be spent by the next. + for i := 0; i < len(chainedTxns)-1; i++ { + op = wire.OutPoint{ + Hash: *chainedTxns[i].Hash(), + Index: 0, + } + expSpend := chainedTxns[i+1] + spend = harness.txPool.CheckSpend(op) + if spend != expSpend { + t.Fatalf( + "expected %v to be spent by %v, instead "+ + "got %v", op, expSpend, spend, + ) + } + } + // The last tx should have no spend. + op = wire.OutPoint{ + Hash: *chainedTxns[txChainLength-1].Hash(), + Index: 0, + } + spend = harness.txPool.CheckSpend(op) + if spend != nil { + t.Fatalf("Unexpeced spend found in pool: %v", spend) + } +} diff --git a/pkg/mempool/policy.go b/pkg/mempool/policy.go new file mode 100644 index 0000000..3d0b0be --- /dev/null +++ b/pkg/mempool/policy.go @@ -0,0 +1,364 @@ +package mempool + +import ( + "fmt" + "github.com/p9c/p9/pkg/amt" + "time" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // maxStandardP2SHSigOps is the maximum number of signature operations that are + // considered standard in a pay-to-script-hash script. + maxStandardP2SHSigOps = 15 + // maxStandardTxCost is the max weight permitted by any transaction according to + // the current default policy. + maxStandardTxWeight = 400000 + // maxStandardSigScriptSize is the maximum size allowed for a transaction input + // signature script to be considered standard. This value allows for a 15-of-15 + // CHECKMULTISIG pay-to-script-hash with compressed keys. The form of the + // overall script is: + // + // OP_0 <15 signatures> OP_PUSHDATA2 <2 bytes len> [OP_15 <15 pubkeys> OP_15 OP_CHECKMULTISIG] + // + // For the p2sh script portion, each of the 15 compressed pubkeys are 33 bytes ( + // plus one for the OP_DATA_33 opcode), and the thus it totals to ( 15*34)+3 = + // 513 bytes. + // + // Next each of the 15 signatures is a max of 73 bytes (plus one for the + // OP_DATA_73 opcode). Also there is one extra byte for the initial extra OP_0 + // push and 3 bytes for the OP_PUSHDATA2 needed to specify the 513 bytes for the + // script push. + // + // That brings the total to 1+(15*74)+3+513 = 1627. This value also adds a few + // extra bytes to provide a little buffer. ( 1 + 15*74 + 3) + (15*34 + 3) + 23 = + // 1650 + maxStandardSigScriptSize = 1650 + // maxStandardMultiSigKeys is the maximum number of public keys allowed in a + // multi-signature transaction output script for it to be considered standard. + maxStandardMultiSigKeys = 3 +) + +// calcMinRequiredTxRelayFee returns the minimum transaction fee required for a +// transaction with the passed serialized size to be accepted into the memory +// pool and relayed. +func calcMinRequiredTxRelayFee(serializedSize int64, minRelayTxFee amt.Amount) int64 { + // Calculate the minimum fee for a transaction to be allowed into the mempool + // and relayed by scaling the base fee ( which is the minimum free transaction + // relay fee). minTxRelayFee is in Satoshi/kB so multiply by serializedSize ( + // which is in bytes) and divide by 1000 to get minimum Satoshis. + minFee := (serializedSize * int64(minRelayTxFee)) / 1000 + + if minFee == 0 && minRelayTxFee > 0 { + minFee = int64(minRelayTxFee) + } + // Set the minimum fee to the maximum possible value if the calculated fee is + // not in the valid range for monetary amounts. + if minFee < 0 || minFee > int64(amt.MaxSatoshi) { + minFee = int64(amt.MaxSatoshi) + } + return minFee +} + +// checkInputsStandard performs a series of checks on a transaction's inputs to +// ensure they are "standard". A standard transaction input within the context +// of this function is one whose referenced public key script is of a standard +// form and for pay-to -script-hash, does not have more than +// maxStandardP2SHSigOps signature operations. However it should also be noted +// that standard inputs also are those which have a clean stack after execution +// and only contain pushed data in their signature scripts. This function does +// not perform those checks because the script engine already does this more +// accurately and concisely via the txscript. ScriptVerifyCleanStack and +// txscript.ScriptVerifySigPushOnly flags. +func checkInputsStandard(tx *util.Tx, utxoView *blockchain.UtxoViewpoint) (e error) { + // NOTE: The reference implementation also does a coinbase check here, but + // coinbases have already been rejected prior to calling this function so no + // need to recheck. + for i, txIn := range tx.MsgTx().TxIn { + // It is safe to elide existence and index checks here since they have already + // been checked prior to calling this function. + entry := utxoView.LookupEntry(txIn.PreviousOutPoint) + originPkScript := entry.PkScript() + switch txscript.GetScriptClass(originPkScript) { + case txscript.ScriptHashTy: + numSigOps := txscript.GetPreciseSigOpCount( + txIn.SignatureScript, originPkScript, true, + ) + if numSigOps > maxStandardP2SHSigOps { + str := fmt.Sprintf( + "transaction input #%d has %d signature"+ + " operations which is more than the allowed max amount of %d", + i, numSigOps, maxStandardP2SHSigOps, + ) + return txRuleError(wire.RejectNonstandard, str) + } + case txscript.NonStandardTy: + str := fmt.Sprintf( + "transaction input #%d has a non-standard"+ + " script form", i, + ) + return txRuleError(wire.RejectNonstandard, str) + } + } + return nil +} + +// checkPkScriptStandard performs a series of checks on a transaction output +// script (public key script) to ensure it is a "standard" public key script. A +// standard public key script is one that is a recognized form, and for +// multi-signature scripts only contains from 1 to maxStandardMultiSigKeys +// public keys. +func checkPkScriptStandard(pkScript []byte, scriptClass txscript.ScriptClass) (e error) { + switch scriptClass { + case txscript.MultiSigTy: + numPubKeys, numSigs, e := txscript.CalcMultiSigStats(pkScript) + if e != nil { + str := fmt.Sprintf( + "multi-signature script parse failure: %v", e, + ) + return txRuleError(wire.RejectNonstandard, str) + } + // A standard multi-signature public key script must contain from 1 to + // maxStandardMultiSigKeys public keys. + if numPubKeys < 1 { + str := "multi-signature script with no pubkeys" + return txRuleError(wire.RejectNonstandard, str) + } + if numPubKeys > maxStandardMultiSigKeys { + str := fmt.Sprintf( + "multi-signature script with %d public keys"+ + " which is more than the allowed max of %d", numPubKeys, + maxStandardMultiSigKeys, + ) + return txRuleError(wire.RejectNonstandard, str) + } + // A standard multi-signature public key script must have at least 1 signature + // and no more signatures than available public keys. + if numSigs < 1 { + return txRuleError( + wire.RejectNonstandard, + "multi-signature script with no signatures", + ) + } + if numSigs > numPubKeys { + str := fmt.Sprintf( + "multi-signature script with %d signatures"+ + " which is more than the available %d public keys", numSigs, + numPubKeys, + ) + return txRuleError(wire.RejectNonstandard, str) + } + case txscript.NonStandardTy: + return txRuleError(wire.RejectNonstandard, "non-standard script form") + } + return nil +} + +// isDust returns whether or not the passed transaction output amount is +// considered dust or not based on the passed minimum transaction relay fee. +// Dust is defined in terms of the minimum transaction relay fee. In particular, +// if the cost to the network to spend coins is more than 1/3 of the minimum +// transaction relay fee, it is considered dust. +func isDust(txOut *wire.TxOut, minRelayTxFee amt.Amount) bool { + // Unspendable outputs are considered dust. + if txscript.IsUnspendable(txOut.PkScript) { + return true + } + // The total serialized size consists of the output and the associated input + // script to redeem it. Since there is no input script to redeem it yet, use the + // minimum size of a typical input script. Pay-to-pubkey-hash bytes breakdown: + // + // Output to hash (34 bytes): + // 8 value, 1 script len, 25 script [1 OP_DUP, 1 OP_HASH_160, + // 1 OP_DATA_20, 20 hash, 1 OP_EQUALVERIFY, 1 OP_CHECKSIG] + // + // Input with compressed pubkey (148 bytes): + // 36 prev outpoint, 1 script len, 107 script [1 OP_DATA_72, 72 sig, + // 1 OP_DATA_33, 33 compressed pubkey], 4 sequence + // Input with uncompressed pubkey (180 bytes): + // + // 36 prev outpoint, 1 script len, 139 script [1 OP_DATA_72, 72 sig, + // 1 OP_DATA_65, 65 compressed pubkey], 4 sequence + // + // Pay-to-pubkey bytes breakdown: + // + // Output to compressed pubkey (44 bytes): + // 8 value, 1 script len, 35 script [1 OP_DATA_33, + // 33 compressed pubkey, 1 OP_CHECKSIG] + // + // Output to uncompressed pubkey (76 bytes): + // 8 value, 1 script len, 67 script [1 OP_DATA_65, 65 pubkey, + // 1 OP_CHECKSIG] + // + // Input (114 bytes): + // 36 prev outpoint, 1 script len, 73 script [1 OP_DATA_72, + // 72 sig], 4 sequence + // + // Pay-to-witness-pubkey-hash bytes breakdown: + // + // Output to witness key hash (31 bytes); + // 8 value, 1 script len, 22 script [1 OP_0, 1 OP_DATA_20, + // 20 bytes hash160] + // + // Input (67 bytes as the 107 witness stack is discounted): + // 36 prev outpoint, 1 script len, 0 script (not sigScript), 107 + // witness stack bytes [1 element length, 33 compressed pubkey, + // element length 72 sig], 4 sequence + // + // Theoretically this could examine the script type of the output script and use + // a different size for the typical input script size for pay-to-pubkey vs + // pay-to-pubkey-hash inputs per the above breakdowns, but the only combination + // which is less than the value chosen is a pay-to-pubkey script with a + // compressed pubkey, which is not very common. + // + // The most common scripts are pay-to-pubkey-hash, and as per the above + // breakdown, the minimum size of a p2pkh input script is 148 bytes. So that + // figure is used. If the output being spent is a witness program, then we apply + // the witness discount to the size of the signature. + // + // The segwit analogue to p2pkh is a p2wkh output. This is the smallest output + // possible using the new segwit features. The 107 bytes of witness data is + // discounted by a factor of 4, leading to a computed value of 67 bytes of + // witness data. + // + // Both cases share a 41 byte preamble required to reference the input being + // spent and the sequence number of the input. + totalSize := txOut.SerializeSize() + 41 + if txscript.IsWitnessProgram(txOut.PkScript) { + totalSize += 107 + } else { + totalSize += 107 + } + // The output is considered dust if the cost to the network to spend the coins + // is more than 1/3 of the minimum free transaction relay fee. minFreeTxRelayFee + // is in Satoshi/KB so multiply by 1000 to convert to bytes. + // + // Using the typical values for a pay-to-pubkey-hash transaction from the + // breakdown above and the default minimum free transaction relay fee of 1000, + // this equates to values less than 546 satoshi being considered dust. + // + // The following is equivalent to (value/totalSize) * (1/3) * 1000 without + // needing to do floating point math. + return txOut.Value*1000/(3*int64(totalSize)) < int64(minRelayTxFee) +} + +// checkTransactionStandard performs a series of checks on a transaction to +// ensure it is a "standard" transaction. +// +// A standard transaction is one that conforms to several additional limiting +// cases over what is considered a "sane" transaction such as having a version +// in the supported range, being finalized, conforming to more stringent size +// constraints, having scripts of recognized forms, and not containing "dust" +// outputs (those that are so small it costs more to process them than they are +// worth). +func checkTransactionStandard( + tx *util.Tx, height int32, + medianTimePast time.Time, minRelayTxFee amt.Amount, + maxTxVersion int32, +) (e error) { + // The transaction must be a currently supported version. + msgTx := tx.MsgTx() + if msgTx.Version > maxTxVersion || msgTx.Version < 1 { + str := fmt.Sprintf( + "transaction version %d is not in the "+ + "valid range of %d-%d", msgTx.Version, 1, + maxTxVersion, + ) + return txRuleError(wire.RejectNonstandard, str) + } + // The transaction must be finalized to be standard and therefore considered for + // inclusion in a block. + if !blockchain.IsFinalizedTransaction(tx, height, medianTimePast) { + return txRuleError( + wire.RejectNonstandard, + "transaction is not finalized", + ) + } + // Since extremely large transactions with a lot of inputs can cost almost as + // much to process as the sender fees, limit the maximum size of a transaction. + // This also helps mitigate CPU exhaustion attacks. + txWeight := blockchain.GetTransactionWeight(tx) + if txWeight > maxStandardTxWeight { + str := fmt.Sprintf( + "weight of transaction %v is larger than max "+ + "allowed weight of %v", txWeight, maxStandardTxWeight, + ) + return txRuleError(wire.RejectNonstandard, str) + } + for i, txIn := range msgTx.TxIn { + // Each transaction input signature script must not exceed the maximum size + // allowed for a standard transaction. See the comment on + // maxStandardSigScriptSize for more details. + sigScriptLen := len(txIn.SignatureScript) + if sigScriptLen > maxStandardSigScriptSize { + str := fmt.Sprintf( + "transaction input %d: signature "+ + "script size of %d bytes is large than max "+ + "allowed size of %d bytes", i, sigScriptLen, + maxStandardSigScriptSize, + ) + return txRuleError(wire.RejectNonstandard, str) + } + // Each transaction input signature script must only contain opcodes which push + // data onto the stack. + if !txscript.IsPushOnlyScript(txIn.SignatureScript) { + str := fmt.Sprintf( + "transaction input %d: signature "+ + "script is not push only", i, + ) + return txRuleError(wire.RejectNonstandard, str) + } + } + // None of the output public key scripts can be a non-standard script or be + // "dust" (except when the script is a null data script). + numNullDataOutputs := 0 + for i, txOut := range msgTx.TxOut { + scriptClass := txscript.GetScriptClass(txOut.PkScript) + e := checkPkScriptStandard(txOut.PkScript, scriptClass) + if e != nil { + // Attempt to extract a reject code from the error so it can be retained. When + // not possible, fall back to a non standard error. + rejectCode := wire.RejectNonstandard + if rejCode, found := extractRejectCode(e); found { + rejectCode = rejCode + } + str := fmt.Sprintf("transaction output %d: %v", i, e) + return txRuleError(rejectCode, str) + } + // Accumulate the number of outputs which only carry data. For all other script + // types, ensure the output value is not "dust". + if scriptClass == txscript.NullDataTy { + numNullDataOutputs++ + } else if isDust(txOut, minRelayTxFee) { + str := fmt.Sprintf( + "transaction output %d: payment of %d is dust"+ + "", i, txOut.Value, + ) + return txRuleError(wire.RejectDust, str) + } + } + // A standard transaction must not have more than one output script that only + // carries data. + if numNullDataOutputs > 1 { + str := "more than one transaction output in a nulldata script" + return txRuleError(wire.RejectNonstandard, str) + } + return nil +} + +// GetTxVirtualSize computes the virtual size of a given transaction. +// A transaction's virtual size is based off its weight, +// creating a discount for any witness data it contains, +// proportional to the current blockchain.WitnessScaleFactor value. +func GetTxVirtualSize(tx *util.Tx) int64 { + // vSize := (weight(tx) + 3) / 4 + // := (((baseSize * 3) + totalSize) + 3) / 4 + // We add 3 here as a way to compute the ceiling of the prior arithmetic + // to 4. The division by 4 creates a discount for wit witness data. + return (blockchain.GetTransactionWeight(tx) + (blockchain.WitnessScaleFactor - 1)) / + blockchain.WitnessScaleFactor +} diff --git a/pkg/mempool/policy_test.go b/pkg/mempool/policy_test.go new file mode 100644 index 0000000..899cddd --- /dev/null +++ b/pkg/mempool/policy_test.go @@ -0,0 +1,560 @@ +package mempool + +import ( + "bytes" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/constant" + "testing" + "time" + + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// TestCalcMinRequiredTxRelayFee tests the calcMinRequiredTxRelayFee API. +func TestCalcMinRequiredTxRelayFee(t *testing.T) { + + tests := []struct { + name string // test description. + size int64 // Transaction size in bytes. + relayFee amt.Amount // minimum relay transaction fee. + want int64 // Expected fee. + }{ + { + // Ensure combination of size and fee that are less than 1000 produce a non-zero fee. + "250 bytes with relay fee of 3", + 250, + 3, + 3, + }, + { + "100 bytes with default minimum relay fee", + 100, + constant.DefaultMinRelayTxFee, + 100, + }, + { + "max standard tx size with default minimum relay fee", + maxStandardTxWeight / 4, + constant.DefaultMinRelayTxFee, + 100000, + }, + { + "max standard tx size with max satoshi relay fee", + maxStandardTxWeight / 4, + amt.MaxSatoshi, + amt.MaxSatoshi.Int64(), + }, + { + "1500 bytes with 5000 relay fee", + 1500, + 5000, + 7500, + }, + { + "1500 bytes with 3000 relay fee", + 1500, + 3000, + 4500, + }, + { + "782 bytes with 5000 relay fee", + 782, + 5000, + 3910, + }, + { + "782 bytes with 3000 relay fee", + 782, + 3000, + 2346, + }, + { + "782 bytes with 2550 relay fee", + 782, + 2550, + 1994, + }, + } + for _, test := range tests { + got := calcMinRequiredTxRelayFee(test.size, test.relayFee) + if got != test.want { + t.Errorf( + "TestCalcMinRequiredTxRelayFee test '%s' "+ + "failed: got %v want %v", test.name, got, + test.want, + ) + continue + } + } +} + +// TestCheckPkScriptStandard tests the checkPkScriptStandard API. +func TestCheckPkScriptStandard(t *testing.T) { + var pubKeys [][]byte + for i := 0; i < 4; i++ { + pk, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + E.Ln(e) + t.Fatalf("TestCheckPkScriptStandard NewPrivateKey failed: %v", e) + return + } + pubKeys = append(pubKeys, pk.PubKey().SerializeCompressed()) + } + tests := []struct { + name string // test description. + script *txscript.ScriptBuilder + isStandard bool + }{ + { + "key1 and key2", + txscript.NewScriptBuilder().AddOp(txscript.OP_2). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + true, + }, + { + "key1 or key2", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + true, + }, + { + "escrow", + txscript.NewScriptBuilder().AddOp(txscript.OP_2). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddData(pubKeys[2]). + AddOp(txscript.OP_3).AddOp(txscript.OP_CHECKMULTISIG), + true, + }, + { + "one of four", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddData(pubKeys[2]).AddData(pubKeys[3]). + AddOp(txscript.OP_4).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed1", + txscript.NewScriptBuilder().AddOp(txscript.OP_3). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed2", + txscript.NewScriptBuilder().AddOp(txscript.OP_2). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_3).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed3", + txscript.NewScriptBuilder().AddOp(txscript.OP_0). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_2).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed4", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_0).AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed5", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]). + AddOp(txscript.OP_CHECKMULTISIG), + false, + }, + { + "malformed6", + txscript.NewScriptBuilder().AddOp(txscript.OP_1). + AddData(pubKeys[0]).AddData(pubKeys[1]), + false, + }, + } + + for _, test := range tests { + script, err := test.script.Script() + + if err != nil { + + t.Fatalf( + "TestCheckPkScriptStandard test '%s' "+ + "failed: %v", test.name, err, + ) + // continue + } + scriptClass := txscript.GetScriptClass(script) + got := checkPkScriptStandard(script, scriptClass) + + if (test.isStandard && got != nil) || + (!test.isStandard && got == nil) { + + t.Fatalf( + "TestCheckPkScriptStandard test '%s' failed", + test.name, + ) + return + } + } +} + +// TestDust tests the isDust API. +func TestDust(t *testing.T) { + + pkScript := []byte{ + 0x76, 0xa9, 0x21, 0x03, 0x2f, 0x7e, 0x43, + 0x0a, 0xa4, 0xc9, 0xd1, 0x59, 0x43, 0x7e, 0x84, 0xb9, + 0x75, 0xdc, 0x76, 0xd9, 0x00, 0x3b, 0xf0, 0x92, 0x2c, + 0xf3, 0xaa, 0x45, 0x28, 0x46, 0x4b, 0xab, 0x78, 0x0d, + 0xba, 0x5e, 0x88, 0xac, + } + tests := []struct { + name string // test description + txOut wire.TxOut + relayFee amt.Amount // minimum relay transaction fee. + isDust bool + }{ + { + // Any value is allowed with a zero relay fee. + "zero value with zero relay fee", + wire.TxOut{Value: 0, PkScript: pkScript}, + 0, + false, + }, + { + // Zero value is dust with any relay fee" + "zero value with very small tx fee", + wire.TxOut{Value: 0, PkScript: pkScript}, + 1, + true, + }, + { + "38 byte public key script with value 584", + wire.TxOut{Value: 584, PkScript: pkScript}, + 1000, + true, + }, + { + "38 byte public key script with value 585", + wire.TxOut{Value: 585, PkScript: pkScript}, + 1000, + false, + }, + { + // Maximum allowed value is never dust. + "max satoshi amount is never dust", + wire.TxOut{Value: amt.MaxSatoshi.Int64(), PkScript: pkScript}, + amt.MaxSatoshi, + false, + }, + { + // Maximum int64 value causes overflow. + "maximum int64 value", + wire.TxOut{Value: 1<<63 - 1, PkScript: pkScript}, + 1<<63 - 1, + true, + }, + { + // Unspendable pkScript due to an invalid public key script. + "unspendable pkScript", + wire.TxOut{Value: 5000, PkScript: []byte{0x01}}, + 0, // no relay fee + true, + }, + } + + for _, test := range tests { + res := isDust(&test.txOut, test.relayFee) + + if res != test.isDust { + + t.Fatalf( + "Dust test '%s' failed: want %v got %v", + test.name, test.isDust, res, + ) + // continue + } + } +} + +// TestCheckTransactionStandard tests the checkTransactionStandard API. +func TestCheckTransactionStandard(t *testing.T) { + + // Create some dummy, but otherwise standard, data for transactions. + prevOutHash, err := chainhash.NewHashFromStr("01") + + if err != nil { + t.Fatalf("NewShaHashFromStr: unexpected error: %v", err) + } + dummyPrevOut := wire.OutPoint{Hash: *prevOutHash, Index: 1} + dummySigScript := bytes.Repeat([]byte{0x00}, 65) + dummyTxIn := wire.TxIn{ + PreviousOutPoint: dummyPrevOut, + SignatureScript: dummySigScript, + Sequence: wire.MaxTxInSequenceNum, + } + addrHash := [20]byte{0x01} + addr, err := btcaddr.NewPubKeyHash( + addrHash[:], + &chaincfg.TestNet3Params, + ) + + if err != nil { + t.Fatalf("NewPubKeyHash: unexpected error: %v", err) + } + dummyPkScript, err := txscript.PayToAddrScript(addr) + + if err != nil { + t.Fatalf("PayToAddrScript: unexpected error: %v", err) + } + dummyTxOut := wire.TxOut{ + Value: 100000000, // 1 DUO + PkScript: dummyPkScript, + } + tests := []struct { + name string + tx wire.MsgTx + height int32 + isStandard bool + code wire.RejectCode + }{ + { + name: "Typical pay-to-pubkey-hash transaction", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: true, + }, + { + name: "Transaction version too high", + tx: wire.MsgTx{ + Version: wire.TxVersion + 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Transaction is not finalized", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: dummyPrevOut, + SignatureScript: dummySigScript, + Sequence: 0, + }, + }, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 300001, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Transaction size is too large", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{ + { + Value: 0, + PkScript: bytes.Repeat( + []byte{0x00}, + (maxStandardTxWeight/4)+1, + ), + }, + }, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Signature script size is too large", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: dummyPrevOut, + SignatureScript: bytes.Repeat( + []byte{0x00}, + maxStandardSigScriptSize+1, + ), + Sequence: wire.MaxTxInSequenceNum, + }, + }, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Signature script that does more than push data", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: dummyPrevOut, + SignatureScript: []byte{ + txscript.OP_CHECKSIGVERIFY, + }, + Sequence: wire.MaxTxInSequenceNum, + }, + }, + TxOut: []*wire.TxOut{&dummyTxOut}, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Valid but non standard public key script", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{ + { + Value: 100000000, + PkScript: []byte{txscript.OP_TRUE}, + }, + }, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "More than one nulldata output", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{ + { + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }, { + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }, + }, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectNonstandard, + }, + { + name: "Dust output", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{ + { + Value: 0, + PkScript: dummyPkScript, + }, + }, + LockTime: 0, + }, + height: 300000, + isStandard: false, + code: wire.RejectDust, + }, + { + name: "One nulldata output with 0 amount (standard)", + tx: wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{&dummyTxIn}, + TxOut: []*wire.TxOut{ + { + Value: 0, + PkScript: []byte{txscript.OP_RETURN}, + }, + }, + LockTime: 0, + }, + height: 300000, + isStandard: true, + }, + } + pastMedianTime := time.Now() + + for _, test := range tests { + // Ensure standardness is as expected. + err := checkTransactionStandard( + util.NewTx(&test.tx), + test.height, pastMedianTime, constant.DefaultMinRelayTxFee, 1, + ) + if err == nil && test.isStandard { + // Test passes since function returned standard for a transaction + // which is intended to be standard. + continue + } + if err == nil && !test.isStandard { + t.Errorf( + "checkTransactionStandard (%s): standard when "+ + "it should not be", test.name, + ) + continue + } + if err != nil && test.isStandard { + t.Errorf( + "checkTransactionStandard (%s): nonstandard "+ + "when it should not be: %v", test.name, err, + ) + continue + } + // Ensure error type is a TxRuleError inside of a RuleError. + rerr, ok := err.(RuleError) + if !ok { + t.Errorf( + "checkTransactionStandard (%s): unexpected "+ + "error type - got %T", test.name, err, + ) + continue + } + txrerr, ok := rerr.Err.(TxRuleError) + if !ok { + t.Errorf( + "checkTransactionStandard (%s): unexpected "+ + "error type - got %T", test.name, rerr.Err, + ) + continue + } + // Ensure the reject code is the expected one. + if txrerr.RejectCode != test.code { + t.Errorf( + "checkTransactionStandard (%s): unexpected "+ + "error code - got %v, want %v", test.name, + txrerr.RejectCode, test.code, + ) + continue + } + } +} diff --git a/pkg/mining/README.md b/pkg/mining/README.md new file mode 100755 index 0000000..6f2aaf4 --- /dev/null +++ b/pkg/mining/README.md @@ -0,0 +1,20 @@ +# mining + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/mining) + +## Overview + +This package contains an implementation of block construction for mining +purposes. This package is currently a work in progress. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/mining +``` + +## License + +Package mining is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/mining/_mining_test.go b/pkg/mining/_mining_test.go new file mode 100755 index 0000000..bcb549c --- /dev/null +++ b/pkg/mining/_mining_test.go @@ -0,0 +1,94 @@ +package mining + +import ( + "container/heap" + "math/rand" + "testing" + + "github.com/p9c/p9/pkg/util" +) + +// TestTxFeePrioHeap ensures the priority queue for transaction fees and priorities works as expected. +func TestTxFeePrioHeap( t *testing.T) { + // Create some fake priority items that exercise the expected sort edge conditions. + testItems := []*txPrioItem{ + {feePerKB: 5678, priority: 3}, + {feePerKB: 5678, priority: 1}, + {feePerKB: 5678, priority: 1}, // Duplicate fee and prio + {feePerKB: 5678, priority: 5}, + {feePerKB: 5678, priority: 2}, + {feePerKB: 1234, priority: 3}, + {feePerKB: 1234, priority: 1}, + {feePerKB: 1234, priority: 5}, + {feePerKB: 1234, priority: 5}, // Duplicate fee and prio + {feePerKB: 1234, priority: 2}, + {feePerKB: 10000, priority: 0}, // Higher fee, smaller prio + {feePerKB: 0, priority: 10000}, // Higher prio, lower fee + } + // Add random data in addition to the edge conditions already manually specified. + randSeed := rand.Int63() + defer func() { + if t.Failed() { + t.Logf("Random numbers using seed: %v", randSeed) + } + }() + prng := rand.New(rand.NewSource(randSeed)) + for i := 0; i < 1000; i++ { + testItems = append(testItems, &txPrioItem{ + feePerKB: int64(prng.Float64() * util.SatoshiPerBitcoin), + priority: prng.Float64() * 100, + }) + } + // Test sorting by fee per KB then priority. + var highest *txPrioItem + priorityQueue := newTxPriorityQueue(len(testItems), true) + for i := 0; i < len(testItems); i++ { + prioItem := testItems[i] + if highest == nil { + highest = prioItem + } + if prioItem.feePerKB >= highest.feePerKB && + prioItem.priority > highest.priority { + highest = prioItem + } + heap.Push(priorityQueue, prioItem) + } + for i := 0; i < len(testItems); i++ { + prioItem := heap.Pop(priorityQueue).(*txPrioItem) + if prioItem.feePerKB >= highest.feePerKB && + prioItem.priority > highest.priority { + t.Fatalf("fee sort: item (fee per KB: %v, "+ + "priority: %v) higher than than prev "+ + "(fee per KB: %v, priority %v)", + prioItem.feePerKB, prioItem.priority, + highest.feePerKB, highest.priority) + } + highest = prioItem + } + // Test sorting by priority then fee per KB. + highest = nil + priorityQueue = newTxPriorityQueue(len(testItems), false) + for i := 0; i < len(testItems); i++ { + prioItem := testItems[i] + if highest == nil { + highest = prioItem + } + if prioItem.priority >= highest.priority && + prioItem.feePerKB > highest.feePerKB { + highest = prioItem + } + heap.Push(priorityQueue, prioItem) + } + for i := 0; i < len(testItems); i++ { + prioItem := heap.Pop(priorityQueue).(*txPrioItem) + if prioItem.priority >= highest.priority && + prioItem.feePerKB > highest.feePerKB { + t.Fatalf("priority sort: item (fee per KB: %v, "+ + "priority: %v) higher than than prev "+ + "(fee per KB: %v, priority %v)", + prioItem.feePerKB, prioItem.priority, + highest.feePerKB, highest.priority) + } + highest = prioItem + } +} diff --git a/pkg/mining/_miningaddresses.go b/pkg/mining/_miningaddresses.go new file mode 100644 index 0000000..90ae553 --- /dev/null +++ b/pkg/mining/_miningaddresses.go @@ -0,0 +1,60 @@ +package mining + +import ( + "github.com/p9c/p9/cmd/node/active" + "github.com/p9c/p9/pkg/btcaddr" + wm "github.com/p9c/p9/pkg/waddrmgr" + "github.com/p9c/p9/cmd/wallet" + "github.com/p9c/p9/pod/config" +) + +// RefillMiningAddresses adds new addresses to the mining address pool for the miner +// todo: make this remove ones that have been used or received a payment or mined +func RefillMiningAddresses(w *wallet.Wallet, cfg *config.Config, stateCfg *active.Config) { + if w == nil { + D.Ln("trying to refill without a wallet") + return + } + if cfg == nil { + D.Ln("config is empty") + return + } + miningAddressLen := len(cfg.MiningAddrs.S()) + toMake := 99 - miningAddressLen + if miningAddressLen >= 99 { + toMake = 0 + } + if toMake < 1 { + D.Ln("not making any new addresses") + return + } + D.Ln("refilling mining addresses") + account, e := w.AccountNumber( + wm.KeyScopeBIP0044, + "default", + ) + if e != nil { + E.Ln("error getting account number ", e) + } + for i := 0; i < toMake; i++ { + var addr btcaddr.Address + addr, e = w.NewAddress( + account, wm.KeyScopeBIP0044, + true, + ) + if e == nil { + // add them to the configuration to be saved + cfg.MiningAddrs.Set(append(cfg.MiningAddrs.S(), addr.EncodeAddress())) + // add them to the active mining address list so they + // are ready to use + stateCfg.ActiveMiningAddrs = append(stateCfg.ActiveMiningAddrs, addr) + } else { + E.Ln("error adding new address ", e) + } + } + if podcfg.Save(cfg) { + D.Ln("saved config with new addresses") + } else { + E.Ln("error adding new addresses", e) + } +} diff --git a/pkg/mining/_policy_test.go b/pkg/mining/_policy_test.go new file mode 100755 index 0000000..927ecc5 --- /dev/null +++ b/pkg/mining/_policy_test.go @@ -0,0 +1,147 @@ +package mining + +import ( + "encoding/hex" + "testing" + + blockchain "github.com/p9c/p9/pkg/chain" + chainhash "github.com/p9c/p9/pkg/chain/hash" + "github.com/p9c/p9/pkg/chain/wire" + "github.com/p9c/p9/pkg/util" +) + +// newHashFromStr converts the passed big-endian hex string into a chainhash.Hash. It only differs from the one available in chainhash in that it panics on an error since it will only (and must only) be called with hard-coded, and therefore known good, hashes. +func newHashFromStr( hexStr string) *chainhash.Hash { + hash, e := chainhash.NewHashFromStr(hexStr) + if e != nil { + L.panic("invalid hash in source file: " + hexStr) + } + return hash +} + +// hexToBytes converts the passed hex string into bytes and will panic if there is an error. This is only provided for the hard-coded constants so errors in the source code can be detected. It will only (and must only) be called with hard-coded values. +func hexToBytes( s string) []byte { + b, e := hex.DecodeString(s) + if e != nil { + L.panic("invalid hex in source file: " + s) + } + return b +} + +// newUtxoViewpoint returns a new utxo view populated with outputs of the provided source transactions as if there were available at the respective block height specified in the heights slice. The length of the source txns and source tx heights must match or it will panic. +func newUtxoViewpoint( sourceTxns []*wire.MsgTx, sourceTxHeights []int32) *blockchain.UtxoViewpoint { + if len(sourceTxns) != len(sourceTxHeights) { + panic("each transaction must have its block height specified") + } + view := blockchain.NewUtxoViewpoint() + for i, tx := range sourceTxns { + view.AddTxOuts(util.NewTx(tx), sourceTxHeights[i]) + } + return view +} + +// TestCalcPriority ensures the priority calculations work as intended. +func TestCalcPriority( t *testing.T) { + // commonSourceTx1 is a valid transaction used in the tests below as an input to transactions that are having their priority calculated. + // From block 7 in main blockchain. + // tx 0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9 + commonSourceTx1 := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: wire.MaxPrevOutIndex, + }, + SignatureScript: hexToBytes("04ffff001d0134"), + Sequence: 0xffffffff, + }}, + TxOut: []*wire.TxOut{{ + Value: 5000000000, + PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c5" + + "3bc1eb68a382e97b1482ecad7b148a6909a5cb2e0ead" + + "dfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8" + + "643f656b412a3ac"), + }}, + LockTime: 0, + } + // commonRedeemTx1 is a valid transaction used in the tests below as the transaction to calculate the priority for. + // It originally came from block 170 in main blockchain. + commonRedeemTx1 := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: *newHashFromStr("0437cd7f8525ceed232435" + + "9c2d0ba26006d92d856a9c20fa0241106ee5" + + "a597c9"), + Index: 0, + }, + SignatureScript: hexToBytes("47304402204e45e16932b8af" + + "514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5f" + + "b8cd410220181522ec8eca07de4860a4acdd12909d83" + + "1cc56cbbac4622082221a8768d1d0901"), + Sequence: 0xffffffff, + }}, + TxOut: []*wire.TxOut{{ + Value: 1000000000, + PkScript: hexToBytes("4104ae1a62fe09c5f51b13905f07f06" + + "b99a2f7159b2225f374cd378d71302fa28414e7aab37" + + "397f554a7df5f142c21c1b7303b8a0626f1baded5c72" + + "a704f7e6cd84cac"), + }, { + Value: 4000000000, + PkScript: hexToBytes("410411db93e1dcdb8a016b49840f8c5" + + "3bc1eb68a382e97b1482ecad7b148a6909a5cb2e0ead" + + "dfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8" + + "643f656b412a3ac"), + }}, + LockTime: 0, + } + tests := []struct { + name string // test description + tx *wire.MsgTx // tx to calc priority for + utxoView *blockchain.UtxoViewpoint // inputs to tx + nextHeight int32 // height for priority calc + want float64 // expected priority + }{ + { + name: "one height 7 input, prio tx height 169", + tx: commonRedeemTx1, + utxoView: newUtxoViewpoint([]*wire.MsgTx{commonSourceTx1}, + []int32{7}), + nextHeight: 169, + want: 5e9, + }, + { + name: "one height 100 input, prio tx height 169", + tx: commonRedeemTx1, + utxoView: newUtxoViewpoint([]*wire.MsgTx{commonSourceTx1}, + []int32{100}), + nextHeight: 169, + want: 2129629629.6296296, + }, + { + name: "one height 7 input, prio tx height 100000", + tx: commonRedeemTx1, + utxoView: newUtxoViewpoint([]*wire.MsgTx{commonSourceTx1}, + []int32{7}), + nextHeight: 100000, + want: 3086203703703.7036, + }, + { + name: "one height 100 input, prio tx height 100000", + tx: commonRedeemTx1, + utxoView: newUtxoViewpoint([]*wire.MsgTx{commonSourceTx1}, + []int32{100}), + nextHeight: 100000, + want: 3083333333333.3335, + }, + } + for i, test := range tests { + got := CalcPriority(test.tx, test.utxoView, test.nextHeight) + if got != test.want { + t.Errorf("CalcPriority #%d (%q): unexpected priority "+ + "got %v want %v", i, test.name, got, test.want) + continue + } + } +} diff --git a/pkg/mining/cpu/README.md b/pkg/mining/cpu/README.md new file mode 100755 index 0000000..a766577 --- /dev/null +++ b/pkg/mining/cpu/README.md @@ -0,0 +1,18 @@ +# cpuminer + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/mining/cpuminer) + +## Overview + +This package is currently a work in progress. It works without issue since it used in several of the integration tests, but the API is not really ready for consumption as it has simply been refactored out of the main codebase for now. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/mining/cpuminer +``` + +## License + +Package cpuminer is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/pkg/mining/cpu/_cpuminer.go b/pkg/mining/cpu/_cpuminer.go new file mode 100644 index 0000000..33f312b --- /dev/null +++ b/pkg/mining/cpu/_cpuminer.go @@ -0,0 +1,720 @@ +package cpuminer + +import ( + "errors" + "fmt" + "math/rand" + "runtime" + "sync" + "time" + + "go.uber.org/atomic" + + blockchain "github.com/p9c/p9/pkg/chain" + "github.com/p9c/p9/pkg/chain/config/netparams" + "github.com/p9c/p9/pkg/chain/fork" + chainhash "github.com/p9c/p9/pkg/chain/hash" + "github.com/p9c/p9/pkg/chain/mining" + "github.com/p9c/p9/pkg/chain/wire" + log "github.com/p9c/p9/pkg/logi" + "github.com/p9c/p9/pkg/util" +) + +var tn = time.Now() + +// CPUMiner provides facilities for solving blocks (mining) using the CPU in a +// concurrency-safe manner. It consists of two main goroutines -- a speed +// monitor and a controller for worker goroutines which generate and solve +// blocks. The number of goroutines can be set via the SetMaxGoRoutines +// function, but the default is based on the number of processor cores in the +// system which is typically sufficient. +type CPUMiner struct { + sync.Mutex + b *blockchain.BlockChain + g *mining.BlkTmplGenerator + cfg Config + numWorkers uint32 + started bool + discreteMining bool + submitBlockLock sync.Mutex + wg sync.WaitGroup + workerWg sync.WaitGroup + updateNumWorkers qu.C + queryHashesPerSec chan float64 + updateHashes chan uint64 + speedMonitorQuit qu.C + quit qu.C + rotator atomic.Uint64 +} + +// Config is a descriptor containing the cpu miner configuration. +type Config struct { + // Blockchain gives access for the miner to information about the chain + // + Blockchain *blockchain.BlockChain + // ChainParams identifies which chain parameters the cpu miner is associated + // with. + ChainParams *netparams.Params + // BlockTemplateGenerator identifies the instance to use in order to + // generate block templates that the miner will attempt to solve. + BlockTemplateGenerator *mining.BlkTmplGenerator + // MiningAddrs is a list of payment addresses to use for the generated + // blocks. Each generated block will randomly choose one of them. + MiningAddrs []util.Address + // ProcessBlock defines the function to call with any solved blocks. It + // typically must run the provided block through the same set of rules and + // handling as any other block coming from the network. + ProcessBlock func(*util.Block, blockchain.BehaviorFlags) (bool, error) + // ConnectedCount defines the function to use to obtain how many other peers + // the server is connected to. This is used by the automatic persistent + // mining routine to determine whether or it should attempt mining. This is + // useful because there is no point in mining when not connected to any + // peers since there would no be anyone to send any found blocks to. + ConnectedCount func() int32 + // IsCurrent defines the function to use to obtain whether or not the block + // chain is current. This is used by the automatic persistent mining + // routine to determine whether or it should attempt mining. This is useful + // because there is no point in mining if the chain is not current since any + // solved blocks would be on a side chain and and up orphaned anyways. + IsCurrent func() bool + // Algo is the name of the type of PoW used for the block header. + Algo string + // NumThreads is the number of threads set in the configuration for the + // CPUMiner + NumThreads uint32 + // Solo sets whether the miner will run when not connected + Solo bool +} + +const ( + // // maxNonce is the maximum value a nonce can be in a block header. + // maxNonce = 2 ^ 32 - 1 + // maxExtraNonce is the maximum value an extra nonce used in a coinbase + // transaction can be. + maxExtraNonce = 2 ^ 64 - 1 + // hpsUpdateSecs is the number of seconds to wait in between each update to the hashes per second monitor. + hpsUpdateSecs = 1 + // hashUpdateSec is the number of seconds each worker waits in between + // notifying the speed monitor with how many hashes have been completed + // while they are actively searching for a solution. This is done to reduce + // the amount of syncs between the workers that must be done to keep track + // of the hashes per second. + hashUpdateSecs = 9 +) + +var ( + // defaultNumWorkers is the default number of workers to use for mining and + // is based on the number of processor cores. This helps ensure the system + // stays reasonably responsive under heavy load. + defaultNumWorkers = uint32(runtime.NumCPU()) +) + +// GenerateNBlocks generates the requested number of blocks. It is self +// contained in that it creates block templates and attempts to solve them +// while detecting when it is performing stale work and reacting accordingly by +// generating a new block template. When a block is solved, it is submitted. +// The function returns a list of the hashes of generated blocks. +func (m *CPUMiner) GenerateNBlocks(workerNumber uint32, n uint32, + algo string) ([]*chainhash.Hash, error) { + m.Lock() + L.Warnf("generating %s blocks...", m.cfg.Algo) + // Respond with an error if server is already mining. + if m.started || m.discreteMining { + m.Unlock() + return nil, errors.New("server is already CPU mining; call " + + "`setgenerate 0` before calling discrete `generate` commands") + } + m.started = true + m.discreteMining = true + m.speedMonitorQuit = qu.Ter() + m.wg.Add(1) + go m.speedMonitor() + m.Unlock() + L.Warnf("generating %d blocks", n) + i := uint32(0) + blockHashes := make([]*chainhash.Hash, n) + // Start a ticker which is used to signal checks for stale work and updates to the speed monitor. + ticker := time.NewTicker(time.Second * hashUpdateSecs) + defer ticker.Stop() + for { + // Read updateNumWorkers in case someone tries a `setgenerate` while + // we're generating. We can ignore it as the `generate` RPC call only + // uses 1 worker. + select { + case <-m.updateNumWorkers: + default: + } + // Grab the lock used for block submission, since the current block will + // be changing and this would otherwise end up building a new block + // template on a block that is in the process of becoming stale. + m.submitBlockLock.Lock() + curHeight := m.g.BestSnapshot().Height + // Choose a payment address at random. + rand.Seed(time.Now().UnixNano()) + payToAddr := m.cfg.MiningAddrs[rand.Intn(len(m.cfg.MiningAddrs))] + // Create a new block template using the available transactions in the + // memory pool as a source of transactions to potentially include in the + // block. + template, e := m.g.NewBlockTemplate(workerNumber, payToAddr, algo) + m.submitBlockLock.Unlock() + if e != nil { + L. L.Warnf("failed to create new block template:", err) + continue + } + // Attempt to solve the block. The function will exit early with false + // when conditions that trigger a stale block, so a new block template + // can be generated. When the return is true a solution was found, so + // submit the solved block. + if m.solveBlock(workerNumber, template.Block, curHeight+1, + m.cfg.ChainParams.Name == "testnet", ticker, nil) { + block := util.NewBlock(template.Block) + m.submitBlock(block) + blockHashes[i] = block.Hash() + i++ + if i == n { + L.Warnf("generated %d blocks", i) + m.Lock() + close(m.speedMonitorQuit) + m.wg.Wait() + m.started = false + m.discreteMining = false + m.Unlock() + return blockHashes, nil + } + } + } +} + +// GetAlgo returns the algorithm currently configured for the miner +func (m *CPUMiner) GetAlgo() (name string) { + return m.cfg.Algo +} + +// HashesPerSecond returns the number of hashes per second the mining process +// is performing. 0 is returned if the miner is not currently running. This +// function is safe for concurrent access. +func (m *CPUMiner) HashesPerSecond() float64 { + m.Lock() + defer m.Unlock() + // Nothing to do if the miner is not currently running. + if !m.started { + return 0 + } + return <-m.queryHashesPerSec +} + +// IsMining returns whether or not the CPU miner has been started and is +// therefore currently mining. This function is safe for concurrent access. +func (m *CPUMiner) IsMining() bool { + m.Lock() + defer m.Unlock() + return m.started +} + +// NumWorkers returns the number of workers which are running to solve blocks. +// This function is safe for concurrent access. +func (m *CPUMiner) NumWorkers() int32 { + m.Lock() + defer m.Unlock() + return int32(m.numWorkers) +} + +// SetAlgo sets the algorithm for the CPU miner +func (m *CPUMiner) SetAlgo( + name string) { + m.cfg.Algo = name +} + +// SetNumWorkers sets the number of workers to create which solve blocks. Any +// negative values will cause a default number of workers to be used which is +// based on the number of processor cores in the system. A value of 0 will +// cause all CPU mining to be stopped. This function is safe for concurrent +// access. +func (m *CPUMiner) SetNumWorkers(numWorkers int32) { + if numWorkers == 0 { + m.Stop() + } + // Don't lock until after the first check since Stop does its own locking. + m.Lock() + defer m.Unlock() + // Use default if provided value is negative. + if numWorkers < 0 { + m.numWorkers = defaultNumWorkers + } else { + m.numWorkers = uint32(numWorkers) + } + // When the miner is already running, notify the controller about the the change. + if m.started { + m.updateNumWorkers <- struct{}{} + } +} + +// Start begins the CPU mining process as well as the speed monitor used to +// track hashing metrics. Calling this function when the CPU miner has already +// been started will have no effect. +// This function is safe for concurrent access. +func (m *CPUMiner) Start() { + if len(m.cfg.MiningAddrs) < 1 { + return + } + m.Lock() + defer m.Unlock() + L.inf.Ln("starting cpu miner") + // Nothing to do if the miner is already running or if running in discrete mode (using GenerateNBlocks). + if m.started || m.discreteMining { + return + } + // randomize the starting point so all network is mining different + m.rotator.Store(uint64(rand.Intn( + len(fork.List[fork.GetCurrent(m.b.BestSnapshot().Height)].Algos)))) + m.quit = qu.Ter() + m.speedMonitorQuit = qu.Ter() + m.wg.Add(2) + go m.speedMonitor() + go m.miningWorkerController() + m.started = true + L.trc.Ln("CPU miner started mining", m.cfg.Algo, m.cfg.NumThreads) +} + +// Stop gracefully stops the mining process by signalling all workers, and the +// speed monitor to quit. Calling this function when the CPU miner has not +// already been started will have no effect. This function is safe for +// concurrent access. +func (m *CPUMiner) Stop() { + m.Lock() + defer m.Unlock() + // Nothing to do if the miner is not currently running or if running in discrete mode (using GenerateNBlocks). + if !m.started || m.discreteMining { + return + } + close(m.quit) + m.wg.Wait() + m.started = false + L.wrn.Ln("CPU miner stopped") +} + +// generateBlocks is a worker that is controlled by the miningWorkerController. +// It is self contained in that it creates block templates and attempts to +// solve them while detecting when it is performing stale work and reacting +// accordingly by generating a new block template. When a block is solved, it +// is submitted. It must be run as a goroutine. +func (m *CPUMiner) generateBlocks(workerNumber uint32, quit qu.C) { + // Start a ticker which is used to signal checks for stale work and updates + // to the speed monitor. + ticker := time.NewTicker(time.Second / 3) // * hashUpdateSecs) + defer ticker.Stop() +out: + for i := 0; ; i++ { + //L.trc.Ln(workerNumber, "generateBlocksLoop start") + // Quit when the miner is stopped. + select { + case <-quit: + break out + default: + // Non-blocking select to fall through + } + // Wait until there is a connection to at least one other peer since + // there is no way to relay a found block or receive transactions to work + // on when there are no connected peers. + if (m.cfg.ConnectedCount() == 0 || !m.cfg.IsCurrent()) && + (m.cfg.ChainParams.Net == wire.MainNet && !m.cfg.Solo) { + log.Print(log.Composite("server has no peers, waiting...", + "STATUS", true), "\r") + time.Sleep(time.Second) + continue + } + select { + case <-quit: + break out + default: + // Non-blocking select to fall through + } + // No point in searching for a solution before the chain is synced. Also, + // grab the same lock as used for block submission, since the current + // block will be changing and this would otherwise end up building a new + // block template on a block that is in the process of becoming stale. + curHeight := m.g.BestSnapshot().Height + if curHeight != 0 && !m.cfg.IsCurrent() && !m.cfg.Solo { + L.Warnf("server is not current yet, waiting") + time.Sleep(time.Second) + continue + } + select { + case <-quit: + break out + default: + // Non-blocking select to fall through + } + // choose the algorithm on a rolling cycle + counter := m.rotator.Load() + //counter /= uint64(len(fork.List[fork.GetCurrent(curHeight+1)].Algos))*2 + m.rotator.Add(1) + algo := "sha256d" + switch fork.GetCurrent(curHeight + 1) { + case 0: + if counter&1 == 1 { + algo = "sha256d" + } else { + algo = "scrypt" + } + case 1: + l9 := uint64(len(fork.P9AlgoVers)) + mod := counter % l9 + algo = fork.P9AlgoVers[int32(mod+5)] + // L.wrn.Ln("algo", algo) + } + select { + case <-quit: + break out + default: + // Non-blocking select to fall through + } + // Choose a payment address at random. + rand.Seed(time.Now().UnixNano()) + payToAddr := m.cfg.MiningAddrs[rand.Intn(len(m.cfg.MiningAddrs))] + // Create a new block template using the available transactions in the + // memory pool as a source of transactions to potentially include in the + // block. + template, e := m.g.NewBlockTemplate(workerNumber, payToAddr, algo) + if e != nil { + L.wrn.Ln("failed to create new block template:", err) + continue + } + // Attempt to solve the block. The function will exit early with false + // when conditions that trigger a stale block, so a new block template + // can be generated. When the return is true a solution was found, so + // submit the solved block. + //L.trc.Ln("attempting to solve block") + select { + case <-quit: + break out + default: + // Non-blocking select to fall through + } + if m.solveBlock(workerNumber, template.Block, curHeight+1, + m.cfg.ChainParams.Name == "testnet", ticker, quit) { + block := util.NewBlock(template.Block) + m.submitBlock(block) + } + } + L.trc.Ln("cpu miner worker finished") + m.workerWg.Done() +} + +// miningWorkerController launches the worker goroutines that are used to +// generate block templates and solve them. It also provides the ability to +// dynamically adjust the number of running worker goroutines. It must be run +// as a goroutine. +func (m *CPUMiner) miningWorkerController() { + L.trc.Ln("starting mining worker controller") + // launchWorkers groups common code to launch a specified number of workers + // for generating blocks. + var runningWorkers qu.Ters + launchWorkers := func(numWorkers uint32) { + for i := uint32(0); i < numWorkers; i++ { + quit := qu.Ter() + runningWorkers = append(runningWorkers, quit) + m.workerWg.Add(1) + go m.generateBlocks(i, quit) + } + } + L.Tracef("spawning %d worker(s)", m.numWorkers) + // Launch the current number of workers by default. + runningWorkers = make(qu.Ters, 0, m.numWorkers) + launchWorkers(m.numWorkers) +out: + for { + select { + // Update the number of running workers. + case <-m.updateNumWorkers: + // No change. + numRunning := uint32(len(runningWorkers)) + if m.numWorkers == numRunning { + continue + } + // Add new workers. + if m.numWorkers > numRunning { + launchWorkers(m.numWorkers - numRunning) + continue + } + // Signal the most recently created goroutines to exit. + for i := numRunning - 1; i >= m.numWorkers; i-- { + close(runningWorkers[i]) + runningWorkers[i] = nil + runningWorkers = runningWorkers[:i] + } + case <-m.quit: + for _, quit := range runningWorkers { + close(quit) + } + break out + } + } + // Wait until all workers shut down to stop the speed monitor since they rely on being able to send updates to it. + m.workerWg.Wait() + close(m.speedMonitorQuit) + m.wg.Done() +} + +// solveBlock attempts to find some combination of a nonce, extra nonce, and +// current timestamp which makes the passed block hash to a value less than the +// target difficulty. The timestamp is updated periodically and the passed +// block is modified with all tweaks during this process. This means that when +// the function returns true, the block is ready for submission. This function +// will return early with false when conditions that trigger a stale block such +// as a new block showing up or periodically when there are new transactions +// and enough time has elapsed without finding a solution. +func (m *CPUMiner) solveBlock(workerNumber uint32, msgBlock *wire.MsgBlock, + blockHeight int32, testnet bool, ticker *time.Ticker, + quit qu.C) bool { + //L.trc.Ln("running solveBlock") + // algoName := fork.GetAlgoName( + // msgBlock.Header.Version, m.b.BestSnapshot().Height) + // Choose a random extra nonce offset for this block template and worker. + enOffset, e := wire.RandomUint64() + if e != nil { + L. L.Warnf("unexpected error while generating random extra nonce"+ + " offset:", + err) + enOffset = 0 + } + // Create some convenience variables. + header := &msgBlock.Header + targetDifficulty := fork.CompactToBig(header.Bits) + // Initial state. + lastGenerated := time.Now() + lastTxUpdate := m.g.GetTxSource().LastUpdated() + hashesCompleted := uint64(0) + // Note that the entire extra nonce range is iterated and the offset is + // added relying on the fact that overflow will wrap around 0 as provided by + // the Go spec. + eN, _ := wire.RandomUint64() + // now := time.Now() + // for extraNonce := eN; extraNonce < eN+maxExtraNonce; extraNonce++ { + did := false + extraNonce := eN + // we only do this once + for !did { + did = true + // Update the extra nonce in the block template with the new value by + // regenerating the coinbase script and setting the merkle root to the + // new value. + //L.trc.Ln("updating extraNonce") + e := m.g.UpdateExtraNonce(msgBlock, blockHeight, extraNonce+enOffset) + if e != nil { + L. } + // Search through the entire nonce range for a solution while + // periodically checking for early quit and stale block conditions along + // with updates to the speed monitor. + var shifter uint64 = 16 + if testnet { + shifter = 16 + } + rn, _ := wire.RandomUint64() + if rn > 1<<63-1< time.Second*3 { + // return false + // } + select { + case <-quit: + return false + case <-ticker.C: + m.updateHashes <- hashesCompleted + hashesCompleted = 0 + // The current block is stale if the best block has changed. + best := m.g.BestSnapshot() + if !header.PrevBlock.IsEqual(&best.Hash) { + return false + } + // The current block is stale if the memory pool has been updated + // since the block template was generated and it has been at least + // one minute. + if lastTxUpdate != m.g.GetTxSource().LastUpdated() && + time.Now().After(lastGenerated.Add(time.Minute)) { + return false + } + e := m.g.UpdateBlockTime(workerNumber, msgBlock) + if e != nil { + L. } + default: + } + var incr uint64 = 1 + header.Nonce = i + hash := header.BlockHashWithAlgos(blockHeight) + hashesCompleted += incr + // The block is solved when the new block hash is less than the target + // difficulty. Yay! + bigHash := blockchain.HashToBig(&hash) + if bigHash.Cmp(targetDifficulty) <= 0 { + m.updateHashes <- hashesCompleted + return true + } + } + return false + } + return false +} + +// speedMonitor handles tracking the number of hashes per second the mining +// process is performing. It must be run as a goroutine. +func (m *CPUMiner) speedMonitor() { + var hashesPerSec float64 + var totalHashes uint64 = 1 + ticker := time.NewTicker(time.Second * hpsUpdateSecs) + defer ticker.Stop() +out: + for i := 0; ; i++ { + select { + // Periodic updates from the workers with how many hashes they have + // performed. + case numHashes := <-m.updateHashes: + totalHashes += numHashes + // Time to update the hashes per second. + case <-ticker.C: + //curHashesPerSec := float64(totalHashes) / hpsUpdateSecs + //if hashesPerSec == 0 { + // hashesPerSec = curHashesPerSec + //} + //hashesPerSec = (hashesPerSec + curHashesPerSec) / 2 + ////totalHashes = 0 + //if hashesPerSec != 0 { + //since := fmt.Sprint(time.Now().Sub(log.StartupTime) / time. + // Second * time.Second) + since := uint64(time.Now().Sub(tn)/time.Second) + 1 + log.Print(log.Composite(fmt.Sprintf( + "--> Hash speed: %d hash/s av since start", + totalHashes/since), + "STATUS", true), + "\r") + //} + // Request for the number of hashes per second. + case m.queryHashesPerSec <- hashesPerSec: + // Nothing to do. + case <-m.speedMonitorQuit: + break out + } + } + m.wg.Done() +} + +// submitBlock submits the passed block to network after ensuring it passes all +// of the consensus validation rules. +func (m *CPUMiner) submitBlock(block *util.Block) bool { + L.trc.Ln("submitting block") + m.submitBlockLock.Lock() + defer m.submitBlockLock.Unlock() + // TODO: This nonsense and the workgroup are the entirely wrong way to + // write a cryptocurrency miner. + // It is not critical work so it should not use a workgroup but rather, + // a semaphore, which can yank all the threads to stop as soon as they + // select on the semaphore, + // whereas this stops 'when all the jobs are finished' for what purpose? + // This miner will be eliminated once the replacement is complete. + // End result of this is node waits for miners to stop, + // which sometimes takes 5 seconds and almost every other height it has + // two submissions processing on one mutex and second and others are + // always stale. So also, the submitlock needs to be revised, + // I think submitlock and waitgroup together are far better replaced by + // a semaphore. Miner should STOP DEAD when a solution is found and wait + // for more work. The kopach controller will stop all miners in the + // network when it receives submissions prejudicially because it is + // better to save power than catch one block in a thousand from an + // economics poinnt of view. + // Every cycle degrades the value and brings closer the hardware failure + // so don't work unless there is a very good reason to. + // Ensure the block is not stale since a new block could have shown up while + // the solution was being found. Typically that condition is detected and + // all work on the stale block is halted to start work on a new block, but + // the check only happens periodically, so it is possible a block was found + // and submitted in between. + msgBlock := block.MsgBlock() + if !msgBlock.Header.PrevBlock.IsEqual(&m.g.BestSnapshot().Hash) { + L.trc.Ln( + "Block submitted via CPU miner with previous block", msgBlock.Header.PrevBlock, + "is stale", msgBlock.Header.Version, + msgBlock.BlockHashWithAlgos(block.Height())) + return false + } + //L.trc.Ln("found block is fresh ", m.cfg.ProcessBlock) + // Process this block using the same rules as blocks coming from other + // nodes. This will in turn relay it to the network like normal. + isOrphan, e := m.cfg.ProcessBlock(block, blockchain.BFNone) + if e != nil { + L. // Anything other than a rule violation is an unexpected error, so log + // that error as an internal error. + if _, ok := err.(blockchain.RuleError); !ok { + L.Warnf( + "Unexpected error while processing block submitted via CPU miner:", err, + ) + return false + } + L.Warnf("block submitted via CPU miner rejected:", err) + return false + } + if isOrphan { + L.wrn.Ln("block is an orphan") + return false + } + //L.trc.Ln("the block was accepted") + coinbaseTx := block.MsgBlock().Transactions[0].TxOut[0] + prevHeight := block.Height() - 1 + prevBlock, _ := m.b.BlockByHeight(prevHeight) + prevTime := prevBlock.MsgBlock().Header.Timestamp.Unix() + since := block.MsgBlock().Header.Timestamp.Unix() - prevTime + bHash := block.MsgBlock().BlockHashWithAlgos(block.Height()) + L.Warnf("new block height %d %08x %s%10d %08x %v %s %ds since prev", + block.Height(), + prevBlock.MsgBlock().Header.Bits, + bHash, + block.MsgBlock().Header.Timestamp.Unix(), + block.MsgBlock().Header.Bits, + util.Amount(coinbaseTx.Value), + fork.GetAlgoName(block.MsgBlock().Header.Version, block.Height()), + since) + return true +} + +// New returns a new instance of a CPU miner for the provided configuration. +// Use Start to begin the mining process. See the documentation for CPUMiner +// type for more details. +func New(cfg *Config) *CPUMiner { + return &CPUMiner{ + b: cfg.Blockchain, + g: cfg.BlockTemplateGenerator, + cfg: *cfg, + numWorkers: cfg.NumThreads, + updateNumWorkers: qu.Ter(), + queryHashesPerSec: make(chan float64), + updateHashes: make(chan uint64), + } +} diff --git a/pkg/mining/log.go b/pkg/mining/log.go new file mode 100644 index 0000000..dff06b3 --- /dev/null +++ b/pkg/mining/log.go @@ -0,0 +1,43 @@ +package mining + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/mining/mining.go b/pkg/mining/mining.go new file mode 100644 index 0000000..8a07fcb --- /dev/null +++ b/pkg/mining/mining.go @@ -0,0 +1,993 @@ +package mining + +import ( + "container/heap" + "fmt" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/bits" + block2 "github.com/p9c/p9/pkg/block" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/fork" + "math/rand" + "time" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // MinHighPriority is the minimum priority value that allows a transaction to be + // considered high priority. + MinHighPriority = amt.SatoshiPerBitcoin * 144.0 / 250 + // blockHeaderOverhead is the max number of bytes it takes to serialize a block + // header and max possible transaction count. + blockHeaderOverhead = wire.MaxBlockHeaderPayload + wire.MaxVarIntPayload + // CoinbaseFlags is added to the coinbase script of a generated block and is + // used to monitor BIP16 support as well as blocks that are generated via pod. + CoinbaseFlags = "/P2SH/pod/" +) + +type ( + // TxDesc is a descriptor about a transaction in a transaction source along with + // additional metadata. + TxDesc struct { + // Tx is the transaction associated with the entry. + Tx *util.Tx + // Added is the time when the entry was added to the source pool. + Added time.Time + // Height is the block height when the entry was added to the the source pool. + Height int32 + // Fee is the total fee the transaction associated with the entry pays. + Fee int64 + // FeePerKB is the fee the transaction pays in Satoshi per 1000 bytes. + FeePerKB int64 + } + // TxSource represents a source of transactions to consider for inclusion in new + // blocks. The interface contract requires that all of these methods are safe + // for concurrent access with respect to the source. + TxSource interface { + // LastUpdated returns the last time a transaction was added to or removed from + // the source pool. + LastUpdated() time.Time + // MiningDescs returns a slice of mining descriptors for all the transactions in + // the source pool. + MiningDescs() []*TxDesc + // HaveTransaction returns whether or not the passed transaction hash exists in + // the source pool. + HaveTransaction(hash *chainhash.Hash) bool + } + // txPrioItem houses a transaction along with extra information that allows the + // transaction to be prioritized and track dependencies on other transactions + // which have not been mined into a block yet. + txPrioItem struct { + tx *util.Tx + fee int64 + priority float64 + feePerKB int64 + // dependsOn holds a map of transaction hashes which this one depends on. + // + // It will only be set when the transaction references other transactions in the + // source pool and hence must come after them in a block. + dependsOn map[chainhash.Hash]struct{} + } + // txPriorityQueueLessFunc describes a function that can be used as a compare + // function for a transaction priority queue (txPriorityQueue). + txPriorityQueueLessFunc func(*txPriorityQueue, int, int) bool + // txPriorityQueue implements a priority queue of txPrioItem elements that + // supports an arbitrary compare function as defined by txPriorityQueueLessFunc. + txPriorityQueue struct { + lessFunc txPriorityQueueLessFunc + items []*txPrioItem + } + // BlockTemplate houses a block that has yet to be solved along with additional + // details about the fees and the number of signature operations for each + // transaction in the block. + BlockTemplate struct { + // Block is a block that is ready to be solved by miners. Thus, it is completely + // valid with the exception of satisfying the proof-of-work requirement. + Block *wire.Block + // Fees contains the amount of fees each transaction in the generated template + // pays in base units. Since the first transaction is the coinbase, the first + // entry (offset 0) will contain the negative of the sum of the fees of all + // other transactions. + Fees []int64 + // SigOpCosts contains the number of signature operations each transaction in + // the generated template performs. + SigOpCosts []int64 + // Height is the height at which the block template connects to the main chain. + Height int32 + // ValidPayAddress indicates whether or not the template coinbase pays to an + // address or is redeemable by anyone. See the documentation on NewBlockTemplate + // for details on which this can be useful to generate templates without a + // coinbase payment address. + ValidPayAddress bool + // WitnessCommitment is a commitment to the witness data (if any) within the + // block. This field will only be populated once segregated witness has been + // activated, and the block contains a transaction which has witness data. + WitnessCommitment []byte + } + // BlkTmplGenerator provides a type that can be used to generate block templates + // based on a given mining policy and source of transactions to choose from. It + // also houses additional state required in order to ensure the templates are + // built on top of the current best chain and adhere to the consensus rules. + BlkTmplGenerator struct { + Policy *Policy + ChainParams *chaincfg.Params + TxSource TxSource + Chain *blockchain.BlockChain + TimeSource blockchain.MedianTimeSource + SigCache *txscript.SigCache + HashCache *txscript.HashCache + } +) + +// Len returns the number of items in the priority queue. +// +// It is part of the heap.Interface implementation. +func (pq *txPriorityQueue) Len() int { + return len(pq.items) +} + +// Less returns whether the item in the priority queue with index i should txsort +// before the item with index j by deferring to the assigned less function. +// +// It is part of the heap.Interface implementation. +func (pq *txPriorityQueue) Less(i, j int) bool { + return pq.lessFunc(pq, i, j) +} + +// Swap swaps the items at the passed indices in the priority queue. +// +// It is part of the heap.Interface implementation. +func (pq *txPriorityQueue) Swap(i, j int) { + pq.items[i], pq.items[j] = pq.items[j], pq.items[i] +} + +// Push pushes the passed item onto the priority queue. +// +// It is part of the heap.Interface implementation. +func (pq *txPriorityQueue) Push(x interface{}) { + pq.items = append(pq.items, x.(*txPrioItem)) +} + +// Pop removes the highest priority item (according to Less) from the priority +// queue and returns it. +// +// It is part of the heap.Interface implementation. +func (pq *txPriorityQueue) Pop() interface{} { + n := len(pq.items) + item := pq.items[n-1] + pq.items[n-1] = nil + pq.items = pq.items[0 : n-1] + return item +} + +// SetLessFunc sets the compare function for the priority queue to the provided +// function. It also invokes heap.Init on the priority queue using the new +// function so it can immediately be used with heap.Push/Pop. +func (pq *txPriorityQueue) SetLessFunc(lessFunc txPriorityQueueLessFunc) { + pq.lessFunc = lessFunc + heap.Init(pq) +} + +// txPQByPriority sorts a txPriorityQueue by transaction priority and then fees +// per kilobyte. +func txPQByPriority(pq *txPriorityQueue, i, j int) bool { + // Using > here so that pop gives the highest priority item as opposed to the + // lowest. Sort by priority first, then fee. + if pq.items[i].priority == pq.items[j].priority { + return pq.items[i].feePerKB > pq.items[j].feePerKB + } + return pq.items[i].priority > pq.items[j].priority +} + +// txPQByFee sorts a txPriorityQueue by fees per kilobyte and then transaction +// priority. +func txPQByFee(pq *txPriorityQueue, i, j int) bool { + // Using > here so that pop gives the highest fee item as opposed to the lowest. + // Sort by fee first, then priority. + if pq.items[i].feePerKB == pq.items[j].feePerKB { + return pq.items[i].priority > pq.items[j].priority + } + return pq.items[i].feePerKB > pq.items[j].feePerKB +} + +// newTxPriorityQueue returns a new transaction priority queue that reserves the +// passed amount of space for the elements. The new priority queue uses either +// the txPQByPriority or the txPQByFee compare function depending on the +// sortByFee parameter and is already initialized for use with heap.Push/Pop. +// The priority queue can grow larger than the reserved space, but extra copies +// of the underlying array can be avoided by reserving a sane value. +func newTxPriorityQueue(reserve int, sortByFee bool) *txPriorityQueue { + pq := &txPriorityQueue{ + items: make([]*txPrioItem, 0, reserve), + } + if sortByFee { + pq.SetLessFunc(txPQByFee) + } else { + pq.SetLessFunc(txPQByPriority) + } + return pq +} + +// mergeUtxoView adds all of the entries in viewB to viewA. The result is that +// viewA will contain all of its original entries plus all of the entries in +// viewB. It will replace any entries in viewB which also exist in viewA if the +// entry in viewA is spent. +func mergeUtxoView(viewA *blockchain.UtxoViewpoint, viewB *blockchain.UtxoViewpoint) { + viewAEntries := viewA.Entries() + for outpoint, entryB := range viewB.Entries() { + if entryA, exists := viewAEntries[outpoint]; !exists || + entryA == nil || entryA.IsSpent() { + viewAEntries[outpoint] = entryB + } + } +} + +// standardCoinbaseScript returns a standard script suitable for use as the +// signature script of the coinbase transaction of a new block. In particular, +// it starts with the block height that is required by version 2 blocks and adds +// the extra nonce as well as additional coinbase flags. +func standardCoinbaseScript(nextBlockHeight int32, extraNonce uint64) ([]byte, error) { + return txscript.NewScriptBuilder().AddInt64(int64(nextBlockHeight)). + AddInt64(int64(extraNonce)). + AddData([]byte(CoinbaseFlags)). + Script() +} + +// createCoinbaseTx returns a coinbase transaction paying an appropriate subsidy +// based on the passed block height to the provided address. When the address is +// nil, the coinbase transaction will instead be redeemable by anyone. See the +// comment for NewBlockTemplate for more information about why the nil address +// handling is useful. +func createCoinbaseTx( + params *chaincfg.Params, coinbaseScript []byte, nextBlockHeight int32, + addr btcaddr.Address, version int32, +) (*util.Tx, error) { + // if this is the hard fork activation height coming up, we create the special + // disbursement coinbase + if nextBlockHeight == fork.List[1].ActivationHeight && + params.Net == wire.MainNet || + nextBlockHeight == fork.List[1].TestnetStart && + params.Net == wire.TestNet3 { + return blockchain.CreateHardForkSubsidyTx(params, coinbaseScript, nextBlockHeight, addr, version) + } + + // Create the script to pay to the provided payment address if one was + // specified. Otherwise create a script that allows the coinbase to be + // redeemable by anyone. + var pkScript []byte + if addr != nil { + var e error + pkScript, e = txscript.PayToAddrScript(addr) + if e != nil { + return nil, e + } + } else { + var e error + scriptBuilder := txscript.NewScriptBuilder() + pkScript, e = scriptBuilder.AddOp(txscript.OP_TRUE).Script() + if e != nil { + return nil, e + } + } + tx := wire.NewMsgTx(wire.TxVersion) + tx.AddTxIn( + &wire.TxIn{ + // Coinbase transactions have no inputs, so previous outpoint is zero hash and + // max index. + PreviousOutPoint: *wire.NewOutPoint( + &chainhash.Hash{}, + wire.MaxPrevOutIndex, + ), + SignatureScript: coinbaseScript, + Sequence: wire.MaxTxInSequenceNum, + }, + ) + tx.AddTxOut( + &wire.TxOut{ + Value: blockchain.CalcBlockSubsidy(nextBlockHeight, params, version), + PkScript: pkScript, + }, + ) + return util.NewTx(tx), nil +} + +// spendTransaction updates the passed view by marking the inputs to the passed +// transaction as spent. It also adds all outputs in the passed transaction +// which are not provably unspendable as available unspent transaction outputs. +func spendTransaction(utxoView *blockchain.UtxoViewpoint, tx *util.Tx, height int32) (e error) { + for _, txIn := range tx.MsgTx().TxIn { + entry := utxoView.LookupEntry(txIn.PreviousOutPoint) + if entry != nil { + entry.Spend() + } + } + utxoView.AddTxOuts(tx, height) + return nil +} + +// logSkippedDeps logs any dependencies which are also skipped as a result of +// skipping a transaction while generating a block template at the trace level. +func logSkippedDeps(tx *util.Tx, deps map[chainhash.Hash]*txPrioItem) { + if deps == nil { + return + } + for _, item := range deps { + T.F( + "skipping tx %s since it depends on %s", item.tx.Hash(), + tx.Hash(), + ) + } +} + +// MinimumMedianTime returns the minimum allowed timestamp for a block building +// on the end of the provided best chain. In particular, it is one second after +// the median timestamp of the last several blocks per the chain consensus +// rules. +func MinimumMedianTime(chainState *blockchain.BestState) time.Time { + return chainState.MedianTime.Add(time.Second) +} + +// medianAdjustedTime returns the current time adjusted to ensure it is at least +// one second after the median timestamp of the last several blocks per the +// chain consensus rules. +func medianAdjustedTime(chainState *blockchain.BestState, timeSource blockchain.MedianTimeSource) time.Time { + // The timestamp for the block must not be before the median timestamp of the + // last several blocks. Thus, choose the maximum between the current time and + // one second after the past median time. The current timestamp is truncated to + // a second boundary before comparison since a block timestamp does not + // supported a precision greater than one second. + newTimestamp := timeSource.AdjustedTime() + minTimestamp := MinimumMedianTime(chainState) + if newTimestamp.Before(minTimestamp) { + newTimestamp = minTimestamp + } + return newTimestamp +} + +// NewBlkTmplGenerator returns a new block template generator for the given +// policy using transactions from the provided transaction source. The +// additional state-related fields are required in order to ensure the templates +// are built on top of the current best chain and adhere to the consensus rules. +func NewBlkTmplGenerator( + policy *Policy, params *chaincfg.Params, + txSource TxSource, chain *blockchain.BlockChain, + timeSource blockchain.MedianTimeSource, + sigCache *txscript.SigCache, + hashCache *txscript.HashCache, +) *BlkTmplGenerator { + return &BlkTmplGenerator{ + Policy: policy, + ChainParams: params, + TxSource: txSource, + Chain: chain, + TimeSource: timeSource, + SigCache: sigCache, + HashCache: hashCache, + } +} + +// NewBlockTemplate returns a new block template that is ready to be solved +// using the transactions from the passed transaction source pool and a coinbase +// that either pays to the passed address if it is not nil, or a coinbase that +// is redeemable by anyone if the passed address is nil. The nil address +// functionality is useful since there are cases such as the getblocktemplate +// RPC where external mining software is responsible for creating their own +// coinbase which will replace the one generated for the block template. Thus +// the need to have configured address can be avoided. +// +// The transactions selected and included are prioritized according to several +// factors. First, each transaction has a priority calculated based on its +// value, age of inputs, and size. +// +// Transactions which consist of larger amounts, older inputs, and small txsizes +// have the highest priority. Second, a fee per kilobyte is calculated for each +// transaction. Transactions with a higher fee per kilobyte are preferred. +// Finally, the block generation related policy settings are all taken into +// account. +// +// Transactions which only spend outputs from other transactions already in the +// block chain are immediately added to a priority queue which either +// prioritizes based on the priority (then fee per kilobyte) or the fee per +// kilobyte (then priority) depending on whether or not the BlockPrioritySize +// policy setting allots space for high-priority transactions. +// +// Transactions which spend outputs from other transactions in the source pool +// are added to a dependency map so they can be added to the priority queue once +// the transactions they depend on have been included. Once the high-priority +// area (if configured) has been filled with transactions, or the priority falls +// below what is considered high-priority, the priority queue is updated to +// prioritize by fees per kilobyte (then priority). +// +// When the fees per kilobyte drop below the TxMinFreeFee policy setting, the +// transaction will be skipped unless the BlockMinSize policy setting is +// nonzero, in which case the block will be filled with the low-fee/free +// transactions until the block size reaches that minimum size. Any transactions +// which would cause the block to exceed the BlockMaxSize policy setting, exceed +// the maximum allowed signature operations per block, or otherwise cause the +// block to be invalid are skipped. +// +// Given the above, a block generated by this function is of the following form: +// +// ----------------------------------- -- -- +// | Coinbase Transaction | | | +// |-----------------------------------| | | +// | | | | ----- policy.BlockPrioritySize +// | High-priority Transactions | | | +// | | | | +// |-----------------------------------| | -- +// | | | +// | | | +// | | |--- policy.BlockMaxSize +// | Transactions prioritized by fee | | +// | until <= policy.TxMinFreeFee | | +// | | | +// | | | +// | | | +// |-----------------------------------| | +// | Low-fee/Non high-priority (free) | | +// | transactions (while block size | | +// | <= policy.BlockMinSize) | | +// ----------------------------------- -- +func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress btcaddr.Address, algo string,) (*BlockTemplate, error) { + T.Ln("NewBlockTemplate", algo) + if algo == "" { + algo = "random" + } + // Extend the most recently known best block. + best := g.Chain.BestSnapshot() + nextBlockHeight := best.Height + 1 + // sanitise the version number + vers := fork.GetAlgoVer(algo, nextBlockHeight) + algo = fork.GetAlgoName(vers, nextBlockHeight) + // Create a standard coinbase transaction paying to the provided address. + // + // NOTE: The coinbase value will be updated to include the fees from the + // selected transactions later after they have actually been selected. It is + // created here to detect any errors early before potentially doing a lot of + // work below. The extra nonce helps ensure the transaction is not a duplicate + // transaction (paying the same value to the same public key address would + // otherwise be an identical transaction for block version 1). + rand.Seed(time.Now().UnixNano()) + extraNonce := rand.Uint64() + var e error + var coinbaseScript []byte + if coinbaseScript, e = standardCoinbaseScript(nextBlockHeight, extraNonce); E.Chk(e) { + return nil, e + } + var coinbaseTx *util.Tx + if coinbaseTx, e = createCoinbaseTx( + g.ChainParams, coinbaseScript, nextBlockHeight, payToAddress, + vers, + ); E.Chk(e) { + return nil, e + } + coinbaseSigOpCost := int64(blockchain.CountSigOps(coinbaseTx)) + // Get the current source transactions and create a priority queue to hold the + // transactions which are ready for inclusion into a block along with some + // priority related and fee metadata. Reserve the same number of items that are + // available for the priority queue. Also, choose the initial txsort order for the + // priority queue based on whether or not there is an area allocated for + // high-priority transactions. + sourceTxns := g.TxSource.MiningDescs() + sortedByFee := g.Policy.BlockPrioritySize == 0 + priorityQueue := newTxPriorityQueue(len(sourceTxns), sortedByFee) + // Create a slice to hold the transactions to be included in the generated block + // with reserved space. Also create a utxo view to house all of the input + // transactions so multiple lookups can be avoided. + blockTxns := make([]*util.Tx, 0, len(sourceTxns)) + blockTxns = append(blockTxns, coinbaseTx) + blockUtxos := blockchain.NewUtxoViewpoint() + // dependers is used to track transactions which depend on another transaction + // in the source pool. This, in conjunction with the dependsOn map kept with + // each dependent transaction helps quickly determine which dependent + // transactions are now eligible for inclusion in the block once each + // transaction has been included. + dependers := make(map[chainhash.Hash]map[chainhash.Hash]*txPrioItem) + // Create slices to hold the fees and number of signature operations for each of + // the selected transactions and add an entry for the coinbase. This allows the + // code below to simply append details about a transaction as it is selected for + // inclusion in the final block. However, since the total fees aren't known yet, + // use a dummy value for the coinbase fee which will be updated later. + txFees := make([]int64, 0, len(sourceTxns)) + txSigOpCosts := make([]int64, 0, len(sourceTxns)) + txFees = append(txFees, -1) // Updated once known + txSigOpCosts = append(txSigOpCosts, coinbaseSigOpCost) + T.F("considering %d transactions for inclusion to new block", len(sourceTxns)) +mempoolLoop: + for _, txDesc := range sourceTxns { + // A block can't have more than one coinbase or contain non-finalized + // transactions. + tx := txDesc.Tx + if blockchain.IsCoinBase(tx) { + T.C( + func() string { + return fmt.Sprintf("skipping coinbase tx %s", tx.Hash()) + }, + ) + continue + } + if !blockchain.IsFinalizedTransaction( + tx, nextBlockHeight, + g.TimeSource.AdjustedTime(), + ) { + T.C( + func() string { + return "skipping non-finalized tx " + tx.Hash().String() + }, + ) + continue + } + // Fetch all of the utxos referenced by the this transaction. + // + // NOTE: This intentionally does not fetch inputs from the mempool since a + // transaction which depends on other transactions in the mempool must come + // after those dependencies in the final generated block. + var utxos *blockchain.UtxoViewpoint + utxos, e = g.Chain.FetchUtxoView(tx) + if e != nil { + W.C( + func() string { + return "unable to fetch utxo view for tx " + tx.Hash().String() + ": " + e.Error() + }, + ) + continue + } + // Setup dependencies for any transactions which reference other transactions in + // the mempool so they can be properly ordered below. + prioItem := &txPrioItem{tx: tx} + for _, txIn := range tx.MsgTx().TxIn { + originHash := &txIn.PreviousOutPoint.Hash + entry := utxos.LookupEntry(txIn.PreviousOutPoint) + if entry == nil || entry.IsSpent() { + if !g.TxSource.HaveTransaction(originHash) { + T.C( + func() string { + return "skipping tx %s because it references unspent output %s which is not available" + + tx.Hash().String() + + txIn.PreviousOutPoint.String() + }, + ) + continue mempoolLoop + } + // The transaction is referencing another transaction in the source pool, so + // setup an ordering dependency. + deps, exists := dependers[*originHash] + if !exists { + deps = make(map[chainhash.Hash]*txPrioItem) + dependers[*originHash] = deps + } + deps[*prioItem.tx.Hash()] = prioItem + if prioItem.dependsOn == nil { + prioItem.dependsOn = make( + map[chainhash.Hash]struct{}, + ) + } + prioItem.dependsOn[*originHash] = struct{}{} + // Skip the check below. We already know the referenced transaction is + // available. + continue + } + } + // Calculate the final transaction priority using the input value age sum as + // well as the adjusted transaction size. The formula is: sum (inputValue * + // inputAge) / adjustedTxSize + prioItem.priority = CalcPriority( + tx.MsgTx(), utxos, + nextBlockHeight, + ) + // Calculate the fee in Satoshi/kB. + prioItem.feePerKB = txDesc.FeePerKB + prioItem.fee = txDesc.Fee + // Add the transaction to the priority queue to mark it ready for inclusion in + // the block unless it has dependencies. + if prioItem.dependsOn == nil { + heap.Push(priorityQueue, prioItem) + } + // Merge the referenced outputs from the input transactions to this transaction + // into the block utxo view. This allows the code below to avoid a second + // lookup. + mergeUtxoView(blockUtxos, utxos) + } + // The starting block size is the size of the block header plus the max possible + // transaction count size, plus the size of the coinbase transaction. + blockWeight := uint32((blockHeaderOverhead) + blockchain.GetTransactionWeight(coinbaseTx)) + blockSigOpCost := coinbaseSigOpCost + totalFees := int64(0) + // // Query the version bits state to see if segwit has been activated, if so then + // // this means that we'll include any transactions with witness data in the + // // mempool, and also add the witness commitment as an OP_RETURN output in the + // // coinbase transaction. + // var segwitState blockchain.ThresholdState + // segwitState, e = g.Chain.ThresholdState(chaincfg.DeploymentSegwit) + // if e != nil { + // // return nil, e + // } + // segwitActive := segwitState == blockchain.ThresholdActive + // witnessIncluded := false + // Choose which transactions make it into the block. + for priorityQueue.Len() > 0 { + // Grab the highest priority (or highest fee per kilobyte depending on the txsort + // order) transaction. + prioItem := heap.Pop(priorityQueue).(*txPrioItem) + tx := prioItem.tx + // switch { + // // If segregated witness has not been activated yet, then we shouldn't include + // // any witness transactions in the block. + // case !segwitActive && tx.HasWitness(): + // continue + // // Otherwise, Keep track of if we've included a transaction with witness data or not. If so, then we'll need + // // to include the witness commitment as the last output in the coinbase transaction. + // case segwitActive && !witnessIncluded && tx.HasWitness(): + // // If we're about to include a transaction bearing witness data, then we'll also + // // need to include a witness commitment in the coinbase transaction. Therefore, + // // we account for the additional weight within the block with a model coinbase + // // tx with a witness commitment. + // coinbaseCopy := util.NewTx(coinbaseTx.MsgTx().Copy()) + // coinbaseCopy.MsgTx().TxIn[0].Witness = [][]byte{ + // bytes.Repeat( + // []byte("a"), + // blockchain.CoinbaseWitnessDataLen, + // ), + // } + // coinbaseCopy.MsgTx().AddTxOut( + // &wire.TxOut{ + // PkScript: bytes.Repeat( + // []byte("a"), + // blockchain.CoinbaseWitnessPkScriptLength, + // ), + // }, + // ) + // // In order to accurately account for the weight addition due to this coinbase + // // transaction, we'll add the difference of the transaction before and after the + // // addition of the commitment to the block weight. + // weightDiff := blockchain.GetTransactionWeight(coinbaseCopy) - + // blockchain.GetTransactionWeight(coinbaseTx) + // blockWeight += uint32(weightDiff) + // witnessIncluded = true + // } + // Grab any transactions which depend on this one. + deps := dependers[*tx.Hash()] + // Enforce maximum block size. Also check for overflow. + txWeight := uint32(blockchain.GetTransactionWeight(tx)) + blockPlusTxWeight := blockWeight + txWeight + if blockPlusTxWeight < blockWeight || + blockPlusTxWeight >= g.Policy.BlockMaxWeight { + T.F("skipping tx %s because it would exceed the max block weight", tx.Hash()) + logSkippedDeps(tx, deps) + continue + } + // Enforce maximum signature operation cost per block. Also check for overflow. + var sigOpCost int + sigOpCost, e = blockchain.GetSigOpCost(tx, false, blockUtxos, true) + if e != nil { + T.C( + func() string { + return "skipping tx " + tx.Hash().String() + + "due to error in GetSigOpCost: " + e.Error() + }, + ) + logSkippedDeps(tx, deps) + continue + } + if blockSigOpCost+int64(sigOpCost) < blockSigOpCost || + blockSigOpCost+int64(sigOpCost) > blockchain.MaxBlockSigOpsCost { + T.C( + func() string { + return "skipping tx " + tx.Hash().String() + + " because it would exceed the maximum sigops per block" + }, + ) + logSkippedDeps(tx, deps) + continue + } + // Skip free transactions once the block is larger than the minimum block size. + if sortedByFee && + prioItem.feePerKB < int64(g.Policy.TxMinFreeFee) && + blockPlusTxWeight >= g.Policy.BlockMinWeight { + T.C( + func() string { + return fmt.Sprintf( + "skipping tx %v with feePerKB %v < TxMinFreeFee %v and block weight %v >= minBlockWeight %v", + tx.Hash(), + prioItem.feePerKB, + g.Policy.TxMinFreeFee, + blockPlusTxWeight, + g.Policy.BlockMinWeight, + ) + }, + ) + logSkippedDeps(tx, deps) + continue + } + // Prioritize by fee per kilobyte once the block is larger than the priority + // size or there are no more high-priority transactions. + if !sortedByFee && (blockPlusTxWeight >= g.Policy.BlockPrioritySize || + prioItem.priority <= MinHighPriority.ToDUO()) { + T.F( + "switching to txsort by fees per kilobyte blockSize %d"+ + " >= BlockPrioritySize %d || priority %.2f <= minHighPriority %.2f", + blockPlusTxWeight, + g.Policy.BlockPrioritySize, + prioItem.priority, + MinHighPriority, + ) + sortedByFee = true + priorityQueue.SetLessFunc(txPQByFee) + } + // Put the transaction back into the priority queue and skip it so it is + // re-prioritized by fees if it won't fit into the high-priority section or the + // priority is too low. Otherwise this transaction will be the final one in the + // high-priority section, so just fall though to the code below so it is added + // now. + if blockPlusTxWeight > g.Policy.BlockPrioritySize || + prioItem.priority < MinHighPriority.ToDUO() { + heap.Push(priorityQueue, prioItem) + continue + } + + // Ensure the transaction inputs pass all of the necessary preconditions before + // allowing it to be added to the block. + _, e = blockchain.CheckTransactionInputs( + tx, nextBlockHeight, + blockUtxos, g.ChainParams, + ) + if e != nil { + T.F( + "skipping tx %s due to error in CheckTransactionInputs: %v", + tx.Hash(), e, + ) + logSkippedDeps(tx, deps) + continue + } + if e = blockchain.ValidateTransactionScripts( + g.Chain, tx, blockUtxos, + txscript.StandardVerifyFlags, g.SigCache, + g.HashCache, + ); E.Chk(e) { + T.F( + "skipping tx %s due to error in ValidateTransactionScripts: %v", + tx.Hash(), e, + ) + logSkippedDeps(tx, deps) + continue + } + + // Spend the transaction inputs in the block utxo view and add an entry for it + // to ensure any transactions which reference this one have it available as an + // input and can ensure they aren't double spending. + if e = spendTransaction(blockUtxos, tx, nextBlockHeight); E.Chk(e) { + } + // Add the transaction to the block, increment counters, and save the fees and + // signature operation counts to the block template. + blockTxns = append(blockTxns, tx) + blockWeight += txWeight + blockSigOpCost += int64(sigOpCost) + totalFees += prioItem.fee + txFees = append(txFees, prioItem.fee) + txSigOpCosts = append(txSigOpCosts, int64(sigOpCost)) + T.F( + "adding tx %s (priority %.2f, feePerKB %.2f)", + prioItem.tx.Hash(), + prioItem.priority, + prioItem.feePerKB, + ) + // Add transactions which depend on this one (and also do not have any other + // unsatisfied dependencies) to the priority queue. + for _, item := range deps { + // Add the transaction to the priority queue if there are no more dependencies + // after this one. + delete(item.dependsOn, *tx.Hash()) + if len(item.dependsOn) == 0 { + heap.Push(priorityQueue, item) + } + } + } + // Now that the actual transactions have been selected, update the block weight + // for the real transaction count and coinbase value with the total fees + // accordingly. + blockWeight -= wire.MaxVarIntPayload - + (uint32(wire.VarIntSerializeSize(uint64(len(blockTxns))))) + coinbaseTx.MsgTx().TxOut[0].Value += totalFees + txFees[0] = -totalFees + // If segwit is active and we included transactions with witness data, then + // // we'll need to include a commitment to the witness data in an OP_RETURN output + // // within the coinbase transaction. + // var witnessCommitment []byte + // if witnessIncluded { + // // The witness of the coinbase transaction MUST be exactly 32-bytes of all + // // zeroes. + // var witnessNonce [blockchain.CoinbaseWitnessDataLen]byte + // coinbaseTx.MsgTx().TxIn[0].Witness = wire.TxWitness{witnessNonce[:]} + // // Next, obtain the merkle root of a tree which consists of the wtxid of all + // // transactions in the block. The coinbase transaction will have a special wtxid + // // of all zeroes. + // witnessMerkleTree := blockchain.BuildMerkleTreeStore( + // blockTxns, + // true, + // ) + // witnessMerkleRoot := witnessMerkleTree.GetRoot() + // // The preimage to the witness commitment is: witnessRoot || coinbaseWitness + // var witnessPreimage [64]byte + // copy(witnessPreimage[:32], witnessMerkleRoot[:]) + // copy(witnessPreimage[32:], witnessNonce[:]) + // // The witness commitment itself is the double-sha256 of the witness preimage + // // generated above. With the commitment generated, the witness script for the + // // output is: OP_RETURN OP_DATA_36 {0xaa21a9ed || witnessCommitment}. The + // // leading prefix is referred to as the "witness magic bytes". + // witnessCommitment = chainhash.DoubleHashB(witnessPreimage[:]) + // witnessScript := append(blockchain.WitnessMagicBytes, witnessCommitment...) + // // Finally, create the OP_RETURN carrying witness commitment output as an + // // additional output within the coinbase. + // commitmentOutput := &wire.TxOut{ + // Value: 0, + // PkScript: witnessScript, + // } + // coinbaseTx.MsgTx().TxOut = append( + // coinbaseTx.MsgTx().TxOut, + // commitmentOutput, + // ) + // } + // Calculate the required difficulty for the block. The timestamp is potentially + // adjusted to ensure it comes after the median time of the last several blocks + // per the chain consensus rules. + ts := medianAdjustedTime(best, g.TimeSource) + T.Ln("legacy ts", ts) + if fork.GetCurrent(nextBlockHeight) > 0 { + ots := g.Chain.BestChain.NodeByHeight(best.Height).Header().Timestamp.Truncate(time.Second).Add(time.Second) + D.Ln("prev timestamp+1", ots) + tn := time.Now().Truncate(time.Second) + if tn.After(ots) { + ts = tn + } else { + ts = ots + } + T.Ln("plan9 ts", ts) + } + // T.Ln("algo ", ts, " ", algo) + var reqDifficulty uint32 + if reqDifficulty, e = g.Chain.CalcNextRequiredDifficulty(algo); E.Chk(e) { + return nil, e + } + T.F("reqDifficulty %d %08x %064x", vers, reqDifficulty, bits.CompactToBig(reqDifficulty)) + // Create a new block ready to be solved. + merkles := blockchain.BuildMerkleTreeStore(blockTxns, false) + var msgBlock wire.Block + msgBlock.Header = wire.BlockHeader{ + Version: vers, + PrevBlock: best.Hash, + MerkleRoot: *merkles.GetRoot(), + Timestamp: ts, + Bits: reqDifficulty, + } + for _, tx := range blockTxns { + if e = msgBlock.AddTransaction(tx.MsgTx()); E.Chk(e) { + return nil, e + } + } + // Finally, perform a full check on the created block against the chain + // consensus rules to ensure it properly connects to the current best chain with + // no issues. + block := block2.NewBlock(&msgBlock) + block.SetHeight(nextBlockHeight) + e = g.Chain.CheckConnectBlockTemplate(block) + if e != nil { + D.Ln("checkconnectblocktemplate err:", e) + return nil, e + } + bh := msgBlock.Header.BlockHash() + D.C( + func() string { + return fmt.Sprintf( + "created new block template (height %d algo %s, %d transactions, "+ + "%d in fees, %d signature operations cost, %d weight, "+ + "target difficulty %064x prevblockhash %064x %064x subsidy %d)", + nextBlockHeight, + algo, + len(msgBlock.Transactions), + totalFees, + blockSigOpCost, + blockWeight, + bits.CompactToBig(msgBlock.Header.Bits), + msgBlock.Header.PrevBlock.CloneBytes(), + bh.CloneBytes(), + msgBlock.Transactions[0].TxOut[0].Value, + ) + }, + ) + // D.S(msgBlock.Header) + // Tracec(func() string { return spew.Sdump(msgBlock) }) + return &BlockTemplate{ + Block: &msgBlock, + Fees: txFees, + SigOpCosts: txSigOpCosts, + Height: nextBlockHeight, + ValidPayAddress: payToAddress != nil, + }, nil +} + +// UpdateBlockTime updates the timestamp in the header of the passed block to +// the current time while taking into account the median time of the last +// several blocks to ensure the new time is after that time per the chain +// consensus rules. +// +// Finally, it will update the target difficulty if needed based on the new time +// for the test networks since their target difficulty can change based upon +// time. +func (g *BlkTmplGenerator) UpdateBlockTime( + workerNumber uint32, msgBlock *wire. +Block, +) (e error) { + // The new timestamp is potentially adjusted to ensure it comes after the median + // time of the last several blocks per the chain consensus rules. + newTime := medianAdjustedTime(g.Chain.BestSnapshot(), g.TimeSource) + msgBlock.Header.Timestamp = newTime + // Recalculate the difficulty if running on a network that requires it. + if g.ChainParams.ReduceMinDifficulty { + var difficulty uint32 + var e error + if difficulty, e = g.Chain.CalcNextRequiredDifficulty( + fork.GetAlgoName( + msgBlock.Header.Version, + g.BestSnapshot().Height, + ), + ); E.Chk(e) { + return e + } + msgBlock.Header.Bits = difficulty + } + return nil +} + +// UpdateExtraNonce updates the extra nonce in the coinbase script of the passed +// block by regenerating the coinbase script with the passed value and block +// height. +// +// It also recalculates and updates the new merkle root that results from +// changing the coinbase script. +func (g *BlkTmplGenerator) UpdateExtraNonce( + msgBlock *wire.Block, + blockHeight int32, extraNonce uint64, +) (e error) { + var coinbaseScript []byte + if coinbaseScript, e = standardCoinbaseScript(blockHeight, extraNonce); E.Chk(e) { + return e + } + if len(coinbaseScript) > blockchain.MaxCoinbaseScriptLen { + return fmt.Errorf( + "coinbase transaction script length of %d is out of range (min: %d, max: %d)", + len(coinbaseScript), blockchain.MinCoinbaseScriptLen, + blockchain.MaxCoinbaseScriptLen, + ) + } + msgBlock.Transactions[0].TxIn[0].SignatureScript = coinbaseScript + // TODO(davec): A util.Block should use saved in the state to avoid + // recalculating all of the other transaction hashes. + // block.Transactions[0].InvalidateCache() + // Recalculate the merkle root with the updated extra nonce. + block := block2.NewBlock(msgBlock) + merkles := blockchain.BuildMerkleTreeStore(block.Transactions(), false) + msgBlock.Header.MerkleRoot = *merkles.GetRoot() + return nil +} + +// BestSnapshot returns information about the current best chain block and +// related state as of the current point in time using the chain instance +// associated with the block template generator. The returned state must be +// treated as immutable since it is shared by all callers. This function is safe +// for concurrent access. +func (g *BlkTmplGenerator) BestSnapshot() *blockchain.BestState { + return g.Chain.BestSnapshot() +} + +// GetTxSource returns the associated transaction source. +// +// This function is safe for concurrent access. +func (g *BlkTmplGenerator) GetTxSource() TxSource { + return g.TxSource +} diff --git a/pkg/mining/policy.go b/pkg/mining/policy.go new file mode 100644 index 0000000..19ba767 --- /dev/null +++ b/pkg/mining/policy.go @@ -0,0 +1,115 @@ +package mining + +import ( + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // UnminedHeight is the height used for the "block" height field of the + // contextual transaction information provided in a transaction store when it + // has not yet been mined into a block. + UnminedHeight = 0x7fffffff +) + +// Policy houses the policy (configuration parameters) which is used to control +// the generation of block templates. See the documentation for NewBlockTemplate +// for more details on each of these parameters are used. +type Policy struct { + // BlockMinWeight is the minimum block weight to be used when generating a block + // template. + BlockMinWeight uint32 + // BlockMaxWeight is the maximum block weight to be used when generating a block + // template. + BlockMaxWeight uint32 + // BlockMinWeight is the minimum block size to be used when generating a block + // template. + BlockMinSize uint32 + // BlockMaxSize is the maximum block size to be used when generating a block + // template. + BlockMaxSize uint32 + // BlockPrioritySize is the size in bytes for high-priority / low-fee + // transactions to be used when generating a block template. + BlockPrioritySize uint32 + // TxMinFreeFee is the minimum fee in Satoshi/1000 bytes that is required for a + // transaction to be treated as free for mining purposes (block template + // generation). + TxMinFreeFee amt.Amount +} + +// minInt is a helper function to return the minimum of two ints. This avoids a +// math import and the need to cast to floats. +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// calcInputValueAge is a helper function used to calculate the input age of a +// transaction. The input age for a txin is the number of confirmations since +// the referenced txout multiplied by its output value. The total input age is +// the sum of this value for each txin. Any inputs to the transaction which are +// currently in the mempool and hence not mined into a block yet, contribute no +// additional input age to the transaction. +func calcInputValueAge(tx *wire.MsgTx, utxoView *blockchain.UtxoViewpoint, nextBlockHeight int32) float64 { + var totalInputAge float64 + for _, txIn := range tx.TxIn { + // Don't attempt to accumulate the total input age if the referenced transaction + // output doesn't exist. + entry := utxoView.LookupEntry(txIn.PreviousOutPoint) + if entry != nil && !entry.IsSpent() { + // Inputs with dependencies currently in the mempool have their block height set + // to a special constant. Their input age should computed as zero since their + // parent hasn't made it into a block yet. + var inputAge int32 + originHeight := entry.BlockHeight() + if originHeight == UnminedHeight { + inputAge = 0 + } else { + inputAge = nextBlockHeight - originHeight + } + // Sum the input value times age. + inputValue := entry.Amount() + totalInputAge += float64(inputValue * int64(inputAge)) + } + } + return totalInputAge +} + +// CalcPriority returns a transaction priority given a transaction and the sum +// of each of its input values multiplied by their age (# of confirmations). +// Thus, the final formula for the priority is: sum(inputValue * inputAge) / +// adjustedTxSize +func CalcPriority(tx *wire.MsgTx, utxoView *blockchain.UtxoViewpoint, nextBlockHeight int32) float64 { + // In order to encourage spending multiple old unspent transaction outputs + // thereby reducing the total set, don't count the constant overhead for each + // input as well as enough bytes of the signature script to cover a + // pay-to-script-hash redemption with a compressed pubkey. + // + // This makes additional inputs free by boosting the priority of the transaction + // accordingly. No more incentive is given to avoid encouraging gaming future + // transactions through the use of junk outputs. + // + // This is the same logic used in the reference implementation. + // + // The constant overhead for a txin is 41 bytes since the previous outpoint is + // 36 bytes + 4 bytes for the sequence + 1 byte the signature script length. A + // compressed pubkey pay-to-script-hash redemption with a maximum len signature + // is of the form: + // + // [OP_DATA_73 <73-byte sig> + OP_DATA_35 + {OP_DATA_33 <33 byte compresed pubkey> + OP_CHECKSIG}] Thus 1 + 73 + 1 + + // 1 + 33 + 1 = 110 + overhead := 0 + for _, txIn := range tx.TxIn { + // Max inputs + size can't possibly overflow here. + overhead += 41 + minInt(110, len(txIn.SignatureScript)) + } + serializedTxSize := tx.SerializeSize() + if overhead >= serializedTxSize { + return 0.0 + } + inputValueAge := calcInputValueAge(tx, utxoView, nextBlockHeight) + return inputValueAge / float64(serializedTxSize-overhead) +} diff --git a/pkg/mining/templates.go b/pkg/mining/templates.go new file mode 100644 index 0000000..89bc29e --- /dev/null +++ b/pkg/mining/templates.go @@ -0,0 +1,450 @@ +package mining + +// +// import ( +// "container/heap" +// "fmt" +// blockchain "github.com/p9c/p9/pkg/chain" +// "github.com/p9c/p9/pkg/chain/fork" +// chainhash "github.com/p9c/p9/pkg/chain/hash" +// txscript "github.com/p9c/p9/pkg/chain/tx/script" +// "github.com/p9c/p9/pkg/chain/wire" +// "github.com/p9c/p9/pkg/util" +// "math/rand" +// "time" +// ) +// +// // GenBlockHeader generate a block given a version number to use for mining +// // (nonce is empty, date can be updated, version changes merkle and target bits. +// // All the data required for this is in the exported fields that are serialized +// // for over the wire +// func (m *MsgBlockTemplate) GenBlockHeader(vers int32) *wire.BlockHeader { +// return &wire.BlockHeader{ +// Version: vers, +// PrevBlock: m.PrevBlock, +// MerkleRoot: m.Merkles[vers], +// Timestamp: m.Timestamp, +// Bits: m.Bits[vers], +// } +// } +// +// // Reconstruct takes a received block from the wire and reattaches the transactions +// func (m *MsgBlockTemplate) Reconstruct(hdr *wire.BlockHeader) *wire.WireBlock { +// msgBlock := &wire.WireBlock{Header: *hdr} +// // the coinbase is the last transaction +// txs := append(m.txs, m.coinbases[msgBlock.Header.Version]) +// for _, tx := range txs { +// if e := msgBlock.AddTransaction(tx.MsgTx()); E.Chk(e) { +// return nil +// } +// } +// return msgBlock +// } +// +// // NewBlockTemplates returns a data structure which has methods to construct +// // block version specific block headers and reconstruct their transactions +// func (g *BlkTmplGenerator) NewBlockTemplates( +// workerNumber uint32, +// payToAddress util.Address, +// ) (*MsgBlockTemplate, error) { +// mbt := &MsgBlockTemplate{Bits: make(map[int32]uint32), Merkles: make(map[int32]chainhash.Hash)} +// // Extend the most recently known best block. +// best := g.Chain.BestSnapshot() +// mbt.PrevBlock = best.Hash +// mbt.Timestamp = g.Chain.BestChain.Tip().Header().Timestamp.Add(time.Second) +// mbt.Height = best.Height + 1 +// // Create a standard coinbase transaction paying to the provided address. +// // +// // NOTE: The coinbase value will be updated to include the fees from the +// // selected transactions later after they have actually been selected. It is +// // created here to detect any errors early before potentially doing a lot of +// // work below. The extra nonce helps ensure the transaction is not a duplicate +// // transaction (paying the same value to the same public key address would +// // otherwise be an identical transaction for block version 1). +// rand.Seed(time.Now().UnixNano()) +// extraNonce := rand.Uint64() +// var e error +// // numAlgos := fork.GetNumAlgos(mbt.Height) +// // coinbaseScripts := make(map[int32][]byte, numAlgos) +// // coinbaseTxs := make(map[int32]*util.Tx, numAlgos) +// var coinbaseSigOpCost int64 +// // blockTemplates := make(map[int32]*BlockTemplate, numAlgos) +// var priorityQueues *txPriorityQueue +// // Get the current source transactions and create a priority queue to hold the +// // transactions which are ready for inclusion into a block along with some +// // priority related and fee metadata. Reserve the same number of items that are +// // available for the priority queue. Also, choose the initial txsort order for the +// // priority queue based on whether or not there is an area allocated for +// // high-priority transactions. +// sourceTxns := g.TxSource.MiningDescs() +// sortedByFee := g.Policy.BlockPrioritySize == 0 +// blockUtxos := blockchain.NewUtxoViewpoint() +// // dependers is used to track transactions which depend on another transaction +// // in the source pool. This, in conjunction with the dependsOn map kept with +// // each dependent transaction helps quickly determine which dependent +// // transactions are now eligible for inclusion in the block once each +// // transaction has been included. +// dependers := make(map[chainhash.Hash]map[chainhash.Hash]*txPrioItem) +// mempoolLoop: +// for _, txDesc := range sourceTxns { +// // A block can't have more than one coinbase or contain non-finalized +// // transactions. +// tx := txDesc.Tx +// if blockchain.IsCoinBase(tx) { +// Tracec( +// func() string { +// return fmt.Sprintf("skipping coinbase tx %s", tx.Hash()) +// }, +// ) +// continue +// } +// if !blockchain.IsFinalizedTransaction( +// tx, mbt.Height, +// g.TimeSource.AdjustedTime(), +// ) { +// Tracec( +// func() string { +// return "skipping non-finalized tx " + tx.Hash().String() +// }, +// ) +// continue +// } +// // Fetch all of the utxos referenced by the this transaction. +// // +// // NOTE: This intentionally does not fetch inputs from the mempool since a +// // transaction which depends on other transactions in the mempool must come +// // after those dependencies in the final generated block. +// utxos, e := g.Chain.FetchUtxoView(tx) +// if e != nil { +// Warnc( +// func() string { +// return "unable to fetch utxo view for tx " + tx.Hash().String() + ": " + err.Error() +// }, +// ) +// continue +// } +// // Setup dependencies for any transactions which reference other transactions in +// // the mempool so they can be properly ordered below. +// prioItem := &txPrioItem{tx: tx} +// for _, txIn := range tx.MsgTx().TxIn { +// originHash := &txIn.PreviousOutPoint.Hash +// entry := utxos.LookupEntry(txIn.PreviousOutPoint) +// if entry == nil || entry.IsSpent() { +// if !g.TxSource.HaveTransaction(originHash) { +// Tracec( +// func() string { +// return "skipping tx %s because it references unspent output %s which is not available" + +// tx.Hash().String() + +// txIn.PreviousOutPoint.String() +// }, +// ) +// continue mempoolLoop +// } +// // The transaction is referencing another transaction in the source pool, so +// // setup an ordering dependency. +// deps, exists := dependers[*originHash] +// if !exists { +// deps = make(map[chainhash.Hash]*txPrioItem) +// dependers[*originHash] = deps +// } +// deps[*prioItem.tx.Hash()] = prioItem +// if prioItem.dependsOn == nil { +// prioItem.dependsOn = make( +// map[chainhash.Hash]struct{}, +// ) +// } +// prioItem.dependsOn[*originHash] = struct{}{} +// // Skip the check below. We already know the referenced transaction is +// // available. +// continue +// } +// } +// // Calculate the final transaction priority using the input value age sum as +// // well as the adjusted transaction size. The formula is: sum (inputValue * +// // inputAge) / adjustedTxSize +// prioItem.priority = CalcPriority( +// tx.MsgTx(), utxos, +// mbt.Height, +// ) +// // Calculate the fee in Satoshi/kB. +// prioItem.feePerKB = txDesc.FeePerKB +// prioItem.fee = txDesc.Fee +// // Add the transaction to the priority queue to mark it ready for inclusion in +// // the block unless it has dependencies. +// if prioItem.dependsOn == nil { +// heap.Push(priorityQueues, prioItem) +// } +// // Merge the referenced outputs from the input transactions to this transaction +// // into the block utxo view. This allows the code below to avoid a second +// // lookup. +// mergeUtxoView(blockUtxos, utxos) +// } +// priorityQueues = newTxPriorityQueue(len(sourceTxns), sortedByFee) +// var coinbaseScript []byte +// if coinbaseScript, e = standardCoinbaseScript(mbt.Height, extraNonce); E.Chk(e) { +// return nil, e +// } +// algos := fork.GetAlgos(mbt.Height) +// var alg int32 +// mbt.coinbases = make(map[int32]*util.Tx) +// // Create a slice to hold the transactions to be included in the generated block +// var coinbaseTx *util.Tx +// for i := range algos { +// alg = algos[i].Version +// if coinbaseTx, e = createCoinbaseTx( +// g.ChainParams, coinbaseScript, mbt.Height, payToAddress, alg, +// ); E.Chk(e) { +// return nil, e +// } +// mbt.coinbases[alg] = coinbaseTx +// // this should be the same for all anyhow, as they are all the same format just +// // diff amounts (note: this might be wrawwnnggrrr) +// coinbaseSigOpCost = int64(blockchain.CountSigOps(mbt.coinbases[alg])) +// } +// // Create slices to hold the fees and number of signature operations for each of +// // the selected transactions and add an entry for the coinbase. This allows the +// // code below to simply append details about a transaction as it is selected for +// // inclusion in the final block. However, since the total fees aren't known yet, +// // use a dummy value for the coinbase fee which will be updated later. +// txFees := make([]int64, 0, len(sourceTxns)) +// txSigOpCosts := make([]int64, 0, len(sourceTxns)) +// txFees = append(txFees, -1) // Updated once known +// txSigOpCosts = append(txSigOpCosts, coinbaseSigOpCost) +// Tracef("considering %d transactions for inclusion to new block", len(sourceTxns)) +// // The starting block size is the size of the block header plus the max possible +// // transaction count size, plus the size of the coinbase transaction. +// // with reserved space. Also create a utxo view to house all of the input +// // transactions so multiple lookups can be avoided. +// blockWeight := uint32((blockHeaderOverhead) + blockchain.GetTransactionWeight(coinbaseTx)) +// blockSigOpCost := coinbaseSigOpCost +// totalFees := int64(0) +// mbt.txs = make([]*util.Tx, 0, len(sourceTxns)) +// // Choose which transactions make it into the block. +// for priorityQueues.Len() > 0 { +// // Grab the highest priority (or highest fee per kilobyte depending on the txsort +// // order) transaction. +// prioItem := heap.Pop(priorityQueues).(*txPrioItem) +// tx := prioItem.tx +// // Grab any transactions which depend on this one. +// deps := dependers[*tx.Hash()] +// // Enforce maximum block size. Also check for overflow. +// txWeight := uint32(blockchain.GetTransactionWeight(tx)) +// blockPlusTxWeight := blockWeight + txWeight +// if blockPlusTxWeight < blockWeight || +// blockPlusTxWeight >= g.Policy.BlockMaxWeight { +// Tracef("skipping tx %s because it would exceed the max block weight", tx.Hash()) +// logSkippedDeps(tx, deps) +// continue +// } +// // Enforce maximum signature operation cost per block. Also check for overflow. +// sigOpCost, e := blockchain.GetSigOpCost(tx, false, blockUtxos, true, false) +// if e != nil { +// Tracec( +// func() string { +// return "skipping tx " + tx.Hash().String() + +// "due to error in GetSigOpCost: " + err.Error() +// }, +// ) +// logSkippedDeps(tx, deps) +// continue +// } +// if blockSigOpCost+int64(sigOpCost) < blockSigOpCost || +// blockSigOpCost+int64(sigOpCost) > blockchain.MaxBlockSigOpsCost { +// Tracec( +// func() string { +// return "skipping tx " + tx.Hash().String() + +// " because it would exceed the maximum sigops per block" +// }, +// ) +// logSkippedDeps(tx, deps) +// continue +// } +// // Skip free transactions once the block is larger than the minimum block size. +// if sortedByFee && +// prioItem.feePerKB < int64(g.Policy.TxMinFreeFee) && +// blockPlusTxWeight >= g.Policy.BlockMinWeight { +// Tracec( +// func() string { +// return fmt.Sprintf( +// "skipping tx %v with feePerKB %v < TxMinFreeFee %v and block weight %v >= minBlockWeight %v", +// tx.Hash(), +// prioItem.feePerKB, +// g.Policy.TxMinFreeFee, +// blockPlusTxWeight, +// g.Policy.BlockMinWeight, +// ) +// }, +// ) +// logSkippedDeps(tx, deps) +// continue +// } +// // Prioritize by fee per kilobyte once the block is larger than the priority +// // size or there are no more high-priority transactions. +// if !sortedByFee && (blockPlusTxWeight >= g.Policy.BlockPrioritySize || +// prioItem.priority <= MinHighPriority.ToDUO()) { +// Tracef( +// "switching to txsort by fees per kilobyte blockSize %d"+ +// " >= BlockPrioritySize %d || priority %.2f <= minHighPriority %.2f", +// blockPlusTxWeight, +// g.Policy.BlockPrioritySize, +// prioItem.priority, +// MinHighPriority, +// ) +// sortedByFee = true +// priorityQueues.SetLessFunc(txPQByFee) +// } +// // Put the transaction back into the priority queue and skip it so it is +// // re-prioritized by fees if it won't fit into the high-priority section or the +// // priority is too low. Otherwise this transaction will be the final one in the +// // high-priority section, so just fall though to the code below so it is added +// // now. +// if blockPlusTxWeight > g.Policy.BlockPrioritySize || +// prioItem.priority < MinHighPriority.ToDUO() { +// heap.Push(priorityQueues, prioItem) +// continue +// } +// +// // Ensure the transaction inputs pass all of the necessary preconditions before +// // allowing it to be added to the block. +// _, e = blockchain.CheckTransactionInputs( +// tx, mbt.Height, +// blockUtxos, g.ChainParams, +// ) +// if e != nil { +// Tracef( +// "skipping tx %s due to error in CheckTransactionInputs: %v", +// tx.Hash(), e, +// ) +// logSkippedDeps(tx, deps) +// continue +// } +// if e = blockchain.ValidateTransactionScripts( +// g.Chain, tx, blockUtxos, +// txscript.StandardVerifyFlags, g.SigCache, +// g.HashCache, +// ); E.Chk(e) { +// Tracef( +// "skipping tx %s due to error in ValidateTransactionScripts: %v", +// tx.Hash(), e, +// ) +// logSkippedDeps(tx, deps) +// continue +// } +// // Spend the transaction inputs in the block utxo view and add an entry for it +// // to ensure any transactions which reference this one have it available as an +// // input and can ensure they aren't double spending. +// if e = spendTransaction(blockUtxos, tx, mbt.Height); E.Chk(e) { +// } +// // Add the transaction to the block, increment counters, and save the fees and +// // signature operation counts to the block template. +// mbt.txs = append(mbt.txs, tx) +// blockWeight += txWeight +// blockSigOpCost += int64(sigOpCost) +// totalFees += prioItem.fee +// txFees = append(txFees, prioItem.fee) +// txSigOpCosts = append(txSigOpCosts, int64(sigOpCost)) +// Tracef( +// "adding tx %s (priority %.2f, feePerKB %.2f)", +// prioItem.tx.Hash(), +// prioItem.priority, +// prioItem.feePerKB, +// ) +// // Add transactions which depend on this one (and also do not have any other +// // unsatisfied dependencies) to the priority queue. +// for _, item := range deps { +// // Add the transaction to the priority queue if there are no more dependencies +// // after this one. +// delete(item.dependsOn, *tx.Hash()) +// if len(item.dependsOn) == 0 { +// heap.Push(priorityQueues, item) +// } +// } +// } +// if fork.GetCurrent(mbt.Height) < 1 { +// // for legacy chain this is the consensus timestamp to use, post hard fork there +// // is no allowance for less than 1 second between block timestamps of +// // sequential, linked blocks, which was filled earlier by default +// mbt.Timestamp = medianAdjustedTime(best, g.TimeSource) +// } +// +// for next, curr, more := fork.AlgoVerIterator(mbt.Height); more(); next() { +// tX := append(mbt.txs, mbt.coinbases[curr()]) +// // Now that the actual transactions have been selected, update the block weight +// // for the real transaction count and coinbase value with the total fees +// // accordingly. +// blockWeight -= wire.MaxVarIntPayload - +// (uint32(wire.VarIntSerializeSize(uint64(len(mbt.txs))))) +// mbt.coinbases[curr()].MsgTx().TxOut[0].Value += totalFees +// txFees[0] = -totalFees +// // Calculate the required difficulty for the block. The timestamp is potentially +// // adjusted to ensure it comes after the median time of the last several blocks +// // per the chain consensus rules. +// algo := fork.GetAlgoName(mbt.Height, curr()) +// D.Ln("algo", algo) +// if mbt.Bits[curr()], e = g.Chain.CalcNextRequiredDifficulty(algo); E.Chk(e) { +// return nil, e +// } +// D.F( +// "%s %d reqDifficulty %08x %064x", algo, curr(), +// mbt.Bits[curr()], fork.CompactToBig(mbt.Bits[curr()]), +// ) +// // Create a new block ready to be solved. +// D.S(tX) +// +// merkles := blockchain.BuildMerkleTreeStore(tX, false) +// mbt.Merkles[curr()] = *merkles[len(merkles)-1] +// // TODO: can we do this once instead of 9 times? +// var msgBlock wire.WireBlock +// msgBlock.Header = wire.BlockHeader{ +// Version: curr(), +// PrevBlock: mbt.PrevBlock, +// MerkleRoot: mbt.Merkles[curr()], +// Timestamp: mbt.Timestamp, +// Bits: mbt.Bits[curr()], +// } +// for _, tx := range tX { +// if e := msgBlock.AddTransaction(tx.MsgTx()); E.Chk(e) { +// return nil, e +// } +// } +// // Finally, perform a full check on the created block against the chain +// // consensus rules to ensure it properly connects to the current best chain with +// // no issues. +// block := util.NewBlock(&msgBlock) +// block.SetHeight(mbt.Height) +// e = g.Chain.CheckConnectBlockTemplate(workerNumber, block) +// if e != nil { +// D.Ln("checkconnectblocktemplate err:", e) +// return nil, e +// } +// Tracec( +// func() string { +// bh := msgBlock.Header.BlockHash() +// return fmt.Sprintf( +// "created new block template (algo %s, %d transactions, "+ +// "%d in fees, %d signature operations cost, %d weight, "+ +// "target difficulty %064x prevblockhash %064x %064x subsidy %d)", +// algo, +// len(msgBlock.Transactions), +// totalFees, +// blockSigOpCost, +// blockWeight, +// fork.CompactToBig(msgBlock.Header.Bits), +// msgBlock.Header.PrevBlock.CloneBytes(), +// bh.CloneBytes(), +// msgBlock.Transactions[0].TxOut[0].Value, +// ) +// }, +// ) +// // // Tracec(func() string { return spew.Sdump(msgBlock) }) +// // blockTemplate := &BlockTemplate{ +// // Block: &msgBlock, +// // Fees: txFees, +// // SigOpCosts: txSigOpCosts, +// // Height: mbt.Height, +// // ValidPayAddress: payToAddress != nil, +// // } +// // blockTemplates[curr()] = blockTemplate +// } +// return mbt, nil +// } diff --git a/pkg/multicast/channel.go b/pkg/multicast/channel.go new file mode 100644 index 0000000..0258a48 --- /dev/null +++ b/pkg/multicast/channel.go @@ -0,0 +1,63 @@ +// Package multicast provides a UDP multicast connection with an in-process multicast interface for sending and +// receiving. +// +// In order to allow processes on the same machine (windows) to receive the messages this code enables multicast +// loopback. It is up to the consuming library to discard messages it sends. This is only necessary because the net +// standard library disables loopback by default though on windows this takes effect whereas on unix platforms it does +// not. +// +// This code was derived from the information found here: +// https://stackoverflow.com/questions/43109552/how-to-set-ip-multicast-loop-on-multicast-udpconn-in-golang + +package multicast + +import ( + "net" + + "golang.org/x/net/ipv4" + + "github.com/p9c/p9/pkg/util/routeable" +) + +func Conn(port int) (conn *net.UDPConn, e error) { + var ipv4Addr = &net.UDPAddr{IP: net.IPv4(224, 0, 0, 1), Port: port} + if conn, e = net.ListenUDP("udp4", ipv4Addr); E.Chk(e) { + return + } + D.Ln("listening on", conn.LocalAddr(), "to", conn.RemoteAddr()) + pc := ipv4.NewPacketConn(conn) + // var ifaces []net.Interface + var iface *net.Interface + // if ifaces, e = net.Interfaces(); E.Chk(e) { + // } + // // This grabs the first physical interface with multicast that is up. Note that this should filter out + // // VPN connections which would normally be selected first but don't actually have a multicast connection + // // to the local area network. + // for i := range ifaces { + // if ifaces[i].Flags&net.FlagMulticast != 0 && + // ifaces[i].Flags&net.FlagUp != 0 && + // ifaces[i].HardwareAddr != nil { + // iface = ifaces[i] + // break + // } + // } + ifcs, _ := routeable.GetAllInterfacesAndAddresses() + for _, ifc := range ifcs { + iface = ifc + if e = pc.JoinGroup(iface, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 1)}); E.Chk(e) { + return + } + // test + var loop bool + if loop, e = pc.MulticastLoopback(); e == nil { + D.F("MulticastLoopback status:%v\n", loop) + if !loop { + if e = pc.SetMulticastLoopback(true); e != nil { + E.F("SetMulticastLoopback error:%v\n", e) + } + } + D.Ln("Multicast Loopback enabled on", ifc.Name) + } + } + return +} diff --git a/pkg/multicast/log.go b/pkg/multicast/log.go new file mode 100644 index 0000000..e642d0b --- /dev/null +++ b/pkg/multicast/log.go @@ -0,0 +1,43 @@ +package multicast + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/netsync/README.md b/pkg/netsync/README.md new file mode 100755 index 0000000..03fa44b --- /dev/null +++ b/pkg/netsync/README.md @@ -0,0 +1,24 @@ +netsync +======= +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/netsync) + +## Overview + +This package implements a concurrency safe block syncing protocol. The +SyncManager communicates with connected peers to perform an initial block +download, keep the chain and unconfirmed transaction pool in sync, and announce +new blocks connected to the chain. Currently the sync manager selects a single +sync peer that it downloads all blocks from until it is up to date with the +longest chain the sync peer is aware of. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/netsync +``` + +## License + +Package netsync is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/netsync/blocklogger.go b/pkg/netsync/blocklogger.go new file mode 100644 index 0000000..b05ab72 --- /dev/null +++ b/pkg/netsync/blocklogger.go @@ -0,0 +1,75 @@ +package netsync + +import ( + "fmt" + "github.com/p9c/p9/pkg/block" + "sync" + "time" +) + +// blockProgressLogger provides periodic logging for other services in order to show users progress of certain "actions" +// involving some or all current blocks. Ex: syncing to best chain, indexing all blocks, etc. +type blockProgressLogger struct { + receivedLogBlocks int64 + receivedLogTx int64 + lastBlockLogTime time.Time + // subsystemLogger *log.Logger + progressAction string + sync.Mutex +} + +// newBlockProgressLogger returns a new block progress logger. The progress message is templated as follows: +// {progressAction } {numProcessed} {blocks|block} in the last {timePeriod} ({numTxs}, height {lastBlockHeight}, +// {lastBlockTimeStamp}) +func newBlockProgressLogger(progressMessage string) *blockProgressLogger { + return &blockProgressLogger{ + lastBlockLogTime: time.Now(), + progressAction: progressMessage, + } +} + +// LogBlockHeight logs a new block height as an information message to show progress to the user. +// +// In order to prevent spam, it limits logging to one message every 10 seconds with duration and totals included. +func (b *blockProgressLogger) LogBlockHeight(block *block.Block) { + b.Lock() + defer b.Unlock() + b.receivedLogBlocks++ + b.receivedLogTx += int64(len(block.WireBlock().Transactions)) + now := time.Now() + duration := now.Sub(b.lastBlockLogTime) + if duration < time.Second*2 { + return + } + // Truncate the duration to 10s of milliseconds. + durationMillis := int64(duration / time.Millisecond) + tDuration := 10 * time.Millisecond * time.Duration(durationMillis/10) + // Log information about new block height. + blockStr := "blocks" + if b.receivedLogBlocks == 1 { + blockStr = "block " + } + txStr := "transactions" + if b.receivedLogTx == 1 { + txStr = "transaction " + } + tD := tDuration.Seconds() + I.F( + "%s %6d %s in the last %s (%6d %s, height %8d, %s) %0.2f tx/s", + b.progressAction, + b.receivedLogBlocks, + blockStr, + fmt.Sprintf("%0.1fs", tD), + b.receivedLogTx, + txStr, block.Height(), + block.WireBlock().Header.Timestamp, + float64(b.receivedLogTx)/tD, + ) + b.receivedLogBlocks = 0 + b.receivedLogTx = 0 + b.lastBlockLogTime = now +} + +func (b *blockProgressLogger) SetLastLogTime(time time.Time) { + b.lastBlockLogTime = time +} diff --git a/pkg/netsync/doc.go b/pkg/netsync/doc.go new file mode 100644 index 0000000..21aa75d --- /dev/null +++ b/pkg/netsync/doc.go @@ -0,0 +1,7 @@ +/*Package netsync implements a concurrency safe block syncing protocol. + +The SyncManager communicates with connected peers to perform an initial block download, keep the chain and unconfirmed +transaction pool in sync, and announce new blocks connected to the chain. Currently the sync manager selects a single +sync peer that it downloads all blocks from until it is up to date with the longest chain the sync peer is aware of. +*/ +package netsync diff --git a/pkg/netsync/interface.go b/pkg/netsync/interface.go new file mode 100644 index 0000000..4b45cae --- /dev/null +++ b/pkg/netsync/interface.go @@ -0,0 +1,31 @@ +package netsync + +import ( + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/mempool" + "github.com/p9c/p9/pkg/peer" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// PeerNotifier exposes methods to notify peers of status changes to transactions, blocks, etc. Currently server (in the +// main package) implements this interface. +type PeerNotifier interface { + AnnounceNewTransactions(newTxs []*mempool.TxDesc) + UpdatePeerHeights(latestBlkHash *chainhash.Hash, latestHeight int32, updateSource *peer.Peer) + RelayInventory(invVect *wire.InvVect, data interface{}) + TransactionConfirmed(tx *util.Tx) +} + +// Config is a configuration struct used to initialize a new SyncManager. +type Config struct { + PeerNotifier PeerNotifier + Chain *blockchain.BlockChain + TxMemPool *mempool.TxPool + ChainParams *chaincfg.Params + DisableCheckpoints bool + MaxPeers int + FeeEstimator *mempool.FeeEstimator +} diff --git a/pkg/netsync/log.go b/pkg/netsync/log.go new file mode 100644 index 0000000..48c9d78 --- /dev/null +++ b/pkg/netsync/log.go @@ -0,0 +1,43 @@ +package netsync + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/netsync/manager.go b/pkg/netsync/manager.go new file mode 100644 index 0000000..cc734c4 --- /dev/null +++ b/pkg/netsync/manager.go @@ -0,0 +1,1428 @@ +package netsync + +import ( + "container/list" + "fmt" + "net" + "sync" + "sync/atomic" + "time" + + block2 "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/database" + "github.com/p9c/p9/pkg/mempool" + peerpkg "github.com/p9c/p9/pkg/peer" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +type ( + // SyncManager is used to communicate block related messages with peers. The + // SyncManager is started as by executing Start() in a goroutine. Once started, + // it selects peers to sync from and starts the initial block download. Once the + // chain is in sync, the SyncManager handles incoming block and header + // notifications and relays announcements of new blocks to peers. + SyncManager struct { + peerNotifier PeerNotifier + started int32 + shutdown int32 + chain *blockchain.BlockChain + txMemPool *mempool.TxPool + chainParams *chaincfg.Params + progressLogger *blockProgressLogger + msgChan chan interface{} + wg sync.WaitGroup + quit qu.C + // These fields should only be accessed from the blockHandler thread + rejectedTxns map[chainhash.Hash]struct{} + requestedTxns map[chainhash.Hash]struct{} + requestedBlocks map[chainhash.Hash]struct{} + syncPeer *peerpkg.Peer + peerStates map[*peerpkg.Peer]*peerSyncState + // The following fields are used for headers-first mode. + headersFirstMode bool + headerList *list.List + startHeader *list.Element + nextCheckpoint *chaincfg.Checkpoint + // An optional fee estimator. + feeEstimator *mempool.FeeEstimator + } + // blockMsg packages a bitcoin block message and the peer it came from together + // so the block handler has access to that information. + blockMsg struct { + block *block2.Block + peer *peerpkg.Peer + reply qu.C + } + // donePeerMsg signifies a newly disconnected peer to the block handler. + donePeerMsg struct { + peer *peerpkg.Peer + } + // getSyncPeerMsg is a message type to be sent across the message channel for + // retrieving the current sync peer. + getSyncPeerMsg struct { + reply chan int32 + } + // headerNode is used as a node in a list of headers that are linked together + // between checkpoints. + headerNode struct { + height int32 + hash *chainhash.Hash + } + // headersMsg packages a bitcoin headers message and the peer it came from + // together so the block handler has access to that information. + headersMsg struct { + headers *wire.MsgHeaders + peer *peerpkg.Peer + } + // invMsg packages a bitcoin inv message and the peer it came from together so + // the block handler has access to that information. + invMsg struct { + inv *wire.MsgInv + peer *peerpkg.Peer + } + // isCurrentMsg is a message type to be sent across the message channel for + // requesting whether or not the sync manager believes it is synced with the + // currently connected peers. + isCurrentMsg struct { + reply chan bool + } + // newPeerMsg signifies a newly connected peer to the block handler. + newPeerMsg struct { + peer *peerpkg.Peer + } + // pauseMsg is a message type to be sent across the message channel for pausing + // the sync manager. This effectively provides the caller with exclusive access + // over the manager until a receive is performed on the unpause channel. + pauseMsg struct { + unpause qu.C + } + // peerSyncState stores additional information that the SyncManager tracks about + // a peer. + peerSyncState struct { + syncCandidate bool + requestQueue []*wire.InvVect + requestedTxns map[chainhash.Hash]struct{} + requestedBlocks map[chainhash.Hash]struct{} + } + // processBlockMsg is a message type to be sent across the message channel for + // requested a block is processed. Note this call differs from blockMsg above in + // that blockMsg is intended for blocks that came from peers and have extra + // handling whereas this message essentially is just a concurrent safe way to + // call ProcessBlock on the internal block chain instance. + processBlockMsg struct { + block *block2.Block + flags blockchain.BehaviorFlags + reply chan processBlockResponse + } + // processBlockResponse is a response sent to the reply channel of a processBlockMsg. + processBlockResponse struct { + isOrphan bool + err error + } + // txMsg packages a bitcoin tx message and the peer it came from together so the + // block handler has access to that information. + txMsg struct { + tx *util.Tx + peer *peerpkg.Peer + reply qu.C + } +) + +const ( + // minInFlightBlocks is the minimum number of blocks that should be in the + // request queue for headers-first mode before requesting more. + minInFlightBlocks = 10 + // maxRejectedTxns is the maximum number of rejected transactions hashes to + // store in memory. + maxRejectedTxns = 1000 + // maxRequestedBlocks is the maximum number of requested block hashes to store + // in memory. + maxRequestedBlocks = wire.MaxInvPerMsg + // maxRequestedTxns is the maximum number of requested transactions hashes to + // store in memory. + maxRequestedTxns = wire.MaxInvPerMsg +) + +// zeroHash is the zero value hash (all zeros) +var zeroHash chainhash.Hash + +// DonePeer informs the blockmanager that a peer has disconnected. +func (sm *SyncManager) DonePeer(peer *peerpkg.Peer) { + // Ignore if we are shutting down. + if atomic.LoadInt32(&sm.shutdown) != 0 { + return + } + sm.msgChan <- &donePeerMsg{peer: peer} +} + +// IsCurrent returns whether or not the sync manager believes it is synced with +// the connected peers. +func (sm *SyncManager) IsCurrent() bool { + reply := make(chan bool) + sm.msgChan <- isCurrentMsg{reply: reply} + return <-reply +} + +// NewPeer informs the sync manager of a newly active peer. +func (sm *SyncManager) NewPeer(peer *peerpkg.Peer) { + // Ignore if we are shutting down. + if atomic.LoadInt32(&sm.shutdown) != 0 { + return + } + sm.msgChan <- &newPeerMsg{peer: peer} +} + +// Pause pauses the sync manager until the returned channel is closed. +// +// Note that while paused, all peer and block processing is halted. The message +// sender should avoid pausing the sync manager for long durations. +func (sm *SyncManager) Pause() chan<- struct{} { + c := qu.T() + sm.msgChan <- pauseMsg{c} + return c +} + +// ProcessBlock makes use of ProcessBlock on an internal instance of a block +// chain. +func (sm *SyncManager) ProcessBlock(block *block2.Block, flags blockchain.BehaviorFlags) (bool, error) { + T.Ln("processing block") + // Traces(block) + reply := make(chan processBlockResponse, 1) + T.Ln("sending to msgChan") + sm.msgChan <- processBlockMsg{block: block, flags: flags, reply: reply} + T.Ln("waiting on reply") + response := <-reply + return response.isOrphan, response.err +} + +// QueueBlock adds the passed block message and peer to the block handling +// queue. Responds to the done channel argument after the block message is +// processed. +func (sm *SyncManager) QueueBlock(block *block2.Block, peer *peerpkg.Peer, done qu.C) { + // Don't accept more blocks if we're shutting down. + if atomic.LoadInt32(&sm.shutdown) != 0 { + done <- struct{}{} + return + } + sm.msgChan <- &blockMsg{block: block, peer: peer, reply: done} +} + +// QueueHeaders adds the passed headers message and peer to the block handling +// queue. +func (sm *SyncManager) QueueHeaders(headers *wire.MsgHeaders, peer *peerpkg.Peer) { + // No channel handling here because peers do not need to block on headers + // messages. + if atomic.LoadInt32(&sm.shutdown) != 0 { + return + } + sm.msgChan <- &headersMsg{headers: headers, peer: peer} +} + +// QueueInv adds the passed inv message and peer to the block handling queue. +func (sm *SyncManager) QueueInv(inv *wire.MsgInv, peer *peerpkg.Peer) { + // No channel handling here because peers do not need to block on inv messages. + if atomic.LoadInt32(&sm.shutdown) != 0 { + return + } + sm.msgChan <- &invMsg{inv: inv, peer: peer} +} + +// QueueTx adds the passed transaction message and peer to the block handling +// queue. Responds to the done channel argument after the tx message is +// processed. +func (sm *SyncManager) QueueTx(tx *util.Tx, peer *peerpkg.Peer, done qu.C) { + // Don't accept more transactions if we're shutting down. + if atomic.LoadInt32(&sm.shutdown) != 0 { + done <- struct{}{} + return + } + sm.msgChan <- &txMsg{tx: tx, peer: peer, reply: done} +} + +// Start begins the core block handler which processes block and inv messages. +func (sm *SyncManager) Start() { + // Already started? + if atomic.AddInt32(&sm.started, 1) != 1 { + return + } + T.Ln("starting sync manager") + sm.wg.Add(1) + go sm.blockHandler(0) +} + +// Stop gracefully shuts down the sync manager by stopping all asynchronous +// handlers and waiting for them to finish. +func (sm *SyncManager) Stop() (e error) { + if atomic.AddInt32(&sm.shutdown, 1) != 1 { + D.Ln("sync manager is already in the process of shutting down") + return nil + } + // DEBUG{"sync manager shutting down"} + sm.quit.Q() + sm.wg.Wait() + return nil +} + +// SyncPeerID returns the ID of the current sync peer, or 0 if there is none. +func (sm *SyncManager) SyncPeerID() int32 { + reply := make(chan int32) + sm.msgChan <- getSyncPeerMsg{reply: reply} + return <-reply +} + +// blockHandler is the main handler for the sync manager. It must be run as a +// goroutine. It processes block and inv messages in a separate goroutine from +// the peer handlers so the block (Block) messages are handled by a single +// thread without needing to lock memory data structures. This is important +// because the sync manager controls which blocks are needed and how the +// fetching should proceed. +func (sm *SyncManager) blockHandler(workerNumber uint32) { +out: + for { + select { + case m := <-sm.msgChan: + switch msg := m.(type) { + case *newPeerMsg: + sm.handleNewPeerMsg(msg.peer) + case *txMsg: + sm.handleTxMsg(msg) + msg.reply <- struct{}{} + case *blockMsg: + sm.handleBlockMsg(0, msg) + msg.reply <- struct{}{} + case *invMsg: + sm.handleInvMsg(msg) + case *headersMsg: + sm.handleHeadersMsg(msg) + case *donePeerMsg: + sm.handleDonePeerMsg(msg.peer) + case getSyncPeerMsg: + var peerID int32 + if sm.syncPeer != nil { + peerID = sm.syncPeer.ID() + } + msg.reply <- peerID + case processBlockMsg: + T.Ln("received processBlockMsg") + var heightUpdate int32 + header := &msg.block.WireBlock().Header + T.Ln("checking if have should have serialized block height") + if blockchain.ShouldHaveSerializedBlockHeight(header) { + T.Ln("reading coinbase transaction") + mbt := msg.block.Transactions() + if len(mbt) > 0 { + coinbaseTx := mbt[len(mbt)-1] + T.Ln("extracting coinbase height") + var e error + var cbHeight int32 + if cbHeight, e = blockchain.ExtractCoinbaseHeight(coinbaseTx); E.Chk(e) { + W.Ln("unable to extract height from coinbase tx:", e) + } else { + heightUpdate = cbHeight + } + } else { + D.Ln("no transactions in block??") + } + } + T.Ln("passing to chain.ProcessBlock") + var isOrphan bool + var e error + if _, isOrphan, e = sm.chain.ProcessBlock( + workerNumber, + msg.block, + msg.flags, + heightUpdate, + ); D.Chk(e) { + D.Ln("error processing new block ", e) + msg.reply <- processBlockResponse{ + isOrphan: false, + err: e, + } + } + T.Ln("sending back message on reply channel") + msg.reply <- processBlockResponse{ + isOrphan: isOrphan, + err: nil, + } + T.Ln("sent reply") + case isCurrentMsg: + msg.reply <- sm.current() + case pauseMsg: + // Wait until the sender unpauses the manager. + <-msg.unpause + default: + T.F("invalid message type in block handler: %Ter", msg) + } + case <-sm.quit.Wait(): + break out + } + } + sm.wg.Done() +} + +// current returns true if we believe we are synced with our peers, false if we +// still have blocks to check +func (sm *SyncManager) current() bool { + if !sm.chain.IsCurrent() { + return false + } + // if blockChain thinks we are current and we have no syncPeer it is probably + // right. + if sm.syncPeer == nil { + return true + } + // No matter what chain thinks, if we are below the block we are syncing to we + // are not current. + if sm.chain.BestSnapshot().Height < sm.syncPeer.LastBlock() { + return false + } + return true +} + +// fetchHeaderBlocks creates and sends a request to the syncPeer for the next +// list of blocks to be downloaded based on the current list of headers. +func (sm *SyncManager) fetchHeaderBlocks() { + // Nothing to do if there is no start header. + if sm.startHeader == nil { + D.Ln("fetchHeaderBlocks called with no start header") + return + } + // Build up a getdata request for the list of blocks the headers describe. The + // size hint will be limited to wire.MaxInvPerMsg by the function, so no need to + // double check it here. + gdmsg := wire.NewMsgGetDataSizeHint(uint(sm.headerList.Len())) + numRequested := 0 + var sh *list.Element + for sh = sm.startHeader; sh != nil; sh = sh.Next() { + node, ok := sh.Value.(*headerNode) + if !ok { + D.Ln("header list node type is not a headerNode") + continue + } + iv := wire.NewInvVect(wire.InvTypeBlock, node.hash) + haveInv, e := sm.haveInventory(iv) + if e != nil { + T.Ln( + "unexpected failure when checking for existing inventory during header block fetch:", + e, + ) + } + var ee error + if !haveInv { + syncPeerState := sm.peerStates[sm.syncPeer] + sm.requestedBlocks[*node.hash] = struct{}{} + syncPeerState.requestedBlocks[*node.hash] = struct{}{} + // If we're fetching from a witness enabled peer post-fork, then ensure that we + // receive all the witness data in the blocks. + // if sm.syncPeer.IsWitnessEnabled() { + // iv.Type = wire.InvTypeWitnessBlock + // } + ee = gdmsg.AddInvVect(iv) + if ee != nil { + D.Ln(ee) + } + numRequested++ + } + sm.startHeader = sh.Next() + if numRequested >= wire.MaxInvPerMsg { + break + } + } + if len(gdmsg.InvList) > 0 { + sm.syncPeer.QueueMessage(gdmsg, nil) + } +} + +// findNextHeaderCheckpoint returns the next checkpoint after the passed height. +// It returns nil when there is not one either because the height is already +// later than the final checkpoint or some other reason such as disabled +// checkpoints. +func (sm *SyncManager) findNextHeaderCheckpoint(height int32) *chaincfg.Checkpoint { + checkpoints := sm.chain.Checkpoints() + if len(checkpoints) == 0 { + return nil + } + // There is no next checkpoint if the height is already after the final checkpoint. + finalCheckpoint := &checkpoints[len(checkpoints)-1] + if height >= finalCheckpoint.Height { + return nil + } + // Find the next checkpoint. + nextCheckpoint := finalCheckpoint + for i := len(checkpoints) - 2; i >= 0; i-- { + if height >= checkpoints[i].Height { + break + } + nextCheckpoint = &checkpoints[i] + } + return nextCheckpoint +} + +// handleBlockMsg handles block messages from all peers. +func (sm *SyncManager) handleBlockMsg(workerNumber uint32, bmsg *blockMsg) { + pp := bmsg.peer + state, exists := sm.peerStates[pp] + if !exists { + T.Ln( + "received block message from unknown peer", pp, + ) + return + } + // If we didn't ask for this block then the peer is misbehaving. + blockHash := bmsg.block.Hash() + if _, exists = state.requestedBlocks[*blockHash]; !exists { + // The regression test intentionally sends some blocks twice to test duplicate + // block insertion fails. Don't disconnect the peer or ignore the block when + // we're in regression test mode in this case so the chain code is actually fed + // the duplicate blocks. + if sm.chainParams != &chaincfg.RegressionTestParams { + W.C( + func() string { + return fmt.Sprintf( + "got unrequested block %v from %s -- disconnecting", + blockHash, + pp.Addr(), + ) + }, + ) + pp.Disconnect() + return + } + } + // When in headers-first mode, if the block matches the hash of the first header + // in the list of headers that are being fetched, it's eligible for less + // validation since the headers have already been verified to link together and + // are valid up to the next checkpoint. Also, remove the list entry for all + // blocks except the checkpoint since it is needed to verify the next round of + // headers links properly. + isCheckpointBlock := false + behaviorFlags := blockchain.BFNone + if sm.headersFirstMode { + firstNodeEl := sm.headerList.Front() + if firstNodeEl != nil { + firstNode := firstNodeEl.Value.(*headerNode) + if blockHash.IsEqual(firstNode.hash) { + behaviorFlags |= blockchain.BFFastAdd + if firstNode.hash.IsEqual(sm.nextCheckpoint.Hash) { + isCheckpointBlock = true + } else { + sm.headerList.Remove(firstNodeEl) + } + } + } + } + // Remove block from request maps. Either chain will know about it and so we + // shouldn't have any more instances of trying to fetch it, or we will fail the + // insert and thus we'll retry next time we get an inv. + delete(state.requestedBlocks, *blockHash) + delete(sm.requestedBlocks, *blockHash) + var heightUpdate int32 + var blkHashUpdate *chainhash.Hash + header := &bmsg.block.WireBlock().Header + if blockchain.ShouldHaveSerializedBlockHeight(header) { + coinbaseTx := bmsg.block.Transactions()[0] + cbHeight, e := blockchain.ExtractCoinbaseHeight(coinbaseTx) + if e != nil { + T.F( + "unable to extract height from coinbase tx: %v", + e, + ) + } else { + heightUpdate = cbHeight + blkHashUpdate = blockHash + } + } + D.Ln("current best height", sm.chain.BestChain.Height()) + _, isOrphan, e := sm.chain.ProcessBlock( + workerNumber, bmsg.block, + behaviorFlags, heightUpdate, + ) + if e != nil { + if heightUpdate+1 <= sm.chain.BestChain.Height() { + // Process the block to include validation, best chain selection, orphan handling, etc. + // When the error is a rule error, it means the block was simply rejected as + // opposed to something actually going wrong, so log it as such. Otherwise, + // something really did go wrong, so log it as an actual error. + // Convert the error into an appropriate reject message and send it. + if _, ok := e.(blockchain.RuleError); ok { + E.F( + "rejected block %v from %s: %v", + blockHash, pp, e, + ) + } else { + E.F("failed to process block %v: %v", blockHash, e) + } + if dbErr, ok := e.(database.DBError); ok && dbErr.ErrorCode == + database.ErrCorruption { + panic(dbErr) + } + code, reason := mempool.ErrToRejectErr(e) + pp.PushRejectMsg(wire.CmdBlock, code, reason, blockHash, false) + return + } else { + isOrphan=true + } + } + // Meta-data about the new block this peer is reporting. We use this below to + // update this peer's lastest block height and the heights of other peers based + // on their last announced block hash. This allows us to dynamically update the + // block heights of peers, avoiding stale heights when looking for a new sync + // peer. Upon acceptance of a block or recognition of an orphan, we also use + // this information to update the block heights over other peers who's invs may + // have been ignored if we are actively syncing while the chain is not yet + // current or who may have lost the lock announcment race. Request the parents + // for the orphan block from the peer that sent it. + if isOrphan { + // We've just received an orphan block from a peer. In order to update the + // height of the peer, we try to extract the block height from the scriptSig of + // the coinbase transaction. Extraction is only attempted if the block's version + // is high enough (ver 2+). + header := &bmsg.block.WireBlock().Header + if blockchain.ShouldHaveSerializedBlockHeight(header) { + coinbaseTx := bmsg.block.Transactions()[0] + var cbHeight int32 + cbHeight, e = blockchain.ExtractCoinbaseHeight(coinbaseTx) + if e != nil { + E.F("unable to extract height from coinbase tx: %v", e) + } else { + D.F( + "extracted height of %v from orphan block", + cbHeight, + ) + heightUpdate = cbHeight + blkHashUpdate = blockHash + } + } + orphanRoot := sm.chain.GetOrphanRoot(blockHash) + var locator blockchain.BlockLocator + locator, e = sm.chain.LatestBlockLocator() + if e != nil { + E.F( + "failed to get block locator for the latest block: %v", + e, + ) + } else { + e = pp.PushGetBlocksMsg(locator, orphanRoot) + if e != nil { + } + } + } else { + // When the block is not an orphan, log information about it and update the + // chain state. + sm.progressLogger.LogBlockHeight(bmsg.block) + // Update this peer's latest block height, for future potential sync node + // candidacy. + best := sm.chain.BestSnapshot() + heightUpdate = best.Height + blkHashUpdate = &best.Hash + // Clear the rejected transactions. + sm.rejectedTxns = make(map[chainhash.Hash]struct{}) + } + // Update the block height for this peer. But only send a message to the server + // for updating peer heights if this is an orphan or our chain is "current". + // This avoids sending a spammy amount of messages if we're syncing the chain + // from scratch. + if blkHashUpdate != nil && heightUpdate != 0 { + pp.UpdateLastBlockHeight(heightUpdate) + if isOrphan || sm.current() { + go sm.peerNotifier.UpdatePeerHeights( + blkHashUpdate, heightUpdate, + pp, + ) + } + } + // Nothing more to do if we aren't in headers-first mode. + if !sm.headersFirstMode { + return + } + // This is headers-first mode, so if the block is not a checkpoint request more + // blocks using the header list when the request queue is getting short. + if !isCheckpointBlock { + if sm.startHeader != nil && + len(state.requestedBlocks) < minInFlightBlocks { + sm.fetchHeaderBlocks() + } + return + } + // This is headers-first mode and the block is a checkpoint. When there is a + // next checkpoint, get the next round of headers by asking for headers starting + // from the block after this one up to the next checkpoint. + prevHeight := sm.nextCheckpoint.Height + prevHash := sm.nextCheckpoint.Hash + sm.nextCheckpoint = sm.findNextHeaderCheckpoint(prevHeight) + if sm.nextCheckpoint != nil { + locator := blockchain.BlockLocator([]*chainhash.Hash{prevHash}) + e = pp.PushGetHeadersMsg(locator, sm.nextCheckpoint.Hash) + if e != nil { + E.F( + "failed to send getheaders message to peer %s: %v", + pp.Addr(), e, + ) + return + } + I.F( + "downloading headers for blocks %d to %d from peer %s", + prevHeight+1, sm.nextCheckpoint.Height, sm.syncPeer.Addr(), + ) + return + } + // This is headers-first mode, the block is a checkpoint, and there are no more + // checkpoints, so switch to normal mode by requesting blocks from the block + // after this one up to the end of the chain (zero hash). + sm.headersFirstMode = false + sm.headerList.Init() + I.Ln( + "reached the final checkpoint -- switching to normal mode", + ) + locator := blockchain.BlockLocator([]*chainhash.Hash{blockHash}) + e = pp.PushGetBlocksMsg(locator, &zeroHash) + if e != nil { + E.Ln( + "failed to send getblocks message to peer", pp, ":", e, + ) + return + } +} + +// handleBlockchainNotification handles notifications from blockchain. It does +// things such as request orphan block parents and relay accepted blocks to +// connected peers. +func (sm *SyncManager) handleBlockchainNotification(notification *blockchain.Notification) { + switch notification.Type { + // A block has been accepted into the block chain. Relay it to other peers. + case blockchain.NTBlockAccepted: + // Don't relay if we are not current. Other peers that are current should + // already know about it. + if !sm.current() { + return + } + block, ok := notification.Data.(*block2.Block) + if !ok { + D.Ln("chain accepted notification is not a block") + break + } + // Generate the inventory vector and relay it. + iv := wire.NewInvVect(wire.InvTypeBlock, block.Hash()) + sm.peerNotifier.RelayInventory(iv, block.WireBlock().Header) + // A block has been connected to the main block chain. + case blockchain.NTBlockConnected: + block, ok := notification.Data.(*block2.Block) + if !ok { + D.Ln("chain connected notification is not a block") + break + } + // Remove all of the transactions (except the coinbase) in the connected block + // from the transaction pool. Secondly, remove any transactions which are now + // double spends as a result of these new transactions. Finally, remove any + // transaction that is no longer an orphan. Transactions which depend on a + // confirmed transaction are NOT removed recursively because they are still + // valid. + for _, tx := range block.Transactions()[1:] { + sm.txMemPool.RemoveTransaction(tx, false) + sm.txMemPool.RemoveDoubleSpends(tx) + sm.txMemPool.RemoveOrphan(tx) + sm.peerNotifier.TransactionConfirmed(tx) + acceptedTxs := sm.txMemPool.ProcessOrphans(sm.chain, tx) + sm.peerNotifier.AnnounceNewTransactions(acceptedTxs) + } + // Register block with the fee estimator, if it exists. + if sm.feeEstimator != nil { + e := sm.feeEstimator.RegisterBlock(block) + // If an error is somehow generated then the fee estimator has entered an + // invalid state. Since it doesn't know how to recover, create a new one. + if e != nil { + sm.feeEstimator = mempool.NewFeeEstimator( + mempool.DefaultEstimateFeeMaxRollback, + mempool.DefaultEstimateFeeMinRegisteredBlocks, + ) + } + } + // A block has been disconnected from the main block chain. + case blockchain.NTBlockDisconnected: + block, ok := notification.Data.(*block2.Block) + if !ok { + D.Ln("chain disconnected notification is not a block.") + break + } + // Reinsert all of the transactions (except the coinbase) into the transaction pool. + for _, tx := range block.Transactions()[1:] { + var ee error + _, _, ee = sm.txMemPool.MaybeAcceptTransaction( + sm.chain, tx, + false, false, + ) + if ee != nil { + // Remove the transaction and all transactions that depend on it if it wasn't + // accepted into the transaction pool. + sm.txMemPool.RemoveTransaction(tx, true) + } + } + // Rollback previous block recorded by the fee estimator. + if sm.feeEstimator != nil { + e := sm.feeEstimator.Rollback(block.Hash()) + if e != nil { + } + } + } +} + +// handleDonePeerMsg deals with peers that have signalled they are done. It +// removes the peer as a candidate for syncing and in the case where it was the +// current sync peer, attempts to select a new best peer to sync from. It is +// invoked from the syncHandler goroutine. +func (sm *SyncManager) handleDonePeerMsg(peer *peerpkg.Peer) { + state, exists := sm.peerStates[peer] + if !exists { + T.Ln("received done peer message for unknown peer", peer) + return + } + // Remove the peer from the list of candidate peers. + delete(sm.peerStates, peer) + T.Ln("lost peer ", peer) + // Remove requested transactions from the global map so that they will be + // fetched from elsewhere next time we get an inv. + for txHash := range state.requestedTxns { + delete(sm.requestedTxns, txHash) + } + // Remove requested blocks from the global map so that they will be fetched from + // elsewhere next time we get an inv. + // + // TODO: we could possibly here check which peers have these blocks and request them now to speed things up a little. + for blockHash := range state.requestedBlocks { + delete(sm.requestedBlocks, blockHash) + } + // Attempt to find a new peer to sync from if the quitting peer is the sync + // peer. Also, reset the headers-first state if in headers-first mode so + if sm.syncPeer == peer { + sm.syncPeer = nil + if sm.headersFirstMode { + best := sm.chain.BestSnapshot() + sm.resetHeaderState(&best.Hash, best.Height) + } + sm.startSync() + } +} + +// handleHeadersMsg handles block header messages from all peers. Headers are +// requested when performing a headers-first sync. +func (sm *SyncManager) handleHeadersMsg(hmsg *headersMsg) { + peer := hmsg.peer + _, exists := sm.peerStates[peer] + if !exists { + T.Ln("received headers message from unknown peer", peer) + return + } + // The remote peer is misbehaving if we didn't request headers. + msg := hmsg.headers + numHeaders := len(msg.Headers) + if !sm.headersFirstMode { + T.F( + "got %d unrequested headers from %s -- disconnecting", + numHeaders, peer, + ) + peer.Disconnect() + return + } + // Nothing to do for an empty headers message. + if numHeaders == 0 { + return + } + // Process all of the received headers ensuring each one connects to the + // previous and that checkpoints match. + receivedCheckpoint := false + var finalHash *chainhash.Hash + for _, blockHeader := range msg.Headers { + blockHash := blockHeader.BlockHash() + finalHash = &blockHash + // Ensure there is a previous header to compare against. + prevNodeEl := sm.headerList.Back() + if prevNodeEl == nil { + W.Ln( + "header list does not contain a previous element as expected -- disconnecting peer", + ) + peer.Disconnect() + return + } + // Ensure the header properly connects to the previous one and add it to the + // list of headers. + node := headerNode{hash: &blockHash} + prevNode := prevNodeEl.Value.(*headerNode) + if prevNode.hash.IsEqual(&blockHeader.PrevBlock) { + node.height = prevNode.height + 1 + e := sm.headerList.PushBack(&node) + if sm.startHeader == nil { + sm.startHeader = e + } + } else { + T.Ln( + "received block header that does not properly connect to the chain from peer", + peer, + "-- disconnecting", + ) + peer.Disconnect() + return + } + // Verify the header at the next checkpoint height matches. + if node.height == sm.nextCheckpoint.Height { + if node.hash.IsEqual(sm.nextCheckpoint.Hash) { + receivedCheckpoint = true + I.F( + "verified downloaded block header against checkpoint at height %d/hash %s", + node.height, + node.hash, + ) + } else { + T.F( + "block header at height %d/hash %s from peer %s does NOT match expected checkpoint hash of"+ + " %s -- disconnecting", + node.height, + node.hash, + peer, + sm.nextCheckpoint.Hash, + ) + peer.Disconnect() + return + } + break + } + } + // When this header is a checkpoint, switch to fetching the blocks for all of + // the headers since the last checkpoint. + if receivedCheckpoint { + // Since the first entry of the list is always the final block that is already + // in the database and is only used to ensure the next header links properly, it + // must be removed before fetching the blocks. + sm.headerList.Remove(sm.headerList.Front()) + I.F( + "received %v block headers: Fetching blocks", + sm.headerList.Len(), + ) + sm.progressLogger.SetLastLogTime(time.Now()) + sm.fetchHeaderBlocks() + return + } + // This header is not a checkpoint, so request the next batch of headers + // starting from the latest known header and ending with the next checkpoint. + locator := blockchain.BlockLocator([]*chainhash.Hash{finalHash}) + e := peer.PushGetHeadersMsg(locator, sm.nextCheckpoint.Hash) + if e != nil { + E.F( + "failed to send getheaders message to peer %s: %v", peer, + e, + ) + return + } +} + +// handleInvMsg handles inv messages from all peers. We examine the inventory +// advertised by the remote peer and act accordingly. +func (sm *SyncManager) handleInvMsg(imsg *invMsg) { + peer := imsg.peer + state, exists := sm.peerStates[peer] + if !exists { + T.Ln("received inv message from unknown peer", peer) + return + } + // Attempt to find the final block in the inventory list. There may not be one. + lastBlock := -1 + invVects := imsg.inv.InvList + for i := len(invVects) - 1; i >= 0; i-- { + if invVects[i].Type == wire.InvTypeBlock { + lastBlock = i + break + } + } + // If this inv contains a block announcement, and this isn't coming from our + // current sync peer or we're current, then update the last announced block for + // this peer. We'll use this information later to update the heights of peers + // based on blocks we've accepted that they previously announced. + if lastBlock != -1 && (peer != sm.syncPeer || sm.current()) { + peer.UpdateLastAnnouncedBlock(&invVects[lastBlock].Hash) + } + // Ignore invs from peers that aren't the sync if we are not current. Helps prevent fetching a mass of orphans. + if peer != sm.syncPeer && !sm.current() { + return + } + // If our chain is current and a peer announces a block we already know of, then update their current block height. + if lastBlock != -1 && sm.current() { + blkHeight, e := sm.chain.BlockHeightByHash(&invVects[lastBlock].Hash) + if e == nil { + peer.UpdateLastBlockHeight(blkHeight) + } + } + // Request the advertised inventory if we don't already have it. Also, request + // parent blocks of orphans if we receive one we already have. Finally, attempt + // to detect potential stalls due to long side chains we already have and + // request more blocks to prevent them. + for i, iv := range invVects { + // Ignore unsupported inventory types. + switch iv.Type { + case wire.InvTypeBlock: + case wire.InvTypeTx: + // case wire.InvTypeWitnessBlock: + // case wire.InvTypeWitnessTx: + default: + continue + } + // Add the inventory to the cache of known inventory for the peer. + peer.AddKnownInventory(iv) + // Ignore inventory when we're in headers-first mode. + if sm.headersFirstMode { + continue + } + // Request the inventory if we don't already have it. + haveInv, e := sm.haveInventory(iv) + if e != nil { + E.Ln("unexpected failure when checking for existing inventory during inv message processing:", e) + continue + } + if !haveInv { + if iv.Type == wire.InvTypeTx { + // Skip the transaction if it has already been rejected. + if _, exists := sm.rejectedTxns[iv.Hash]; exists { + continue + } + } + // Ignore invs block invs from non-witness enabled peers, as after segwit + // activation we only want to download from peers that can provide us full + // witness data for blocks. PARALLELCOIN HAS NO WITNESS STUFF if + // !peer.IsWitnessEnabled() && iv.Type == wire.InvTypeBlock { + // continue + // } + // Add it to the request queue. + state.requestQueue = append(state.requestQueue, iv) + continue + } + if iv.Type == wire.InvTypeBlock { + // The block is an orphan block that we already have. When the existing orphan + // was processed, it requested the missing parent blocks. When this scenario + // happens, it means there were more blocks missing than are allowed into a + // single inventory message. As a result, once this peer requested the final + // advertised block, the remote peer noticed and is now resending the orphan + // block as an available block to signal there are more missing blocks that need + // to be requested. + if sm.chain.IsKnownOrphan(&iv.Hash) { + // Request blocks starting at the latest known up to the root of the orphan that just came in. + orphanRoot := sm.chain.GetOrphanRoot(&iv.Hash) + locator, e := sm.chain.LatestBlockLocator() + if e != nil { + E.Ln("failed to get block locator for the latest block:", e) + continue + } + e = peer.PushGetBlocksMsg(locator, orphanRoot) + if e != nil { + } + continue + } + // We already have the final block advertised by this inventory message, so + // force a request for more. This should only happen if we're on a really long + // side chain. + if i == lastBlock { + // Request blocks after this one up to the final one the remote peer knows about + // (zero stop hash). + locator := sm.chain.BlockLocatorFromHash(&iv.Hash) + e := peer.PushGetBlocksMsg(locator, &zeroHash) + if e != nil { + } + } + } + } + // Request as much as possible at once. Anything that won't fit into request + // will be requested on the next inv message. + numRequested := 0 + gdmsg := wire.NewMsgGetData() + requestQueue := state.requestQueue + for len(requestQueue) != 0 { + iv := requestQueue[0] + requestQueue[0] = nil + requestQueue = requestQueue[1:] + switch iv.Type { + // case wire.InvTypeWitnessBlock: + // fallthrough + case wire.InvTypeBlock: + // Request the block if there is not already a pending request. + if _, exists := sm.requestedBlocks[iv.Hash]; !exists { + sm.requestedBlocks[iv.Hash] = struct{}{} + sm.limitMap(sm.requestedBlocks, maxRequestedBlocks) + state.requestedBlocks[iv.Hash] = struct{}{} + // if peer.IsWitnessEnabled() { + // iv.Type = wire.InvTypeWitnessBlock + // } + e := gdmsg.AddInvVect(iv) + if e != nil { + } + numRequested++ + } + // case wire.InvTypeWitnessTx: + // fallthrough + case wire.InvTypeTx: + // Request the transaction if there is not already a pending request. + if _, exists := sm.requestedTxns[iv.Hash]; !exists { + sm.requestedTxns[iv.Hash] = struct{}{} + sm.limitMap(sm.requestedTxns, maxRequestedTxns) + state.requestedTxns[iv.Hash] = struct{}{} + // If the peer is capable, request the txn including all witness + // data. + // if peer.IsWitnessEnabled() { + // iv.Type = wire.InvTypeWitnessTx + // } + e := gdmsg.AddInvVect(iv) + if e != nil { + } + numRequested++ + } + } + if numRequested >= wire.MaxInvPerMsg { + break + } + } + state.requestQueue = requestQueue + if len(gdmsg.InvList) > 0 { + peer.QueueMessage(gdmsg, nil) + } +} + +// handleNewPeerMsg deals with new peers that have signalled they may be +// considered as a sync peer (they have already successfully negotiated). It +// also starts syncing if needed. It is invoked from the syncHandler goroutine. +func (sm *SyncManager) handleNewPeerMsg(peer *peerpkg.Peer) { + // Ignore if in the process of shutting down. + if atomic.LoadInt32(&sm.shutdown) != 0 { + return + } + T.F("new valid peer %s (%s)", peer, peer.UserAgent()) + // Initialize the peer state + isSyncCandidate := sm.isSyncCandidate(peer) + if isSyncCandidate { + I.Ln(peer, "is a sync candidate") + } + sm.peerStates[peer] = &peerSyncState{ + syncCandidate: isSyncCandidate, + requestedTxns: make(map[chainhash.Hash]struct{}), + requestedBlocks: make(map[chainhash.Hash]struct{}), + } + // Start syncing by choosing the best candidate if needed. + if isSyncCandidate && sm.syncPeer == nil { + sm.startSync() + } +} + +// handleTxMsg handles transaction messages from all peers. +func (sm *SyncManager) handleTxMsg(tmsg *txMsg) { + peer := tmsg.peer + state, exists := sm.peerStates[peer] + if !exists { + W.C( + func() string { + return "received tx message from unknown peer " + + peer.String() + }, + ) + return + } + // NOTE: BitcoinJ, and possibly other wallets, don't follow the spec of sending + // an inventory message and allowing the remote peer to decide whether or not + // they want to request the transaction via a getdata message. Unfortunately, + // the reference implementation permits unrequested data, so it has allowed + // wallets that don't follow the spec to proliferate. While this is not ideal, + // there is no check here to disconnect peers for sending unsolicited + // transactions to provide interoperability. + txHash := tmsg.tx.Hash() + // Ignore transactions that we have already rejected. Do not send a reject + // message here because if the transaction was already rejected, the transaction + // was unsolicited. + if _, exists = sm.rejectedTxns[*txHash]; exists { + D.C( + func() string { + return "ignoring unsolicited previously rejected transaction " + + txHash.String() + " from " + peer.String() + }, + ) + return + } + // Process the transaction to include validation, insertion in the memory pool, + // orphan handling, etc. + acceptedTxs, e := sm.txMemPool.ProcessTransaction( + sm.chain, tmsg.tx, + true, true, mempool.Tag(peer.ID()), + ) + // Remove transaction from request maps. Either the mempool/chain already knows + // about it and as such we shouldn't have any more instances of trying to fetch + // it, or we failed to insert and thus we'll retry next time we get an inv. + delete(state.requestedTxns, *txHash) + delete(sm.requestedTxns, *txHash) + if e != nil { + // Do not request this transaction again until a new block has been processed. + sm.rejectedTxns[*txHash] = struct{}{} + sm.limitMap(sm.rejectedTxns, maxRejectedTxns) + // When the error is a rule error, it means the transaction was simply rejected + // as opposed to something actually going wrong, so log it as such. Otherwise, + // something really did go wrong, so log it as an actual error. + if _, ok := e.(mempool.RuleError); ok { + D.F( + "rejected transaction %v from %s: %v", + txHash, + peer, + e, + ) + } else { + E.F( + "failed to process transaction %v: %v", + txHash, + e, + ) + } + // Convert the error into an appropriate reject message and send it. + code, reason := mempool.ErrToRejectErr(e) + peer.PushRejectMsg(wire.CmdTx, code, reason, txHash, false) + return + } + sm.peerNotifier.AnnounceNewTransactions(acceptedTxs) +} + +// haveInventory returns whether or not the inventory represented by the passed +// inventory vector is known. This includes checking all of the various places +// inventory can be when it is in different states such as blocks that are part +// of the main chain, on a side chain, in the orphan pool, and transactions that +// are in the memory pool (either the main pool or orphan pool). +func (sm *SyncManager) haveInventory(invVect *wire.InvVect) (bool, error) { + switch invVect.Type { + // case wire.InvTypeWitnessBlock: + // fallthrough + case wire.InvTypeBlock: + // Ask chain if the block is known to it in any form (main chain, side chain, or + // orphan). + return sm.chain.HaveBlock(&invVect.Hash) + // case wire.InvTypeWitnessTx: + // fallthrough + case wire.InvTypeTx: + // Ask the transaction memory pool if the transaction is known to it in any form + // (main pool or orphan). + if sm.txMemPool.HaveTransaction(&invVect.Hash) { + return true, nil + } + // Chk if the transaction exists from the point of view of the end of the main + // chain. Note that this is only a best effort since it is expensive to check + // existence of every output and the only purpose of this check is to avoid + // downloading already known transactions. Only the first two outputs are + // checked because the vast majority of transactions consist of two outputs + // where one is some form of "pay-to-somebody-else" and the other is a change + // output. + prevOut := wire.OutPoint{Hash: invVect.Hash} + for i := uint32(0); i < 2; i++ { + prevOut.Index = i + entry, e := sm.chain.FetchUtxoEntry(prevOut) + if e != nil { + return false, e + } + if entry != nil && !entry.IsSpent() { + return true, nil + } + } + return false, nil + } + // The requested inventory is is an unsupported type, so just claim it is known + // to avoid requesting it. + return true, nil +} + +// isSyncCandidate returns whether or not the peer is a candidate to consider +// syncing from. +func (sm *SyncManager) isSyncCandidate(peer *peerpkg.Peer) bool { + // Typically a peer is not a candidate for sync if it's not a full node, however + // regression test is special in that the regression tool is not a full node and + // still needs to be considered a sync candidate. + if sm.chainParams == &chaincfg.RegressionTestParams { + // The peer is not a candidate if it's not coming from localhost or the hostname + // can't be determined for some reason. + var host string + var e error + host, _, e = net.SplitHostPort(peer.Addr()) + if e != nil { + return false + } + if host != "127.0.0.1" && host != "localhost" { + return false + } + // } else { + // // The peer is not a candidate for sync if it's not a full node. Additionally, if the segwit soft-fork package + // // has activated, then the peer must also be upgraded. + // segwitActive, e := sm.chain.IsDeploymentActive(chaincfg.DeploymentSegwit) + // if e != nil { + // // Error("unable to query for segwit soft-fork state:", e) + // } + // nodeServices := peer.Services() + // if nodeServices&wire.SFNodeNetwork != wire.SFNodeNetwork || + // (segwitActive && !peer.IsWitnessEnabled()) { + // return false + // } + } + // Candidate if all checks passed. + return true +} + +// limitMap is a helper function for maps that require a maximum limit by +// evicting a random transaction if adding a new value would cause it to +// overflow the maximum allowed. +func (sm *SyncManager) limitMap(m map[chainhash.Hash]struct{}, limit int) { + if len(m)+1 > limit { + // Remove a random entry from the map. For most compilers, Go's range statement + // iterates starting at a random item although that is not 100% guaranteed by + // the spec. The iteration order is not important here because an adversary + // would have to be able to pull off preimage attacks on the hashing function in + // order to target eviction of specific entries anyways. + for txHash := range m { + delete(m, txHash) + return + } + } +} + +// resetHeaderState sets the headers-first mode state to values appropriate for +// syncing from a new peer. +func (sm *SyncManager) resetHeaderState(newestHash *chainhash.Hash, newestHeight int32) { + sm.headersFirstMode = false + sm.headerList.Init() + sm.startHeader = nil + // When there is a next checkpoint, add an entry for the latest known block into + // the header pool. This allows the next downloaded header to prove it links to + // the chain properly. + if sm.nextCheckpoint != nil { + node := headerNode{height: newestHeight, hash: newestHash} + sm.headerList.PushBack(&node) + } +} + +// startSync will choose the best peer among the available candidate peers to +// download/sync the blockchain from. When syncing is already running, it simply +// returns. It also examines the candidates for any which are no longer +// candidates and removes them as needed. +func (sm *SyncManager) startSync() { + // Return now if we're already syncing. + if sm.syncPeer != nil { + return + } + // Once the segwit soft-fork package has activated, we only want to sync from + // peers which are witness enabled to ensure that we fully validate all + // blockchain data. + // var e error + // var segwitActive bool + // segwitActive, e = sm.chain.IsDeploymentActive(chaincfg.DeploymentSegwit) + // if e != nil { + // Error("unable to query for segwit soft-fork state:", e) + // return + // } + best := sm.chain.BestSnapshot() + var bestPeer *peerpkg.Peer + for peer, state := range sm.peerStates { + if !state.syncCandidate { + continue + } + // if segwitActive && !peer.IsWitnessEnabled() { + // D.Ln("peer", peer, "not witness enabled, skipping") + // continue + // } Remove sync candidate peers that are no longer candidates due to passing + // their latest known block. + // + // NOTE: The < is intentional as opposed to <=. While technically the peer + // doesn't have a later block when it's equal, it will likely have one soon so + // it is a reasonable choice. It also allows the case where both are at 0 such + // as during regression test. + if peer.LastBlock() < best.Height { + // state.syncCandidate = false + continue + } + // TODO(davec): Use a better algorithm to choose the best peer. For now, just pick the first available candidate. + bestPeer = peer + } + // Start syncing from the best peer if one was selected. + if bestPeer != nil { + // Clear the requestedBlocks if the sync peer changes, otherwise we may ignore blocks we need that the last sync + // peer failed to send. + sm.requestedBlocks = make(map[chainhash.Hash]struct{}) + locator, e := sm.chain.LatestBlockLocator() + if e != nil { + E.Ln("failed to get block locator for the latest block:", e) + return + } + T.C( + func() string { + return fmt.Sprintf("syncing to block height %d from peer %v", bestPeer.LastBlock(), bestPeer.Addr()) + }, + ) + // When the current height is less than a known checkpoint we can use block + // headers to learn about which blocks comprise the chain up to the checkpoint + // and perform less validation for them. This is possible since each header + // contains the hash of the previous header and a merkle root. + // + // Therefore if we validate all of the received headers link together properly + // and the checkpoint hashes match, we can be sure the hashes for the blocks in + // between are accurate. Further, once the full blocks are downloaded, the + // merkle root is computed and compared against the value in the header which + // proves the full block hasn't been tampered with. Once we have passed the + // final checkpoint, or checkpoints are disabled, use standard inv messages + // learn about the blocks and fully validate them. Finally, regression test mode + // does not support the headers-first approach so do normal block downloads when + // in regression test mode. + if sm.nextCheckpoint != nil && + best.Height < sm.nextCheckpoint.Height && + sm.chainParams != &chaincfg.RegressionTestParams { + e := bestPeer.PushGetHeadersMsg(locator, sm.nextCheckpoint.Hash) + if e != nil { + } + sm.headersFirstMode = true + I.F( + "downloading headers for blocks %d to %d from peer %s", + best.Height+1, + sm.nextCheckpoint.Height, + bestPeer.Addr(), + ) + } else { + e := bestPeer.PushGetBlocksMsg(locator, &zeroHash) + if e != nil { + } + } + sm.syncPeer = bestPeer + } else { + T.Ln("no sync peer candidates available") + } +} + +// New constructs a new SyncManager. Use Start to begin processing asynchronous +// block, tx, and inv updates. +func New(config *Config) (*SyncManager, error) { + sm := SyncManager{ + peerNotifier: config.PeerNotifier, + chain: config.Chain, + txMemPool: config.TxMemPool, + chainParams: config.ChainParams, + rejectedTxns: make(map[chainhash.Hash]struct{}), + requestedTxns: make(map[chainhash.Hash]struct{}), + requestedBlocks: make(map[chainhash.Hash]struct{}), + peerStates: make(map[*peerpkg.Peer]*peerSyncState), + progressLogger: newBlockProgressLogger("processed"), + msgChan: make(chan interface{}, config.MaxPeers*3), + headerList: list.New(), + quit: qu.T(), + feeEstimator: config.FeeEstimator, + } + best := sm.chain.BestSnapshot() + if !config.DisableCheckpoints { + // Initialize the next checkpoint based on the current height. + sm.nextCheckpoint = sm.findNextHeaderCheckpoint(best.Height) + if sm.nextCheckpoint != nil { + sm.resetHeaderState(&best.Hash, best.Height) + } + } else { + I.Ln("checkpoints are disabled") + } + sm.chain.Subscribe(sm.handleBlockchainNotification) + return &sm, nil +} diff --git a/pkg/opts/LICENSE b/pkg/opts/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/opts/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/opts/README.md b/pkg/opts/README.md new file mode 100644 index 0000000..a657a56 --- /dev/null +++ b/pkg/opts/README.md @@ -0,0 +1,7 @@ +# opts +Types for use with a generator for easy to use CLI and environment variables +reading and concurrent safe configuration system for applications. + +With the generator and these types you can maintain *one* specification for +your application's configuration system and update with one click or go +generate command. diff --git a/pkg/opts/binary/binary.go b/pkg/opts/binary/binary.go new file mode 100644 index 0000000..20e2bcd --- /dev/null +++ b/pkg/opts/binary/binary.go @@ -0,0 +1,154 @@ +package binary + +import ( + "encoding/json" + "fmt" + "strings" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/opt" +) + +// Opt stores an boolean configuration value +type Opt struct { + meta.Data + hook []Hook + value *uberatomic.Bool + Def bool +} + +type Hook func(b bool) error + +// New creates a new Opt with default values set +func New(m meta.Data, def bool, hook ...Hook) *Opt { + return &Opt{value: uberatomic.NewBool(def), Data: m, Def: def, hook: hook} +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string. +// The value can be right up against the keyword or separated by a '='. +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + // if the input is empty, the user intends the opposite of the default + if input == "" { + x.value.Store(!x.Def) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing characters + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + input = strings.ToLower(input) + switch input { + case "t", "true", "+": + e = x.Set(true) + case "f", "false", "-": + e = x.Set(false) + default: + e = fmt.Errorf("input on opt %s: '%s' is not valid for a boolean flag", x.Name(), input) + } + return +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// True returns whether the value is set to true (it returns the value) +func (x *Opt) True() bool { + return x.value.Load() +} + +// False returns whether the value is false (it returns the inverse of the value) +func (x *Opt) False() bool { + return !x.value.Load() +} + +// Flip changes the value to its opposite +func (x *Opt) Flip() { + I.Ln("flipping", x.Name(), "to", !x.value.Load()) + x.value.Toggle() +} + +func (x *Opt) runHooks(b bool) (e error) { + for i := range x.hook { + if e = x.hook[i](b); E.Chk(e) { + break + } + } + return +} + +// Set changes the value currently stored +func (x *Opt) Set(b bool) (e error) { + if e = x.runHooks(b); E.Chk(e) { + I.Ln("setting", x.Name(), "to", b) + x.value.Store(b) + } + return +} + +// String returns a string form of the value +func (x *Opt) String() string { + return fmt.Sprint(x.Data.Option, ": ", x.True()) +} + +// T sets the value to true +func (x *Opt) T() *Opt { + x.value.Store(true) + return x +} + +// F sets the value to false +func (x *Opt) F() *Opt { + x.value.Store(false) + return x +} + +// MarshalJSON returns the json representation of a Opt +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation of a Opt +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.value.Load() + if e = json.Unmarshal(data, &v); E.Chk(e) { + return + } + e = x.Set(v) + return +} diff --git a/pkg/opts/binary/log.go b/pkg/opts/binary/log.go new file mode 100644 index 0000000..cf79721 --- /dev/null +++ b/pkg/opts/binary/log.go @@ -0,0 +1,44 @@ +package binary + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/cmds/commands.go b/pkg/opts/cmds/commands.go new file mode 100644 index 0000000..7ee6ed2 --- /dev/null +++ b/pkg/opts/cmds/commands.go @@ -0,0 +1,106 @@ +package cmds + +// Commands are a slice of Command entries +type Commands []Command + +// Command is a specification for a command and can include any number of subcommands +type Command struct { + Name string + Title string + Description string + Entrypoint func(c interface{}) error + Commands Commands + Colorizer func(a ...interface{}) string + AppText string + Parent *Command +} + +func (c Commands) PopulateParents(parent *Command) { + if parent != nil { + T.Ln("backlinking children of", parent.Name) + } + for i := range c { + c[i].Parent = parent + c[i].Commands.PopulateParents(&c[i]) + } +} + +// GetAllCommands returns all of the available command names +func (c Commands) GetAllCommands() (o []string) { + c.ForEach(func(cm Command) bool { + o = append(o, cm.Name) + o = append(o, cm.Commands.GetAllCommands()...) + return true + }, 0, 0, + ) + return +} + +var tabs = "\t\t\t\t\t" + +// Find the Command you are looking for. Note that the namespace is assumed to be flat, no duplicated names on different +// levels, as it returns on the first one it finds, which goes depth-first recursive +func (c Commands) Find( + name string, hereDepth, hereDist int, skipFirst bool, +) (found bool, depth, dist int, cm *Command, e error) { + if c == nil { + dist = hereDist + depth = hereDepth + return + } + if hereDist == 0 { + D.Ln("searching for command:", name) + } + depth = hereDepth + 1 + T.Ln(tabs[:depth]+"->", depth) + dist = hereDist + for i := range c { + T.Ln(tabs[:depth]+"walking", c[i].Name, depth, dist) + dist++ + if c[i].Name == name { + if skipFirst { + continue + } + dist-- + T.Ln(tabs[:depth]+"found", name, "at depth", depth, "distance", dist) + found = true + cm = &c[i] + e = nil + return + } + if found, depth, dist, cm, e = c[i].Commands.Find(name, depth, dist, false); E.Chk(e) { + T.Ln(tabs[:depth]+"error", c[i].Name) + return + } + if found { + return + } + } + T.Ln(tabs[:hereDepth]+"<-", hereDepth) + if hereDepth == 0 { + D.Ln("search text", name, "not found") + } + depth-- + return +} + +func (c Commands) ForEach(fn func(Command) bool, hereDepth, hereDist int) (ret bool, depth, dist int, e error) { + if c == nil { + dist = hereDist + depth = hereDepth + return + } + depth = hereDepth + 1 + T.Ln(tabs[:depth]+"->", depth) + dist = hereDist + for i := range c { + T.Ln(tabs[:depth]+"walking", c[i].Name, depth, dist) + if !fn(c[i]) { + // if the closure returns false break out of the loop + return + } + } + T.Ln(tabs[:hereDepth]+"<-", hereDepth) + depth-- + return +} diff --git a/pkg/opts/cmds/commands_test.go b/pkg/opts/cmds/commands_test.go new file mode 100644 index 0000000..f1a89aa --- /dev/null +++ b/pkg/opts/cmds/commands_test.go @@ -0,0 +1,75 @@ +package cmds + +import ( + "testing" +) + +func TestCommands_GetAllCommands(t *testing.T) { + cm := GetCommands() + I.S(cm.GetAllCommands()) +} + + +// GetCommands returns available subcommands in Parallelcoin Pod +func GetCommands() (c Commands) { + c = Commands{ + {Name: "gui", Title: + "ParallelCoin GUI Wallet/Miner/Explorer", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "version", Title: + "print version and exit", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "ctl", Title: + "command line wallet and chain RPC client", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "node", Title: + "ParallelCoin blockchain node", + Entrypoint: func(c interface{}) error { return nil }, + Commands: []Command{ + {Name: "dropaddrindex", Title: + "drop the address database index", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "droptxindex", Title: + "drop the transaction database index", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "dropcfindex", Title: + "drop the cfilter database index", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "dropindexes", Title: + "drop all of the indexes", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "resetchain", Title: + "deletes the current blockchain cache to force redownload", + Entrypoint: func(c interface{}) error { return nil }, + }, + }, + }, + {Name: "wallet", Title: + "run the wallet server (requires a chain node to function)", + Entrypoint: func(c interface{}) error { return nil }, + Commands: []Command{ + {Name: "drophistory", Title: + "reset the wallet transaction history", + Entrypoint: func(c interface{}) error { return nil }, + }, + }, + }, + {Name: "kopach", Title: + "standalone multicast miner for easy mining farm deployment", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "worker", Title: + "single thread worker process, normally started by kopach", + Entrypoint: func(c interface{}) error { return nil }, + }, + } + return +} + diff --git a/pkg/opts/cmds/log.go b/pkg/opts/cmds/log.go new file mode 100644 index 0000000..a75af0b --- /dev/null +++ b/pkg/opts/cmds/log.go @@ -0,0 +1,44 @@ +package cmds + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/duration/duration.go b/pkg/opts/duration/duration.go new file mode 100644 index 0000000..4a1f0d6 --- /dev/null +++ b/pkg/opts/duration/duration.go @@ -0,0 +1,136 @@ +package duration + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/opt" + "github.com/p9c/p9/pkg/opts/sanitizers" +) + +// Opt stores an time.Duration configuration value +type Opt struct { + meta.Data + hook []Hook + clamp func(input time.Duration) (result time.Duration) + Min, Max time.Duration + Value *uberatomic.Duration + Def time.Duration +} + +type Hook func(d time.Duration) error + +// New creates a new Opt with a given default value set +func New(m meta.Data, def time.Duration, min, max time.Duration, hook ...Hook) *Opt { + return &Opt{ + Value: uberatomic.NewDuration(def), + Data: m, + Def: def, + Min: min, + Max: max, + hook: hook, + clamp: sanitizers.ClampDuration(min, max), + } +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("duration opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing '=' + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + var v time.Duration + if v, e = time.ParseDuration(input); E.Chk(e) { + return + } + if e = x.Set(v); E.Chk(e) { + } + return +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the value stored +func (x *Opt) V() time.Duration { + return x.Value.Load() +} + +func (x *Opt) runHooks(d time.Duration) (e error) { + for i := range x.hook { + if e = x.hook[i](d); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(d time.Duration) (e error) { + d = x.clamp(d) + if e = x.runHooks(d); !E.Chk(e) { + x.Value.Store(d) + } + return +} + +// String returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprintf("%s: %v", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.Value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load() + e = json.Unmarshal(data, &v) + e = x.Set(v) + return +} diff --git a/pkg/opts/duration/log.go b/pkg/opts/duration/log.go new file mode 100644 index 0000000..984c82c --- /dev/null +++ b/pkg/opts/duration/log.go @@ -0,0 +1,44 @@ +package duration + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/float/float.go b/pkg/opts/float/float.go new file mode 100644 index 0000000..106db55 --- /dev/null +++ b/pkg/opts/float/float.go @@ -0,0 +1,136 @@ +package float + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/opt" + "github.com/p9c/p9/pkg/opts/sanitizers" + + uberatomic "go.uber.org/atomic" +) + +// Opt stores an float64 configuration value +type Opt struct { + meta.Data + hook []Hook + Min, Max float64 + clamp func(input float64) (result float64) + Value *uberatomic.Float64 + Def float64 +} + +type Hook func(f float64) error + +// New returns a new Opt value set to a default value +func New(m meta.Data, def float64, min, max float64, hook ...Hook) *Opt { + return &Opt{ + Value: uberatomic.NewFloat64(def), + Data: m, + Def: def, + Min: min, + Max: max, + hook: hook, + clamp: sanitizers.ClampFloat(min, max), + } +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("floating point number opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing '=' + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + var v float64 + if v, e = strconv.ParseFloat(input, 64); E.Chk(e) { + return + } + if e = x.Set(v); E.Chk(e) { + } + return x, e +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the value stored +func (x *Opt) V() float64 { + return x.Value.Load() +} + +func (x *Opt) runHooks(f float64) (e error) { + for i := range x.hook { + if e = x.hook[i](f); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(f float64) (e error) { + f = x.clamp(f) + if e = x.runHooks(f); !E.Chk(e) { + x.Value.Store(f) + } + return +} + +// String returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprintf("%s: %0.8f", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation of +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.Value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation of +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load() + e = json.Unmarshal(data, &v) + e = x.Set(v) + return +} diff --git a/pkg/opts/float/log.go b/pkg/opts/float/log.go new file mode 100644 index 0000000..99d69fc --- /dev/null +++ b/pkg/opts/float/log.go @@ -0,0 +1,44 @@ +package float + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/integer/int.go b/pkg/opts/integer/int.go new file mode 100644 index 0000000..7f11ece --- /dev/null +++ b/pkg/opts/integer/int.go @@ -0,0 +1,136 @@ +package integer + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + uberatomic "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/opt" + "github.com/p9c/p9/pkg/opts/sanitizers" +) + +// Opt stores an int configuration value +type Opt struct { + meta.Data + hook []Hook + Min, Max int + clamp func(input int) (result int) + Value *uberatomic.Int64 + Def int64 +} + +type Hook func(i int) error + +// New creates a new Opt with a given default value +func New(m meta.Data, def int64, min, max int, hook ...Hook) *Opt { + return &Opt{ + Value: uberatomic.NewInt64(def), + Data: m, + Def: def, + Min: min, + Max: max, + hook: hook, + clamp: sanitizers.ClampInt(min, max), + } +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("integer number opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing '=' + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + var v int64 + if v, e = strconv.ParseInt(input, 10, 64); E.Chk(e) { + return + } + if e = x.Set(int(v)); E.Chk(e) { + } + return x, e +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the stored int +func (x *Opt) V() int { + return int(x.Value.Load()) +} + +func (x *Opt) runHooks(ii int) (e error) { + for i := range x.hook { + if e = x.hook[i](ii); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(i int) (e error) { + i = x.clamp(i) + if e = x.runHooks(i); !E.Chk(e) { + x.Value.Store(int64(i)) + } + return +} + +// String returns the string stored +func (x *Opt) String() string { + return fmt.Sprintf("%s: %d", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation of +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.Value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation of +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load() + e = json.Unmarshal(data, &v) + e = x.Set(int(v)) + return +} diff --git a/pkg/opts/integer/log.go b/pkg/opts/integer/log.go new file mode 100644 index 0000000..1e8f042 --- /dev/null +++ b/pkg/opts/integer/log.go @@ -0,0 +1,44 @@ +package integer + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/list/log.go b/pkg/opts/list/log.go new file mode 100644 index 0000000..a1c87fc --- /dev/null +++ b/pkg/opts/list/log.go @@ -0,0 +1,44 @@ +package list + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/list/strings.go b/pkg/opts/list/strings.go new file mode 100644 index 0000000..4612f4d --- /dev/null +++ b/pkg/opts/list/strings.go @@ -0,0 +1,166 @@ +package list + +import ( + "encoding/json" + "fmt" + "strings" + "sync/atomic" + + "github.com/p9c/p9/pkg/opts/normalize" + + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/opt" + "github.com/p9c/p9/pkg/opts/sanitizers" +) + +// Opt stores a string slice configuration value +type Opt struct { + meta.Data + hook []Hook + Value *atomic.Value + Def []string +} + +type Hook func(s []string) error + +// New creates a new Opt with default values set +func New(m meta.Data, def []string, hook ...Hook) *Opt { + as := &atomic.Value{} + as.Store(def) + return &Opt{Value: as, Data: m, Def: def, hook: hook} +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput adds the value from a string. For this opt this means appending to the list +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("string opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + // if value has a comma in it, it's a list of items, so split them and append them + slice := x.S() + if strings.Contains(input, ",") { + split := strings.Split(input, ",") + for i := range split { + var cleaned string + if cleaned, e = sanitizers.StringType(x.Data.Type, split[i], x.Data.DefaultPort); E.Chk(e) { + return + } + if cleaned != "" { + I.Ln("setting value for", x.Data.Name, cleaned) + split[i] = cleaned + } + } + e = x.Set(append(slice, split...)) + } else { + var cleaned string + if cleaned, e = sanitizers.StringType(x.Data.Type, input, x.Data.DefaultPort); E.Chk(e) { + return + } + if cleaned != "" { + I.Ln("setting value for", x.Data.Name, cleaned) + input = cleaned + } + if e = x.Set(append(slice, input)); E.Chk(e) { + } + + } + // ensure there is no duplicates + e = x.Set(normalize.RemoveDuplicateAddresses(x.V())) + return x, e +} + +// LoadInput sets the value from a string. For this opt this replacing the list +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + old := x.V() + _ = x.Set([]string{}) + if o, e = x.ReadInput(input); E.Chk(e) { + // if input failed to parse, restore its prior state + _ = x.Set(old) + } + return +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the stored value +func (x *Opt) V() []string { + return x.Value.Load().([]string) +} + +// Len returns the length of the slice of strings +func (x *Opt) Len() int { + return len(x.S()) +} + +func (x *Opt) runHooks(s []string) (e error) { + for i := range x.hook { + if e = x.hook[i](s); E.Chk(e) { + break + } + } + return +} + +// Set the slice of strings stored +func (x *Opt) Set(ss []string) (e error) { + if e = x.runHooks(ss); !E.Chk(e) { + x.Value.Store(ss) + } + return +} + +// S returns the value as a slice of string +func (x *Opt) S() []string { + return x.Value.Load().([]string) +} + +// String returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprint(x.Data.Option, ": ", x.S()) +} + +// MarshalJSON returns the json representation of +func (x *Opt) MarshalJSON() (b []byte, e error) { + xs := x.Value.Load().([]string) + return json.Marshal(xs) +} + +// UnmarshalJSON decodes a JSON representation of +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + var v []string + e = json.Unmarshal(data, &v) + x.Value.Store(v) + return +} diff --git a/pkg/opts/meta/data.go b/pkg/opts/meta/data.go new file mode 100644 index 0000000..6ba45e1 --- /dev/null +++ b/pkg/opts/meta/data.go @@ -0,0 +1,24 @@ +package meta + +type ( + // Data is the information about the opt to be used by interface code and other presentations of the data + Data struct { + Option string + Aliases []string + Group string + Tags []string + Label string + Description string + Documentation string + Type string + Options []string + OmitEmpty bool + Name string + DefaultPort int + } +) + +func (m Data) GetAllOptionStrings() (opts []string) { + opts = append([]string{m.Option}, m.Aliases...) + return opts +} diff --git a/pkg/opts/meta/log.go b/pkg/opts/meta/log.go new file mode 100644 index 0000000..9e9f23d --- /dev/null +++ b/pkg/opts/meta/log.go @@ -0,0 +1,44 @@ +package meta + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/normalize/log.go b/pkg/opts/normalize/log.go new file mode 100644 index 0000000..ae33cd5 --- /dev/null +++ b/pkg/opts/normalize/log.go @@ -0,0 +1,44 @@ +package normalize + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/normalize/normalize.go b/pkg/opts/normalize/normalize.go new file mode 100644 index 0000000..3cf3954 --- /dev/null +++ b/pkg/opts/normalize/normalize.go @@ -0,0 +1,44 @@ +package normalize + +import ( + "net" +) + +// address returns addr with the passed default port appended if there is not +// already a port specified. +func address(addr, defaultPort string) string { + var e error + if _, _, e = net.SplitHostPort(addr); E.Chk(e) { + return net.JoinHostPort(addr, defaultPort) + } + return addr +} + +// Addresses returns a new slice with all the passed peer addresses normalized +// with the given default port, and all duplicates removed. +func Addresses(addrs []string, defaultPort string) []string { + for i := range addrs { + addrs[i] = address(addrs[i], defaultPort) + } + return RemoveDuplicateAddresses(addrs) +} + +// RemoveDuplicateAddresses returns a new slice with all duplicate entries in +// addrs removed. +func RemoveDuplicateAddresses(addrs []string) (result []string) { + result = make([]string, 0, len(addrs)) + seen := map[string]struct{}{} + for _, val := range addrs { + if _, ok := seen[val]; !ok { + result = append(result, val) + seen[val] = struct{}{} + } + } + return result +} + +// StringSliceAddresses normalizes a slice of addresses +func StringSliceAddresses(a []string, port string) { + variable := a + a = Addresses(variable, port) +} diff --git a/pkg/opts/opt/log.go b/pkg/opts/opt/log.go new file mode 100644 index 0000000..c098a14 --- /dev/null +++ b/pkg/opts/opt/log.go @@ -0,0 +1,44 @@ +package opt + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/opt/option.go b/pkg/opts/opt/option.go new file mode 100644 index 0000000..7966a88 --- /dev/null +++ b/pkg/opts/opt/option.go @@ -0,0 +1,21 @@ +package opt + +import ( + "github.com/p9c/p9/pkg/opts/meta" +) + +type ( + // Option is an interface to simplify concurrent-safe access to a variety of types of configuration item + Option interface { + LoadInput(input string) (o Option, e error) + ReadInput(input string) (o Option, e error) + GetMetadata() *meta.Data + Name() string + String() string + MarshalJSON() (b []byte, e error) + UnmarshalJSON(data []byte) (e error) + GetAllOptionStrings() []string + Type() interface{} + SetName(string) + } +) diff --git a/pkg/opts/sanitizers/log.go b/pkg/opts/sanitizers/log.go new file mode 100644 index 0000000..cdb88b9 --- /dev/null +++ b/pkg/opts/sanitizers/log.go @@ -0,0 +1,44 @@ +package sanitizers + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/sanitizers/numbers.go b/pkg/opts/sanitizers/numbers.go new file mode 100644 index 0000000..c65d411 --- /dev/null +++ b/pkg/opts/sanitizers/numbers.go @@ -0,0 +1,41 @@ +package sanitizers + +import ( + "time" +) + +func ClampInt(min, max int) func(input int) (result int) { + return func(input int) (result int) { + if input > max { + return max + } + if input < min { + return min + } + return input + } +} + +func ClampFloat(min, max float64) func(input float64) (result float64) { + return func(input float64) (result float64) { + if input > max { + return max + } + if input < min { + return min + } + return input + } +} + +func ClampDuration(min, max time.Duration) func(input time.Duration) (result time.Duration) { + return func(input time.Duration) (result time.Duration) { + if input > max { + return max + } + if input < min { + return min + } + return input + } +} diff --git a/pkg/opts/sanitizers/strings.go b/pkg/opts/sanitizers/strings.go new file mode 100644 index 0000000..d1ddc9d --- /dev/null +++ b/pkg/opts/sanitizers/strings.go @@ -0,0 +1,72 @@ +package sanitizers + +import ( + "fmt" + "net" + "os" + "os/user" + "path/filepath" + "strings" +) + +const ( + NetAddress = "netaddress" + Password = "password" + FilePath = "filepath" + Directory = "directory" +) + +func StringType(typ, input string, defaultPort int) (cleaned string, e error) { + switch typ { + case NetAddress: + var h, p string + if h, p, e = net.SplitHostPort(input); E.Chk(e) { + e = fmt.Errorf("address value '%s' not a valid address", input) + return + } + if p == "" { + cleaned = net.JoinHostPort(h, fmt.Sprint(defaultPort)) + } + case Password: + // password type is mainly here for the input method of the app using this config library + case FilePath: + if strings.HasPrefix(input, "~") { + var homeDir string + var usr *user.User + var e error + if usr, e = user.Current(); e == nil { + homeDir = usr.HomeDir + } + // Fall back to standard HOME environment variable that works for most POSIX OSes if the directory from the Go + // standard lib failed. + if e != nil || homeDir == "" { + homeDir = os.Getenv("HOME") + } + + input = strings.Replace(input, "~", homeDir, 1) + } + if cleaned, e = filepath.Abs(filepath.Clean(input)); E.Chk(e) { + } + case Directory: + if strings.HasPrefix(input, "~") { + var homeDir string + var usr *user.User + var e error + if usr, e = user.Current(); e == nil { + homeDir = usr.HomeDir + } + // Fall back to standard HOME environment variable that works for most POSIX OSes if the directory from the Go + // standard lib failed. + if e != nil || homeDir == "" { + homeDir = os.Getenv("HOME") + } + + input = strings.Replace(input, "~", homeDir, 1) + } + if cleaned, e = filepath.Abs(filepath.Clean(input)); E.Chk(e) { + } + default: + cleaned = input + } + return +} diff --git a/pkg/opts/text/log.go b/pkg/opts/text/log.go new file mode 100644 index 0000000..3dde2f1 --- /dev/null +++ b/pkg/opts/text/log.go @@ -0,0 +1,44 @@ +package text + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/text/string.go b/pkg/opts/text/string.go new file mode 100644 index 0000000..e81f193 --- /dev/null +++ b/pkg/opts/text/string.go @@ -0,0 +1,186 @@ +package text + +import ( + "encoding/json" + "fmt" + "strings" + "sync/atomic" + + "github.com/p9c/p9/pkg/opts/meta" + "github.com/p9c/p9/pkg/opts/opt" + "github.com/p9c/p9/pkg/opts/sanitizers" +) + +// Opt stores a string configuration value +type Opt struct { + meta.Data + hook []Hook + Value *atomic.Value + Def string +} + +type Hook func(s []byte) error + +// New creates a new Opt with a given default value set +func New(m meta.Data, def string, hook ...Hook) *Opt { + v := &atomic.Value{} + v.Store([]byte(def)) + return &Opt{Value: v, Data: m, Def: def, hook: hook} +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("string opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading `=` and retains any following instances of `=` + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + if x.Data.Options != nil { + var matched string + e = fmt.Errorf("option value not found '%s'", input) + for _, i := range x.Data.Options { + op := i + if len(i) >= len(input) { + op = i[:len(input)] + } + if input == op { + if e == nil { + return x, fmt.Errorf("ambiguous short option value '%s' matches multiple options: %s, %s", input, matched, i) + } + matched = i + e = nil + } else { + continue + } + } + if E.Chk(e) { + return + } + input = matched + } else { + var cleaned string + if cleaned, e = sanitizers.StringType(x.Data.Type, input, x.Data.DefaultPort); E.Chk(e) { + return + } + if cleaned != "" { + I.Ln("setting value for", x.Data.Name, cleaned) + input = cleaned + } + } + e = x.Set(input) + return x, e +} + +// LoadInput sets the value from a string +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the stored string +func (x *Opt) V() string { + return string(x.Value.Load().([]byte)) +} + +// Empty returns true if the string is empty +func (x *Opt) Empty() bool { + return len(x.Value.Load().([]byte)) == 0 +} + +// Bytes returns the raw bytes in the underlying storage +// note that this returns a copy because anything done to the slice affects +// all accesses afterwards, thus there is also a zero function +// todo: make an option for the byte buffer to be MMU fenced to prevent +// elevated privilege processes from accessing this memory. +func (x *Opt) Bytes() []byte { + byt := x.Value.Load().([]byte) + o := make([]byte, len(byt)) + copy(o,byt) + return o +} + +// Zero the bytes +func(x *Opt) Zero() { + byt := x.Value.Load().([]byte) + for i := range byt { + byt[i]=0 + } + x.Value.Store(byt) +} + +func (x *Opt) runHooks(s []byte) (e error) { + for i := range x.hook { + if e = x.hook[i](s); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(s string) (e error) { + if e = x.runHooks([]byte(s)); !E.Chk(e) { + x.Value.Store([]byte(s)) + } + return +} + +// SetBytes sets the string from bytes +func (x *Opt) SetBytes(s []byte) (e error) { + if e = x.runHooks(s); !E.Chk(e) { + x.Value.Store(s) + } + return +} + +// Opt returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprintf("%s: '%s'", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := string(x.Value.Load().([]byte)) + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load().([]byte) + e = json.Unmarshal(data, &v) + x.Value.Store(v) + return +} diff --git a/pkg/p9icons/data.go b/pkg/p9icons/data.go new file mode 100644 index 0000000..689036f --- /dev/null +++ b/pkg/p9icons/data.go @@ -0,0 +1,482 @@ +//go:generate go run ./genicons/. -pkg p9icons . +// generated by go run gen.go; DO NOT EDIT + +package p9icons + +var AddressBook = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x16, 0x02, 0x82, + 0x30, 0xcf, 0xcf, 0x30, 0x30, 0xcf, 0xcf, 0xcf, 0xcf, 0x80, 0x80, 0xc0, 0x55, 0xa5, 0x55, 0xb5, + 0xe6, 0x40, 0xe8, 0xad, 0x4a, 0xe7, 0x55, 0xc5, 0xb0, 0xdd, 0x85, 0x80, 0xad, 0x8a, 0xcd, 0x84, + 0xad, 0x8a, 0xad, 0x8a, 0xe9, 0x55, 0xd5, 0xa0, 0xe0, 0x89, 0xb0, 0x35, 0xab, 0x55, 0xb5, 0x55, + 0xa5, 0x55, 0xb5, 0xe1, 0x80, 0x81, 0xc0, 0xad, 0x5a, 0xad, 0x4a, 0xe7, 0x55, 0x85, 0xe9, 0xad, + 0xea, 0xe7, 0xad, 0x7a, 0xb0, 0x25, 0x7a, 0x80, 0x55, 0x75, 0x35, 0x7b, 0x55, 0x75, 0x55, 0x75, + 0xe8, 0x55, 0x55, 0xa0, 0x20, 0x79, 0x4f, 0xcd, 0x54, 0xad, 0x4a, 0xad, 0x5a, 0xad, 0x4a, 0xe1, + 0x80, 0x82, 0xc0, 0xc0, 0x89, 0x80, 0xb6, 0xbd, 0x7f, 0xcd, 0x8c, 0xbd, 0x77, 0x69, 0x92, 0xdd, + 0x71, 0xdd, 0x91, 0x69, 0x7e, 0xbd, 0x7f, 0x69, 0x7a, 0xbd, 0x7f, 0x45, 0x78, 0xbd, 0x7b, 0xdd, + 0x7d, 0xad, 0x82, 0x35, 0x7b, 0x45, 0x84, 0xbd, 0x77, 0x45, 0x84, 0x11, 0x79, 0x80, 0x35, 0x77, + 0x55, 0x79, 0xf1, 0x76, 0xbd, 0x77, 0xbd, 0x7f, 0x25, 0x7e, 0x79, 0x7f, 0x45, 0x7c, 0xbd, 0x7f, + 0x25, 0x7a, 0x45, 0x80, 0x55, 0x7d, 0xf1, 0x82, 0xad, 0x6e, 0x35, 0x8f, 0xad, 0x6e, 0xdd, 0x85, + 0x80, 0x55, 0x89, 0xf1, 0x82, 0xdd, 0x89, 0x79, 0x83, 0x00, 0xa0, 0x89, 0x88, 0xb5, 0x80, 0xcd, + 0x80, 0x79, 0x7f, 0x45, 0x84, 0xf1, 0x82, 0x45, 0x84, 0x99, 0x85, 0x80, 0x69, 0x86, 0x99, 0x75, + 0x69, 0x86, 0xbd, 0x73, 0x45, 0x80, 0xcd, 0x7c, 0xcd, 0x80, 0x25, 0x6a, 0x55, 0x6d, 0x25, 0x6a, + 0x99, 0x6d, 0x80, 0xf1, 0x6a, 0xbd, 0x93, 0xad, 0x6a, 0x89, 0x98, 0xad, 0x7e, 0xad, 0x96, 0xa0, + 0xad, 0x96, 0x35, 0x93, 0xad, 0x96, 0x89, 0x84, 0x80, 0xdd, 0x89, 0x25, 0x7e, 0x69, 0x8a, 0xdd, + 0x7d, 0x20, 0x11, 0x81, 0x55, 0x85, 0xb1, 0x35, 0x7f, 0x89, 0x80, 0xad, 0x7a, 0xf1, 0x82, 0x45, + 0x74, 0xf1, 0x82, 0x25, 0x7a, 0x80, 0x11, 0x65, 0xf1, 0x7e, 0xdd, 0x65, 0x35, 0x63, 0xa1, 0xf1, + 0x6a, 0x99, 0x7d, 0x69, 0x6e, 0x55, 0x65, 0xf1, 0x86, 0x55, 0x65, 0x79, 0x9f, 0x55, 0x65, 0xc0, + 0xf1, 0x7a, 0xc0, 0x89, 0x80, 0xe2, 0x45, 0x80, 0x88, 0xb5, 0xbd, 0x7f, 0xad, 0x82, 0x80, 0xcd, + 0x84, 0x89, 0x80, 0x25, 0x86, 0x89, 0x80, 0x55, 0x81, 0x99, 0x81, 0x25, 0x82, 0x35, 0x83, 0x25, + 0x82, 0x45, 0x80, 0x80, 0xcd, 0x80, 0x80, 0x11, 0x81, 0xbd, 0x7f, 0x89, 0x80, 0xbd, 0x7f, 0xcd, + 0x80, 0xbd, 0x7f, 0x55, 0x81, 0x35, 0x7f, 0x89, 0x80, 0xbd, 0x7f, 0xcd, 0x80, 0x35, 0x7f, 0x55, + 0x81, 0x69, 0x7e, 0x89, 0x80, 0x79, 0x7f, 0xcd, 0x80, 0x69, 0x7e, 0x11, 0x81, 0x55, 0x7d, 0x20, + 0x55, 0x81, 0x99, 0x71, 0xb3, 0x79, 0x7f, 0xbd, 0x7f, 0xad, 0x7e, 0xbd, 0x7f, 0x25, 0x7e, 0xbd, + 0x7f, 0xad, 0x7e, 0x80, 0x99, 0x7d, 0x45, 0x80, 0xcd, 0x7c, 0xcd, 0x80, 0x35, 0x7f, 0x89, 0x80, + 0x69, 0x7e, 0x55, 0x81, 0x99, 0x7d, 0x25, 0x82, 0x79, 0x7f, 0x11, 0x81, 0xf1, 0x7e, 0x25, 0x82, + 0x69, 0x7e, 0x79, 0x83, 0x80, 0x89, 0x80, 0x25, 0x82, 0x45, 0x80, 0x88, 0xe1, +} + +var Balance = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Blocks = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Copy = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Help = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x10, 0x02, 0x81, + 0xcf, 0x80, 0x30, 0xcf, 0x30, 0x30, 0x80, 0x80, 0xc0, 0x55, 0x95, 0x50, 0xe9, 0xad, 0xca, 0xe6, + 0xad, 0x5a, 0xb0, 0x25, 0x7a, 0x80, 0x55, 0x75, 0x35, 0x7b, 0x55, 0x75, 0x55, 0x75, 0xe8, 0x50, + 0xe6, 0x55, 0x95, 0xe1, 0x80, 0x81, 0xc0, 0x55, 0x65, 0x55, 0x4d, 0xe9, 0xad, 0xda, 0xb0, 0x80, + 0xdd, 0x85, 0x35, 0x7b, 0xad, 0x8a, 0x55, 0x75, 0xad, 0x8a, 0xe7, 0x55, 0xcd, 0xb0, 0xdd, 0x85, + 0x80, 0xad, 0x8a, 0x35, 0x7b, 0xad, 0x8a, 0x55, 0x75, 0xe8, 0x55, 0x4d, 0xe6, 0x55, 0x65, 0xe1, +} + +var History = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x10, 0x02, 0x81, + 0xcf, 0xcf, 0xcf, 0xcf, 0x30, 0x30, 0x80, 0x80, 0xc0, 0x55, 0x4d, 0x55, 0xa5, 0xe8, 0x55, 0x65, + 0xe7, 0x55, 0xe5, 0xe9, 0x01, 0xc0, 0xb0, 0x80, 0xdd, 0x85, 0x35, 0x7b, 0xad, 0x8a, 0x55, 0x75, + 0xad, 0x8a, 0xe6, 0x30, 0xa0, 0x25, 0x52, 0xe0, 0x55, 0x4d, 0x35, 0xab, 0x55, 0x4d, 0x55, 0xa5, + 0xe1, 0x80, 0x81, 0xc0, 0xad, 0xb2, 0xad, 0x5a, 0xe9, 0xa0, 0xe6, 0x55, 0x4d, 0xe9, 0x60, 0xb0, + 0x80, 0x25, 0x7a, 0xcd, 0x84, 0x55, 0x75, 0xad, 0x8a, 0x55, 0x75, 0xe7, 0x01, 0xd0, 0xa0, 0xdd, + 0xad, 0x20, 0xad, 0xb2, 0xcd, 0x54, 0xad, 0xb2, 0xad, 0x5a, 0xe1, +} + +var Info = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x10, 0x80, 0xd1, 0xf0, 0xf0, 0x00, 0x04, 0x01, 0xf0, 0x80, 0xf0, 0xf0, 0x00, 0x04, 0x01, 0x10, + 0x80, 0xe2, 0x55, 0x79, 0x58, 0xd1, 0xad, 0x86, 0xad, 0x86, 0x00, 0x04, 0x55, 0x8d, 0x80, 0xad, + 0x86, 0xad, 0x86, 0x00, 0x04, 0xad, 0x72, 0x80, 0xe1, +} + +var Light = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x16, 0x02, 0x82, + 0xcf, 0x80, 0x30, 0xcf, 0xcf, 0x30, 0xcf, 0xcf, 0xcf, 0x80, 0x80, 0xc0, 0xad, 0xa2, 0xad, 0x7a, + 0xb3, 0x80, 0x79, 0x6b, 0x69, 0x6e, 0x35, 0x5b, 0x55, 0x59, 0x99, 0x5d, 0x60, 0xdd, 0x81, 0x35, + 0x63, 0xad, 0x8e, 0x99, 0x61, 0xad, 0x9e, 0xad, 0x7e, 0x45, 0x8c, 0xbd, 0x83, 0x35, 0x97, 0x45, + 0x8c, 0x25, 0x9e, 0xbd, 0x83, 0x35, 0x83, 0x25, 0x86, 0xbd, 0x87, 0x25, 0x86, 0xcd, 0x8c, 0xe8, + 0xad, 0xa2, 0xe7, 0xc0, 0xe9, 0xbd, 0x7f, 0xb0, 0x80, 0x35, 0x7b, 0x25, 0x82, 0x69, 0x76, 0xdd, + 0x85, 0x35, 0x73, 0xa0, 0x99, 0x9d, 0x35, 0x8f, 0xad, 0xa2, 0x99, 0x85, 0xad, 0xa2, 0xad, 0x7a, + 0xe2, 0xad, 0x4a, 0xad, 0x7a, 0xd1, 0x55, 0xb5, 0x55, 0xb5, 0x00, 0x04, 0xad, 0xea, 0x80, 0x55, + 0xb5, 0x55, 0xb5, 0x00, 0x04, 0x55, 0x15, 0x80, 0xe2, 0x70, 0x55, 0xb5, 0xd1, 0x90, 0x90, 0x00, + 0x04, 0xa0, 0x80, 0x90, 0x90, 0x00, 0x04, 0x60, 0x80, 0xe1, 0x80, 0x81, 0xc0, 0x99, 0x91, 0xdd, + 0x75, 0x20, 0x70, 0xad, 0x7a, 0xb0, 0x35, 0x7f, 0x79, 0x7f, 0xdd, 0x7d, 0x79, 0x7f, 0x11, 0x7d, + 0x80, 0x00, 0x80, 0xcd, 0x74, 0x20, 0x99, 0x79, 0xbd, 0x7b, 0xb0, 0x35, 0x7f, 0x79, 0x7f, 0xdd, + 0x7d, 0x79, 0x7f, 0x11, 0x7d, 0x80, 0x20, 0x70, 0x55, 0x85, 0xb0, 0x79, 0x7f, 0x89, 0x80, 0xf1, + 0x7e, 0x11, 0x81, 0xf1, 0x7e, 0xdd, 0x81, 0x90, 0x80, 0x99, 0x81, 0x89, 0x80, 0x25, 0x82, 0x20, + 0x25, 0x8a, 0x89, 0x8c, 0xe8, 0xad, 0xa2, 0xe7, 0x55, 0x85, 0xe8, 0x55, 0x85, 0xb0, 0x80, 0x79, + 0x7f, 0xbd, 0x7f, 0xf1, 0x7e, 0x79, 0x7f, 0x69, 0x7e, 0x22, 0x35, 0x77, 0x11, 0x75, 0x88, 0x55, + 0x7d, 0x69, 0x86, 0x45, 0x84, 0xb0, 0xcd, 0x80, 0x89, 0x80, 0x25, 0x82, 0x89, 0x80, 0xf1, 0x82, + 0x80, 0x22, 0x69, 0x86, 0xbd, 0x7b, 0x88, 0xad, 0x82, 0x35, 0x77, 0xf1, 0x8a, 0xa0, 0xf1, 0x82, + 0x45, 0x84, 0xad, 0x82, 0xcd, 0x84, 0xad, 0x82, 0x55, 0x85, 0xe9, 0x55, 0x9d, 0xe7, 0x55, 0x85, + 0xe8, 0x69, 0x86, 0x20, 0x25, 0x8a, 0x79, 0x73, 0xb0, 0x89, 0x80, 0x79, 0x7f, 0xcd, 0x80, 0xad, + 0x7e, 0x89, 0x80, 0xdd, 0x7d, 0x80, 0x25, 0x92, 0x25, 0x76, 0x99, 0x91, 0xdd, 0x75, 0xe1, 0x80, + 0x82, 0xc0, 0x55, 0x85, 0xf0, 0xe7, 0x55, 0x75, 0xb0, 0x25, 0x7a, 0x80, 0x55, 0x75, 0x35, 0x7b, + 0x55, 0x75, 0x55, 0x75, 0xe9, 0xad, 0x72, 0xe7, 0xc0, 0xe9, 0x55, 0x8d, 0xa0, 0xa0, 0x35, 0xb3, + 0x35, 0x8b, 0xf0, 0x55, 0x85, 0xf0, 0xe1, +} + +var Link = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0xad, 0x5a, 0xad, 0x62, 0xa1, 0x18, 0xad, 0x62, 0x00, 0xad, 0x6e, 0x00, 0x55, 0x7d, 0x00, 0x98, + 0x18, 0xb0, 0xad, 0x5a, 0xb0, 0x00, 0xad, 0x62, 0xb0, 0xa0, 0x81, 0x6f, 0xb0, 0x51, 0x7a, 0xcd, + 0x8e, 0xcd, 0x7c, 0xad, 0x82, 0x00, 0x35, 0x83, 0xad, 0x82, 0xa0, 0xb1, 0x85, 0xcd, 0x8e, 0x81, + 0x90, 0xb0, 0x55, 0x9d, 0xb0, 0x00, 0x55, 0xa5, 0xb0, 0xa1, 0xe8, 0xb0, 0x01, 0xc0, 0x98, 0x01, + 0xc0, 0x55, 0x7d, 0x01, 0xc0, 0xad, 0x6e, 0xe8, 0xad, 0x62, 0x55, 0xa5, 0xad, 0x62, 0x00, 0x55, + 0x9d, 0xad, 0x62, 0xa0, 0x81, 0x90, 0xad, 0x62, 0xb1, 0x85, 0xe1, 0x6b, 0x35, 0x83, 0x70, 0x00, + 0xcd, 0x7c, 0x70, 0xa0, 0x51, 0x7a, 0xe1, 0x6b, 0x81, 0x6f, 0xad, 0x62, 0xad, 0x62, 0xad, 0x62, + 0x00, 0xad, 0x5a, 0xad, 0x62, 0xe2, 0xad, 0x5a, 0x55, 0x6d, 0x00, 0xad, 0x62, 0x55, 0x6d, 0xa0, + 0x99, 0x69, 0x55, 0x6d, 0x85, 0x6f, 0xd1, 0x71, 0xbd, 0x71, 0x70, 0x00, 0x50, 0x70, 0xa1, 0x11, + 0x65, 0x70, 0xad, 0x62, 0x69, 0x7a, 0xad, 0x62, 0x55, 0x7d, 0xad, 0x62, 0x45, 0x80, 0x11, 0x65, + 0xad, 0x82, 0x50, 0xad, 0x82, 0x00, 0xbd, 0x71, 0xad, 0x82, 0xa0, 0x85, 0x6f, 0xdd, 0x88, 0x99, + 0x69, 0x55, 0x8d, 0xad, 0x62, 0x55, 0x8d, 0x00, 0xad, 0x5a, 0x55, 0x8d, 0xa1, 0xdd, 0x51, 0x55, + 0x8d, 0xad, 0x4a, 0x25, 0x86, 0xad, 0x4a, 0x55, 0x7d, 0xad, 0x4a, 0x89, 0x74, 0xdd, 0x51, 0x55, + 0x6d, 0xad, 0x5a, 0x55, 0x6d, 0xe2, 0x55, 0x9d, 0x55, 0x6d, 0x00, 0x55, 0xa5, 0x55, 0x6d, 0xa1, + 0x25, 0xae, 0x55, 0x6d, 0x55, 0xb5, 0x89, 0x74, 0x55, 0xb5, 0x55, 0x7d, 0x55, 0xb5, 0x25, 0x86, + 0x25, 0xae, 0x55, 0x8d, 0x55, 0xa5, 0x55, 0x8d, 0x00, 0x55, 0x9d, 0x55, 0x8d, 0xa0, 0x69, 0x96, + 0x55, 0x8d, 0x7d, 0x90, 0xdd, 0x88, 0x45, 0x8e, 0xad, 0x82, 0x00, 0xb0, 0xad, 0x82, 0xa1, 0xf1, + 0x9a, 0xad, 0x82, 0x55, 0x9d, 0x45, 0x80, 0x55, 0x9d, 0x55, 0x7d, 0x55, 0x9d, 0x69, 0x7a, 0xf1, + 0x9a, 0x70, 0xb0, 0x70, 0x00, 0x45, 0x8e, 0x70, 0xa0, 0x7d, 0x90, 0xd1, 0x71, 0x69, 0x96, 0x55, + 0x6d, 0x55, 0x9d, 0x55, 0x6d, 0xe1, +} + +var LinkOff = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x68, 0xad, 0x4a, 0x03, 0xad, 0x6a, 0x20, 0xf1, 0x7a, 0x99, 0x65, 0x69, 0x7e, 0x79, 0x63, 0x68, + 0xad, 0x4a, 0xe2, 0x98, 0xad, 0x4a, 0x03, 0x99, 0x81, 0x79, 0x63, 0x11, 0x85, 0x99, 0x65, 0x55, + 0x95, 0x20, 0x98, 0xad, 0x4a, 0xe2, 0xad, 0x5a, 0xad, 0x62, 0xa1, 0x18, 0xad, 0x62, 0x00, 0xad, + 0x6e, 0x00, 0x55, 0x7d, 0x00, 0x98, 0x18, 0xb0, 0xad, 0x5a, 0xb0, 0x00, 0xad, 0x62, 0xb0, 0xa0, + 0x99, 0x6d, 0xb0, 0xf1, 0x76, 0x55, 0x91, 0x35, 0x7b, 0x90, 0x00, 0xad, 0x6e, 0x90, 0xa0, 0xbd, + 0x6b, 0x35, 0x8b, 0x79, 0x67, 0x55, 0x8d, 0xad, 0x62, 0x55, 0x8d, 0x00, 0xad, 0x5a, 0x55, 0x8d, + 0xa1, 0xdd, 0x51, 0x55, 0x8d, 0xad, 0x4a, 0x25, 0x86, 0xad, 0x4a, 0x55, 0x7d, 0xad, 0x4a, 0x89, + 0x74, 0xdd, 0x51, 0x55, 0x6d, 0xad, 0x5a, 0x55, 0x6d, 0x00, 0xad, 0x62, 0x55, 0x6d, 0xa0, 0x79, + 0x67, 0x55, 0x6d, 0xbd, 0x6b, 0x79, 0x6f, 0xad, 0x6e, 0xad, 0x72, 0x00, 0x35, 0x7b, 0xad, 0x72, + 0xa0, 0x35, 0x77, 0x55, 0x69, 0x99, 0x6d, 0xad, 0x62, 0xad, 0x62, 0xad, 0x62, 0x00, 0xad, 0x5a, + 0xad, 0x62, 0xe2, 0x55, 0x9d, 0xad, 0x62, 0xa0, 0x69, 0x92, 0xad, 0x62, 0x11, 0x89, 0x55, 0x69, + 0xcd, 0x84, 0xad, 0x72, 0x00, 0x55, 0x91, 0xad, 0x72, 0xa0, 0x45, 0x94, 0x79, 0x6f, 0x89, 0x98, + 0x55, 0x6d, 0x55, 0x9d, 0x55, 0x6d, 0x00, 0x55, 0xa5, 0x55, 0x6d, 0xa1, 0x25, 0xae, 0x55, 0x6d, + 0x55, 0xb5, 0x89, 0x74, 0x55, 0xb5, 0x55, 0x7d, 0x55, 0xb5, 0x25, 0x86, 0x25, 0xae, 0x55, 0x8d, + 0x55, 0xa5, 0x55, 0x8d, 0x00, 0x55, 0x9d, 0x55, 0x8d, 0xa0, 0x89, 0x98, 0x55, 0x8d, 0x45, 0x94, + 0x35, 0x8b, 0x55, 0x91, 0x90, 0x00, 0xcd, 0x84, 0x90, 0xa0, 0xcd, 0x88, 0x55, 0x91, 0x69, 0x92, + 0xb0, 0x55, 0x9d, 0xb0, 0x00, 0x55, 0xa5, 0xb0, 0xa1, 0xe8, 0xb0, 0x01, 0xc0, 0x98, 0x01, 0xc0, + 0x55, 0x7d, 0x01, 0xc0, 0xad, 0x6e, 0xe8, 0xad, 0x62, 0x55, 0xa5, 0xad, 0x62, 0x00, 0x55, 0x9d, + 0xad, 0x62, 0xe2, 0xf1, 0x7a, 0x69, 0x9a, 0x03, 0xad, 0x6a, 0xe0, 0x68, 0x55, 0xb5, 0x69, 0x7e, + 0x89, 0x9c, 0xf1, 0x7a, 0x69, 0x9a, 0xe2, 0x11, 0x85, 0x69, 0x9a, 0x03, 0x99, 0x81, 0x89, 0x9c, + 0x98, 0x55, 0xb5, 0x55, 0x95, 0xe0, 0x11, 0x85, 0x69, 0x9a, 0xe1, +} + +var Loading = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Mine = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x39, 0xa2, 0x49, 0x45, 0xa0, 0x0d, 0x9b, 0x79, 0x45, 0x3d, 0x92, 0x25, 0x48, 0x8d, 0x89, 0x15, + 0x4d, 0x00, 0x95, 0x87, 0x21, 0x4a, 0xa0, 0x61, 0x86, 0x55, 0x48, 0x09, 0x84, 0xe1, 0x47, 0x39, + 0x82, 0x19, 0x49, 0x00, 0x41, 0x7f, 0x15, 0x4b, 0xa0, 0x71, 0x7d, 0x49, 0x4c, 0xf9, 0x7c, 0x9d, + 0x4e, 0x2d, 0x7e, 0x69, 0x50, 0x00, 0x25, 0x80, 0x5d, 0x53, 0xa5, 0xa1, 0x75, 0x91, 0x5b, 0xc5, + 0x6e, 0x79, 0x65, 0x95, 0x6d, 0x81, 0x6d, 0x59, 0x6d, 0x4d, 0x6f, 0x89, 0x6f, 0x71, 0x70, 0xd1, + 0x70, 0x21, 0x6f, 0x21, 0x78, 0xdd, 0x67, 0x09, 0x81, 0x7d, 0x60, 0x45, 0x8b, 0xa5, 0x59, 0x85, + 0x95, 0xad, 0x52, 0xc1, 0x9f, 0x45, 0x4d, 0x11, 0xa9, 0x85, 0x49, 0xcd, 0xaa, 0xb9, 0x48, 0x8d, + 0xaa, 0x65, 0x46, 0xcd, 0xa8, 0xe9, 0x45, 0xd5, 0xa6, 0x6d, 0x45, 0xa1, 0xa4, 0x39, 0x45, 0x39, + 0xa2, 0x49, 0x45, 0xe2, 0x44, 0x69, 0x53, 0x05, 0x25, 0x5e, 0xf9, 0x55, 0x7d, 0x5a, 0xfd, 0x56, + 0xf9, 0x5f, 0x89, 0x5d, 0x55, 0x64, 0xb9, 0x63, 0xb1, 0x62, 0x99, 0x5a, 0x44, 0x69, 0x53, 0xe2, + 0x65, 0x70, 0x05, 0x56, 0x05, 0xd5, 0x6b, 0xc9, 0x56, 0x15, 0x68, 0x39, 0x56, 0x6d, 0x6a, 0x71, + 0x5e, 0xe1, 0x6b, 0xdd, 0x65, 0x19, 0x6e, 0xdd, 0x5c, 0x65, 0x70, 0x05, 0x56, 0xe2, 0x11, 0x4d, + 0x21, 0x5a, 0xa1, 0xfd, 0x49, 0x55, 0x5a, 0xb1, 0x44, 0x9d, 0x5c, 0x45, 0x43, 0x85, 0x5b, 0x6d, + 0x3f, 0x8d, 0x58, 0x65, 0x41, 0x41, 0x5b, 0xd9, 0x40, 0xb9, 0x62, 0x01, 0xd9, 0x40, 0x19, 0xbf, + 0xf1, 0xbc, 0x15, 0xbf, 0xa0, 0xa9, 0xc4, 0x15, 0xbf, 0x49, 0xb9, 0x0d, 0xbc, 0x45, 0xb2, 0x91, + 0xb8, 0x00, 0xd6, 0xf5, 0xac, 0xa0, 0xe5, 0xb1, 0x0d, 0xab, 0x15, 0xa9, 0x41, 0xa7, 0x71, 0xa8, + 0xcd, 0xa6, 0x00, 0xc5, 0x9d, 0x59, 0x9f, 0xa0, 0x81, 0x9c, 0x3d, 0x9e, 0x11, 0x93, 0xa9, 0xa3, + 0x85, 0x91, 0xc5, 0x9b, 0x00, 0x51, 0x89, 0xed, 0x9b, 0xa0, 0xa1, 0x8c, 0x89, 0x95, 0xb9, 0x8e, + 0xed, 0x8c, 0xd9, 0x89, 0xed, 0x87, 0x00, 0xe1, 0x7d, 0x59, 0x81, 0xa0, 0x75, 0x7d, 0xf9, 0x7e, + 0x3d, 0x75, 0x59, 0x7a, 0x66, 0x61, 0x79, 0x00, 0xf5, 0x69, 0xd9, 0x78, 0xa0, 0xd5, 0x63, 0xe9, + 0x78, 0x9d, 0x6a, 0xd5, 0x6d, 0x69, 0x64, 0x29, 0x6f, 0x00, 0x4d, 0x61, 0x29, 0x70, 0xa0, 0x95, + 0x5e, 0xdd, 0x6e, 0xf5, 0x61, 0xf5, 0x63, 0x21, 0x60, 0x1d, 0x63, 0x00, 0x55, 0x4f, 0xa9, 0x5b, + 0xa0, 0x6d, 0x4f, 0x61, 0x5a, 0x79, 0x4e, 0x09, 0x5a, 0x11, 0x4d, 0x21, 0x5a, 0xe2, 0x19, 0x93, + 0x2d, 0x5b, 0xa1, 0x79, 0x91, 0x21, 0x5c, 0xfd, 0x8f, 0x21, 0x5d, 0x55, 0x8e, 0x3d, 0x5e, 0xb1, + 0x8c, 0x55, 0x5f, 0x31, 0x8b, 0x55, 0x60, 0xb1, 0x89, 0x75, 0x61, 0x00, 0x19, 0xa1, 0x79, 0x84, + 0xa0, 0x29, 0xa2, 0x3d, 0x86, 0x7d, 0xa4, 0xb1, 0x86, 0x71, 0xa6, 0x81, 0x85, 0x00, 0x69, 0xa9, + 0x85, 0x83, 0xa0, 0x3d, 0xab, 0x51, 0x82, 0xbd, 0xab, 0xd9, 0x7f, 0x81, 0xaa, 0x2d, 0x7e, 0x00, + 0x19, 0x93, 0x2d, 0x5b, 0xe2, 0x21, 0x84, 0x4d, 0x66, 0x05, 0xa1, 0x7d, 0xdd, 0x6b, 0x7d, 0x77, + 0x45, 0x70, 0x95, 0x80, 0x89, 0x6e, 0xc9, 0x87, 0xc9, 0x6d, 0x2d, 0x85, 0xf5, 0x69, 0x21, 0x84, + 0x4d, 0x66, 0xe2, 0x8d, 0x89, 0xb5, 0x75, 0x05, 0x05, 0x81, 0x41, 0x76, 0x75, 0x79, 0x19, 0x76, + 0xc5, 0x81, 0x2d, 0x7a, 0xf9, 0x87, 0xe1, 0x7d, 0x31, 0x88, 0x41, 0x79, 0x8d, 0x89, 0xb5, 0x75, + 0xe1, +} + +var NoLight = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x16, 0x02, 0x82, + 0xcf, 0xcf, 0x30, 0xcf, 0xcf, 0xcf, 0x30, 0x30, 0xcf, 0x80, 0x80, 0xc0, 0xad, 0xa2, 0xad, 0x7a, + 0xb3, 0x80, 0x79, 0x6b, 0x69, 0x6e, 0x35, 0x5b, 0x55, 0x59, 0x99, 0x5d, 0x60, 0xdd, 0x81, 0x35, + 0x63, 0xad, 0x8e, 0x99, 0x61, 0xad, 0x9e, 0xad, 0x7e, 0x45, 0x8c, 0xbd, 0x83, 0x35, 0x97, 0x45, + 0x8c, 0x25, 0x9e, 0xbd, 0x83, 0x35, 0x83, 0x25, 0x86, 0xbd, 0x87, 0x25, 0x86, 0xcd, 0x8c, 0xe8, + 0xad, 0xa2, 0xe7, 0xc0, 0xe9, 0xbd, 0x7f, 0xb0, 0x80, 0x35, 0x7b, 0x25, 0x82, 0x69, 0x76, 0xdd, + 0x85, 0x35, 0x73, 0xa0, 0x99, 0x9d, 0x35, 0x8f, 0xad, 0xa2, 0x99, 0x85, 0xad, 0xa2, 0xad, 0x7a, + 0xe2, 0x70, 0x55, 0xb5, 0xd1, 0x90, 0x90, 0x00, 0x04, 0xa0, 0x80, 0x90, 0x90, 0x00, 0x04, 0x60, + 0x80, 0xe1, 0x80, 0x81, 0xc0, 0x99, 0x91, 0xdd, 0x75, 0x20, 0x70, 0xad, 0x7a, 0xb0, 0x35, 0x7f, + 0x79, 0x7f, 0xdd, 0x7d, 0x79, 0x7f, 0x11, 0x7d, 0x80, 0x00, 0x80, 0xcd, 0x74, 0x20, 0x99, 0x79, + 0xbd, 0x7b, 0xb0, 0x35, 0x7f, 0x79, 0x7f, 0xdd, 0x7d, 0x79, 0x7f, 0x11, 0x7d, 0x80, 0x20, 0x70, + 0x55, 0x85, 0xb0, 0x79, 0x7f, 0x89, 0x80, 0xf1, 0x7e, 0x11, 0x81, 0xf1, 0x7e, 0xdd, 0x81, 0x90, + 0x80, 0x99, 0x81, 0x89, 0x80, 0x25, 0x82, 0x20, 0x25, 0x8a, 0x89, 0x8c, 0xe8, 0xad, 0xa2, 0xe7, + 0x55, 0x85, 0xe8, 0x55, 0x85, 0xb0, 0x80, 0x79, 0x7f, 0xbd, 0x7f, 0xf1, 0x7e, 0x79, 0x7f, 0x69, + 0x7e, 0x22, 0x35, 0x77, 0x11, 0x75, 0x88, 0x55, 0x7d, 0x69, 0x86, 0x45, 0x84, 0xb0, 0xcd, 0x80, + 0x89, 0x80, 0x25, 0x82, 0x89, 0x80, 0xf1, 0x82, 0x80, 0x22, 0x69, 0x86, 0xbd, 0x7b, 0x88, 0xad, + 0x82, 0x35, 0x77, 0xf1, 0x8a, 0xa0, 0xf1, 0x82, 0x45, 0x84, 0xad, 0x82, 0xcd, 0x84, 0xad, 0x82, + 0x55, 0x85, 0xe9, 0x55, 0x9d, 0xe7, 0x55, 0x85, 0xe8, 0x69, 0x86, 0x20, 0x25, 0x8a, 0x79, 0x73, + 0xb0, 0x89, 0x80, 0x79, 0x7f, 0xcd, 0x80, 0xad, 0x7e, 0x89, 0x80, 0xdd, 0x7d, 0x80, 0x25, 0x92, + 0x25, 0x76, 0x99, 0x91, 0xdd, 0x75, 0xe1, 0x80, 0x82, 0xc0, 0x55, 0x85, 0xf0, 0xe7, 0x55, 0x75, + 0xb0, 0x25, 0x7a, 0x80, 0x55, 0x75, 0x35, 0x7b, 0x55, 0x75, 0x55, 0x75, 0xe9, 0xad, 0x72, 0xe7, + 0xc0, 0xe9, 0x55, 0x8d, 0xa0, 0xa0, 0x35, 0xb3, 0x35, 0x8b, 0xf0, 0x55, 0x85, 0xf0, 0xe1, +} + +var NoMine = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x99, 0x9d, 0x00, 0x00, 0x05, 0x9a, 0x09, 0x40, 0xa0, 0xd5, 0x97, 0x11, 0x40, 0x2d, 0x96, 0xc1, + 0x41, 0x31, 0x96, 0xe9, 0x43, 0x00, 0x39, 0x96, 0x75, 0x47, 0xa5, 0xf1, 0x88, 0x91, 0x48, 0xc5, + 0x7d, 0x1d, 0x4d, 0x61, 0x78, 0x29, 0x53, 0x35, 0x77, 0x8d, 0x54, 0x69, 0x78, 0xb5, 0x56, 0x31, + 0x7a, 0x4d, 0x56, 0x4d, 0x84, 0x39, 0x54, 0xc9, 0x8f, 0xf1, 0x52, 0x19, 0x9c, 0xd1, 0x52, 0x7d, + 0xa8, 0x99, 0x52, 0x05, 0xb4, 0xa9, 0x53, 0xdd, 0xbd, 0xa1, 0x55, 0xc1, 0xbf, 0xe9, 0x55, 0xd1, + 0xc0, 0xd5, 0x53, 0x9d, 0xbf, 0x79, 0x52, 0x19, 0xba, 0x85, 0x4c, 0xd9, 0xae, 0x35, 0x48, 0x89, + 0xa1, 0x59, 0x47, 0x00, 0x81, 0xa1, 0xcd, 0x43, 0xa0, 0x79, 0xa1, 0xa5, 0x41, 0xc5, 0x9f, 0xfd, + 0x3f, 0x99, 0x9d, 0x00, 0xe2, 0x05, 0x9f, 0x51, 0x58, 0xa1, 0x15, 0x9e, 0x4d, 0x58, 0x25, 0x9d, + 0x51, 0x58, 0x25, 0x9c, 0x55, 0x58, 0x29, 0x9a, 0x59, 0x58, 0x61, 0x98, 0x5d, 0x58, 0x7d, 0x96, + 0x79, 0x58, 0x00, 0xe9, 0x96, 0x99, 0x82, 0xa0, 0xd5, 0x96, 0xa9, 0x84, 0x89, 0x98, 0x51, 0x86, + 0xd1, 0x9a, 0x65, 0x86, 0x00, 0x65, 0x9e, 0x5d, 0x86, 0xa0, 0x95, 0xa0, 0x55, 0x86, 0x59, 0xa2, + 0x8d, 0x84, 0x39, 0xa2, 0x7d, 0x82, 0x00, 0xcd, 0xa1, 0x5d, 0x58, 0xa0, 0xdd, 0xa0, 0x55, 0x58, + 0xf1, 0x9f, 0x51, 0x58, 0x05, 0x9f, 0x51, 0x58, 0xe2, 0x11, 0x4d, 0x21, 0x5a, 0xa1, 0xfd, 0x49, + 0x55, 0x5a, 0xb1, 0x44, 0x9d, 0x5c, 0x45, 0x43, 0x85, 0x5b, 0x6d, 0x3f, 0x8d, 0x58, 0x65, 0x41, + 0x41, 0x5b, 0xd9, 0x40, 0xb9, 0x62, 0x01, 0xd9, 0x40, 0x19, 0xbf, 0xf1, 0xbc, 0x15, 0xbf, 0xa0, + 0xa9, 0xc4, 0x15, 0xbf, 0x49, 0xb9, 0x0d, 0xbc, 0x45, 0xb2, 0x91, 0xb8, 0x00, 0xd6, 0xf5, 0xac, + 0xa0, 0xe5, 0xb1, 0x0d, 0xab, 0x15, 0xa9, 0x41, 0xa7, 0x71, 0xa8, 0xcd, 0xa6, 0x00, 0xc5, 0x9d, + 0x59, 0x9f, 0xa0, 0x81, 0x9c, 0x3d, 0x9e, 0x11, 0x93, 0xa9, 0xa3, 0x85, 0x91, 0xc5, 0x9b, 0x00, + 0x51, 0x89, 0xed, 0x9b, 0xa0, 0xa1, 0x8c, 0x89, 0x95, 0xb9, 0x8e, 0xed, 0x8c, 0xd9, 0x89, 0xed, + 0x87, 0x00, 0xe1, 0x7d, 0x59, 0x81, 0xa0, 0x75, 0x7d, 0xf9, 0x7e, 0x3d, 0x75, 0x59, 0x7a, 0x66, + 0x61, 0x79, 0x00, 0xf5, 0x69, 0xd9, 0x78, 0xa0, 0xd5, 0x63, 0xe9, 0x78, 0x9d, 0x6a, 0xd5, 0x6d, + 0x69, 0x64, 0x29, 0x6f, 0x00, 0x4d, 0x61, 0x29, 0x70, 0xa0, 0x95, 0x5e, 0xdd, 0x6e, 0xf5, 0x61, + 0xf5, 0x63, 0x21, 0x60, 0x1d, 0x63, 0x00, 0x55, 0x4f, 0xa9, 0x5b, 0xa0, 0x6d, 0x4f, 0x61, 0x5a, + 0x79, 0x4e, 0x09, 0x5a, 0x11, 0x4d, 0x21, 0x5a, 0xe1, +} + +var Ok = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x10, 0x80, 0xd1, 0xf0, 0xf0, 0x00, 0x04, 0x01, 0xf0, 0x80, 0xf0, 0xf0, 0x00, 0x04, 0x01, 0x10, + 0x80, 0xe1, +} + +var Overview = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x0a, 0x02, 0x80, + 0x30, 0xcf, 0x30, 0x80, 0x80, 0xc0, 0x55, 0x89, 0xad, 0x9e, 0xb0, 0x35, 0x7f, 0x80, 0xad, 0x7e, + 0x89, 0x80, 0xad, 0x7e, 0x55, 0x81, 0xe9, 0x55, 0x85, 0xb0, 0x80, 0xcd, 0x80, 0x89, 0x80, 0x55, + 0x81, 0x55, 0x81, 0x55, 0x81, 0x80, 0xad, 0x8a, 0x25, 0xa6, 0xad, 0x8a, 0x55, 0xa5, 0xe9, 0xad, + 0x7a, 0xa0, 0xad, 0x8a, 0x35, 0x9f, 0x25, 0x8a, 0xad, 0x9e, 0x55, 0x89, 0xad, 0x9e, 0xe1, +} + +var ParallelCoin = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x19, 0x8c, 0x75, 0x44, 0xa3, 0x39, 0x85, 0x75, 0x44, 0xd1, 0x7e, 0xe1, 0x45, 0x21, 0x79, 0x35, + 0x49, 0x6d, 0x73, 0xc1, 0x4c, 0x29, 0x6f, 0x09, 0x51, 0x99, 0x6b, 0x7d, 0x56, 0xb5, 0x69, 0x55, + 0x59, 0x49, 0x68, 0x69, 0x5c, 0x55, 0x67, 0xf9, 0x5f, 0x61, 0x66, 0x4d, 0x63, 0xe9, 0x65, 0xdd, + 0x66, 0xe9, 0x65, 0x69, 0x6a, 0x00, 0xe9, 0x65, 0xd9, 0xac, 0xa0, 0x5d, 0x6b, 0x4d, 0xb2, 0xe1, + 0x6f, 0x91, 0xb6, 0xb9, 0x72, 0x69, 0xb9, 0x00, 0xb9, 0x72, 0x69, 0x6a, 0xa5, 0xb9, 0x72, 0x4d, + 0x63, 0x15, 0x75, 0x5d, 0x5d, 0x11, 0x7a, 0x61, 0x58, 0xd1, 0x7e, 0x69, 0x53, 0xfd, 0x84, 0x09, + 0x51, 0x19, 0x8c, 0x09, 0x51, 0x39, 0x93, 0x09, 0x51, 0x25, 0x99, 0x69, 0x53, 0x21, 0x9e, 0x61, + 0x58, 0x1d, 0xa3, 0x5d, 0x5d, 0x7d, 0xa5, 0x4d, 0x63, 0x7d, 0xa5, 0x69, 0x6a, 0x7d, 0xa5, 0x89, + 0x71, 0xe1, 0xa2, 0x75, 0x77, 0xa9, 0x9d, 0x71, 0x7c, 0x71, 0x98, 0x6d, 0x81, 0x81, 0x92, 0x91, + 0x83, 0x19, 0x8c, 0xcd, 0x83, 0x00, 0x19, 0x8c, 0x15, 0x91, 0xa7, 0xa9, 0x8f, 0x15, 0x91, 0x39, + 0x93, 0x9d, 0x90, 0xc9, 0x96, 0xa9, 0x8f, 0xdd, 0x99, 0xb5, 0x8e, 0x2d, 0x9d, 0x0d, 0x8d, 0x45, + 0xa0, 0x29, 0x8b, 0xb9, 0xa5, 0xd5, 0x87, 0xfd, 0xa9, 0x55, 0x83, 0x51, 0xad, 0x65, 0x7d, 0xa5, + 0xb0, 0xb5, 0x77, 0x4d, 0xb2, 0x4d, 0x71, 0x4d, 0xb2, 0xa9, 0x6a, 0x4d, 0xb2, 0xc5, 0x63, 0xe1, + 0xb0, 0x5d, 0x5d, 0x51, 0xad, 0xad, 0x57, 0xfd, 0xa9, 0xf9, 0x51, 0xb9, 0xa5, 0xb5, 0x4d, 0x45, + 0xa0, 0x25, 0x4a, 0x2d, 0x9d, 0x41, 0x48, 0x55, 0x9a, 0xd5, 0x46, 0xc9, 0x96, 0xe1, 0x45, 0x75, + 0x93, 0xed, 0x44, 0xa9, 0x8f, 0x75, 0x44, 0x19, 0x8c, 0x75, 0x44, 0xe2, 0xdd, 0x8b, 0x51, 0x5e, + 0xa3, 0x39, 0x85, 0x51, 0x5e, 0xc5, 0x7f, 0xc5, 0x63, 0xc5, 0x7f, 0x69, 0x6a, 0xc5, 0x7f, 0x4d, + 0x71, 0xfd, 0x84, 0xc1, 0x76, 0xdd, 0x8b, 0x85, 0x76, 0x81, 0x92, 0x85, 0x76, 0xf5, 0x97, 0x11, + 0x71, 0xf5, 0x97, 0x69, 0x6a, 0xf5, 0x97, 0xc5, 0x63, 0x81, 0x92, 0x51, 0x5e, 0xdd, 0x8b, 0x51, + 0x5e, 0xe2, 0x9d, 0x4f, 0xad, 0x65, 0x00, 0x9d, 0x4f, 0xad, 0x98, 0xa0, 0x39, 0x52, 0x85, 0x9b, + 0xb9, 0x56, 0xcd, 0x9f, 0x69, 0x5c, 0x7d, 0xa5, 0x01, 0x69, 0x5c, 0xad, 0x65, 0x9d, 0x4f, 0xad, + 0x65, 0xe1, +} + +var ParallelCoinRound = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x80, 0x00, 0xa3, 0xa5, 0x5c, 0x00, 0x00, 0xa5, 0x5c, 0x00, 0x80, 0x00, 0x5d, 0xa3, 0xa5, 0x5c, + 0x01, 0xc0, 0x80, 0x01, 0xc0, 0x5d, 0xa3, 0x01, 0xc0, 0x01, 0xc0, 0x5d, 0xa3, 0x01, 0xc0, 0x80, + 0x01, 0xc0, 0xa5, 0x5c, 0x5d, 0xa3, 0x00, 0x80, 0x00, 0xe2, 0x29, 0x88, 0xd9, 0x57, 0xa7, 0x91, + 0x8a, 0xd9, 0x57, 0x21, 0x8d, 0x29, 0x58, 0x5d, 0x8f, 0xcd, 0x58, 0xc5, 0x91, 0x71, 0x59, 0xb1, + 0x93, 0x69, 0x5a, 0xc5, 0x95, 0xb1, 0x5b, 0x71, 0x99, 0x15, 0x5e, 0x55, 0x9c, 0xf5, 0x60, 0x91, + 0x9e, 0xcd, 0x64, 0xf5, 0xa0, 0xa5, 0x68, 0xed, 0xa1, 0xf5, 0x6c, 0xed, 0xa1, 0x99, 0x71, 0xed, + 0xa1, 0x15, 0x76, 0xcd, 0xa0, 0x69, 0x7a, 0x91, 0x9e, 0x3d, 0x7e, 0x55, 0x9c, 0x3d, 0x82, 0x71, + 0x99, 0x49, 0x85, 0xc5, 0x95, 0x85, 0x87, 0xb1, 0x93, 0xcd, 0x88, 0x71, 0x91, 0xed, 0x89, 0x5d, + 0x8f, 0x91, 0x8a, 0xf5, 0x8c, 0x35, 0x8b, 0x91, 0x8a, 0x85, 0x8b, 0x29, 0x88, 0x85, 0x8b, 0x00, + 0x29, 0x88, 0x91, 0x82, 0xa5, 0x7d, 0x8c, 0x69, 0x82, 0x7d, 0x90, 0xf5, 0x80, 0xa8, 0x99, 0x7d, + 0x85, 0x97, 0x3d, 0x7a, 0x49, 0x99, 0x3d, 0x76, 0x49, 0x99, 0x71, 0x71, 0x49, 0x99, 0xa5, 0x6c, + 0xb1, 0x97, 0xa5, 0x68, 0x55, 0x94, 0x49, 0x65, 0xf5, 0x90, 0xed, 0x61, 0xf5, 0x8c, 0x55, 0x60, + 0x29, 0x88, 0x55, 0x60, 0x5d, 0x83, 0x55, 0x60, 0x35, 0x7f, 0xed, 0x61, 0x78, 0x49, 0x65, 0xa5, + 0x78, 0xa5, 0x68, 0x0d, 0x77, 0xa5, 0x6c, 0x0d, 0x77, 0x71, 0x71, 0x00, 0x0d, 0x77, 0xb9, 0xa6, + 0xa0, 0x21, 0x75, 0xcd, 0xa4, 0x15, 0x72, 0xed, 0xa1, 0x69, 0x6e, 0x3d, 0x9e, 0x00, 0x69, 0x6e, + 0x71, 0x71, 0xa3, 0x69, 0x6e, 0x0d, 0x6f, 0xb9, 0x6e, 0xa5, 0x6c, 0x5d, 0x6f, 0x69, 0x6a, 0x60, + 0x50, 0xf5, 0x70, 0xed, 0x65, 0x3d, 0x72, 0x48, 0xa5, 0x74, 0x51, 0x60, 0x85, 0x77, 0x71, 0x5d, + 0x5d, 0x7b, 0x0d, 0x5b, 0x35, 0x7f, 0xcd, 0x58, 0x85, 0x83, 0xd9, 0x57, 0x29, 0x88, 0xd9, 0x57, + 0xe2, 0x90, 0x49, 0x69, 0xa3, 0x7d, 0x8c, 0x49, 0x69, 0x29, 0x90, 0xf5, 0x6c, 0x29, 0x90, 0x71, + 0x71, 0x29, 0x90, 0xed, 0x75, 0x7d, 0x8c, 0x99, 0x79, 0x90, 0x99, 0x79, 0x5d, 0x83, 0xc5, 0x79, + 0xd9, 0x7f, 0x15, 0x76, 0xd9, 0x7f, 0x71, 0x71, 0xd9, 0x7f, 0xf5, 0x6c, 0x85, 0x83, 0x49, 0x69, + 0x90, 0x49, 0x69, 0xe2, 0x5d, 0x5f, 0x3d, 0x6e, 0x01, 0x50, 0x3d, 0x6e, 0x50, 0x49, 0x99, 0xa0, + 0x29, 0x64, 0x71, 0x95, 0x21, 0x61, 0x91, 0x92, 0x5d, 0x5f, 0xa5, 0x90, 0x00, 0x5d, 0x5f, 0x3d, + 0x6e, 0xe1, +} + +var Peers = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x80, 0x80, 0xc0, + 0x55, 0x6d, 0x80, 0xd1, 0xad, 0x92, 0xad, 0x92, 0x00, 0x04, 0x55, 0xa5, 0x80, 0xad, 0x92, 0xad, + 0x92, 0x00, 0x04, 0xad, 0x5a, 0x80, 0xe1, +} + +var Receive = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Received = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x08, 0x02, 0x40, + 0x88, 0x8f, 0x80, 0x80, 0xc0, 0xad, 0x52, 0xad, 0xaa, 0xe8, 0x55, 0x55, 0xb0, 0x80, 0x25, 0x7a, + 0xcd, 0x84, 0x55, 0x75, 0xad, 0x8a, 0x55, 0x75, 0xe7, 0x01, 0xc0, 0xb0, 0xdd, 0x85, 0x80, 0xad, + 0x8a, 0xcd, 0x84, 0xad, 0x8a, 0xad, 0x8a, 0xe9, 0x55, 0xd5, 0xb0, 0x80, 0xdd, 0x85, 0x35, 0x7b, + 0xad, 0x8a, 0x55, 0x75, 0xad, 0x8a, 0xe6, 0x55, 0x5d, 0xa0, 0x79, 0x57, 0x55, 0xb5, 0xad, 0x52, + 0x89, 0xb0, 0xad, 0x52, 0xad, 0xaa, 0xe1, +} + +var Search = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x0a, 0x02, 0x80, + 0xbb, 0xde, 0xfb, 0x80, 0x80, 0xc0, 0xbd, 0x87, 0xdd, 0x65, 0xb0, 0x79, 0x7b, 0xad, 0x7a, 0xcd, + 0x74, 0x79, 0x77, 0x99, 0x6d, 0x79, 0x77, 0x90, 0x25, 0x72, 0x35, 0x83, 0x99, 0x6d, 0x89, 0x88, + 0xb1, 0xf1, 0x7e, 0x11, 0x81, 0x35, 0x7f, 0xf1, 0x82, 0x45, 0x80, 0xbd, 0x83, 0x11, 0x81, 0x11, + 0x81, 0xf1, 0x82, 0xcd, 0x80, 0xbd, 0x83, 0xbd, 0x7f, 0xa0, 0xad, 0x6a, 0x11, 0x65, 0xbd, 0x6f, + 0xad, 0x62, 0x55, 0x75, 0xad, 0x62, 0x90, 0xad, 0x8a, 0x69, 0x82, 0x69, 0x8e, 0xad, 0x86, 0xb1, + 0x89, 0x80, 0x89, 0x80, 0x55, 0x81, 0x11, 0x81, 0x25, 0x82, 0x11, 0x81, 0x89, 0x80, 0x80, 0x55, + 0x81, 0xbd, 0x7f, 0x99, 0x81, 0x79, 0x7f, 0xa0, 0x89, 0x88, 0xcd, 0x68, 0x89, 0x88, 0xf1, 0x66, + 0xbd, 0x87, 0xdd, 0x65, 0xe2, 0xad, 0x52, 0x55, 0x75, 0xd1, 0xad, 0xa2, 0xad, 0xa2, 0x00, 0x04, + 0x55, 0xc5, 0x80, 0xad, 0xa2, 0xad, 0xa2, 0x00, 0x04, 0xad, 0x3a, 0x80, 0xe1, +} + +var Send = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Sent = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x08, 0x02, 0x40, + 0x88, 0x8f, 0x80, 0x80, 0xc0, 0xad, 0x52, 0xad, 0xaa, 0xe8, 0x55, 0x55, 0xb0, 0x80, 0x25, 0x7a, + 0xcd, 0x84, 0x55, 0x75, 0xad, 0x8a, 0x55, 0x75, 0xe7, 0x01, 0xc0, 0xb0, 0xdd, 0x85, 0x80, 0xad, + 0x8a, 0xcd, 0x84, 0xad, 0x8a, 0xad, 0x8a, 0xe9, 0x55, 0xd5, 0xb0, 0x80, 0xdd, 0x85, 0x35, 0x7b, + 0xad, 0x8a, 0x55, 0x75, 0xad, 0x8a, 0xe6, 0x55, 0x5d, 0xa0, 0x79, 0x57, 0x55, 0xb5, 0xad, 0x52, + 0x89, 0xb0, 0xad, 0x52, 0xad, 0xaa, 0xe1, +} + +var Settings = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x10, 0x02, 0x81, + 0x88, 0x88, 0x88, 0x30, 0x30, 0x30, 0x80, 0x80, 0xc0, 0x99, 0xa9, 0x89, 0x88, 0xb0, 0x45, 0x80, + 0x25, 0x7e, 0x89, 0x80, 0x45, 0x7c, 0x89, 0x80, 0x25, 0x7a, 0x90, 0xbd, 0x7f, 0x78, 0x79, 0x7f, + 0x25, 0x7a, 0x20, 0x98, 0x79, 0x77, 0xb0, 0x11, 0x81, 0x35, 0x7f, 0x99, 0x81, 0x99, 0x7d, 0xcd, + 0x80, 0x45, 0x7c, 0x00, 0xad, 0xaa, 0xcd, 0x5c, 0xb0, 0x35, 0x7f, 0xad, 0x7e, 0xdd, 0x7d, 0x25, + 0x7e, 0x89, 0x7c, 0xf1, 0x7e, 0x20, 0xad, 0x72, 0x25, 0x86, 0xb0, 0xcd, 0x7c, 0x99, 0x7d, 0x99, + 0x79, 0xbd, 0x7b, 0xdd, 0x75, 0x25, 0x7a, 0x20, 0xad, 0x7e, 0x55, 0x71, 0xb0, 0xbd, 0x7f, 0xad, + 0x7e, 0xad, 0x7e, 0x99, 0x7d, 0x55, 0x7d, 0x99, 0x7d, 0xe7, 0x11, 0x69, 0xb0, 0xad, 0x7e, 0x80, + 0x55, 0x7d, 0x11, 0x81, 0x55, 0x7d, 0x69, 0x82, 0x20, 0xad, 0x7e, 0xad, 0x8e, 0xb0, 0x45, 0x7c, + 0x99, 0x81, 0xcd, 0x78, 0x79, 0x83, 0xdd, 0x75, 0xdd, 0x85, 0x20, 0xad, 0x72, 0xdd, 0x79, 0xb0, + 0xad, 0x7e, 0x79, 0x7f, 0x11, 0x7d, 0x80, 0x89, 0x7c, 0x11, 0x81, 0x20, 0x89, 0x74, 0xbd, 0x93, + 0xb0, 0x35, 0x7f, 0x55, 0x81, 0xbd, 0x7f, 0xf1, 0x82, 0xcd, 0x80, 0xbd, 0x83, 0x20, 0x98, 0x89, + 0x88, 0xb0, 0xbd, 0x7f, 0xdd, 0x81, 0x79, 0x7f, 0xbd, 0x83, 0x79, 0x7f, 0xdd, 0x85, 0x90, 0x45, + 0x80, 0x88, 0x89, 0x80, 0xdd, 0x85, 0x00, 0xad, 0x4a, 0x11, 0x91, 0xb0, 0xf1, 0x7e, 0xcd, 0x80, + 0x69, 0x7e, 0x69, 0x82, 0x35, 0x7f, 0xbd, 0x83, 0x00, 0x55, 0x55, 0x89, 0xa8, 0xb0, 0xcd, 0x80, + 0x55, 0x81, 0x25, 0x82, 0xdd, 0x81, 0x79, 0x83, 0x11, 0x81, 0x20, 0x55, 0x8d, 0xdd, 0x79, 0xb0, + 0x35, 0x83, 0x69, 0x82, 0x69, 0x86, 0x45, 0x84, 0x25, 0x8a, 0xdd, 0x85, 0x20, 0x55, 0x81, 0xad, + 0x8e, 0xb0, 0x45, 0x80, 0x55, 0x81, 0x55, 0x81, 0x69, 0x82, 0xad, 0x82, 0x69, 0x82, 0xe7, 0xf1, + 0x96, 0xb0, 0x55, 0x81, 0x80, 0xad, 0x82, 0xf1, 0x7e, 0xad, 0x82, 0x99, 0x7d, 0x20, 0x55, 0x81, + 0x55, 0x71, 0xb0, 0xbd, 0x83, 0x69, 0x7e, 0x35, 0x87, 0x89, 0x7c, 0x25, 0x8a, 0x25, 0x7a, 0x20, + 0x55, 0x8d, 0x25, 0x86, 0xb0, 0x55, 0x81, 0x89, 0x80, 0xf1, 0x82, 0x80, 0x79, 0x83, 0xf1, 0x7e, + 0x20, 0x79, 0x8b, 0x45, 0x6c, 0xb0, 0xcd, 0x80, 0xad, 0x7e, 0x45, 0x80, 0x11, 0x7d, 0x35, 0x7f, + 0x45, 0x7c, 0x00, 0x99, 0xa9, 0x89, 0x88, 0xe2, 0x80, 0x55, 0x9d, 0xb2, 0x55, 0x71, 0x80, 0x55, + 0x65, 0x68, 0x55, 0x65, 0x55, 0x65, 0x80, 0x55, 0x71, 0x98, 0x55, 0x65, 0xad, 0x9a, 0x55, 0x65, + 0xad, 0x8e, 0x80, 0xad, 0x9a, 0x98, 0xad, 0x9a, 0xad, 0x9a, 0xa0, 0xad, 0x9a, 0x55, 0x91, 0xad, + 0x8e, 0x55, 0x9d, 0x80, 0x55, 0x9d, 0xe1, 0x80, 0x81, 0xc0, 0x80, 0xad, 0x62, 0xb1, 0x69, 0x6e, + 0x80, 0x40, 0x69, 0x8e, 0x40, 0xc0, 0x80, 0x99, 0x91, 0x69, 0x8e, 0xc0, 0xc0, 0xc0, 0x90, 0xc0, + 0x99, 0x71, 0xc0, 0x40, 0xa0, 0xc0, 0x11, 0x71, 0x99, 0x91, 0xad, 0x62, 0x80, 0xad, 0x62, 0xe2, + 0x80, 0xa0, 0xb1, 0x89, 0x78, 0x80, 0xad, 0x72, 0x25, 0x7a, 0xad, 0x72, 0xad, 0x72, 0x80, 0x89, + 0x78, 0xdd, 0x85, 0xad, 0x72, 0x55, 0x8d, 0xad, 0x72, 0x90, 0x55, 0x8d, 0xdd, 0x85, 0x55, 0x8d, + 0x55, 0x8d, 0xa0, 0x55, 0x8d, 0x25, 0x8a, 0x79, 0x87, 0xa0, 0x80, 0xa0, 0xe1, +} + +var Terminal = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x0a, 0x02, 0x80, + 0x62, 0x62, 0x62, 0x80, 0x80, 0xc0, 0xad, 0xaa, 0x55, 0xa5, 0xe8, 0x55, 0x65, 0xe6, 0x55, 0x55, + 0xe9, 0x01, 0xc0, 0xe7, 0x55, 0xd5, 0xe3, 0x80, 0xad, 0x2a, 0xd0, 0xad, 0x8a, 0xad, 0x8a, 0x00, + 0x04, 0xad, 0x8a, 0xad, 0x8a, 0xe9, 0xad, 0xca, 0xd0, 0xad, 0x8a, 0xad, 0x8a, 0x00, 0x04, 0x55, + 0x75, 0xad, 0x8a, 0xe6, 0x55, 0x55, 0xd0, 0xad, 0x8a, 0xad, 0x8a, 0x00, 0x04, 0x55, 0x75, 0x55, + 0x75, 0xe8, 0xad, 0x5a, 0xd0, 0xad, 0x8a, 0xad, 0x8a, 0x00, 0x04, 0xad, 0x8a, 0x55, 0x75, 0xe7, + 0x55, 0xd5, 0xe3, 0xad, 0x5a, 0xad, 0xca, 0xe9, 0x55, 0x75, 0xe7, 0xad, 0x9a, 0xe9, 0xad, 0x8a, + 0xe7, 0x55, 0x65, 0xe3, 0xc5, 0x6d, 0xad, 0x6a, 0x00, 0xb5, 0x5d, 0x60, 0xe6, 0xcd, 0x6c, 0x20, + 0x99, 0x91, 0x99, 0x91, 0xb0, 0x15, 0x82, 0x15, 0x82, 0x15, 0x82, 0x81, 0x85, 0x80, 0x95, 0x87, + 0x00, 0xe9, 0x6c, 0xad, 0x9a, 0xe6, 0xd1, 0x5d, 0x20, 0x49, 0x95, 0xad, 0x6a, 0xe1, +} + +var TxNumber = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, +} + +var Unconfirmed = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x04, 0x0e, 0x00, 0x00, 0x00, 0x01, 0xc0, 0x01, 0xc0, 0x10, 0x02, 0x81, + 0xcf, 0xcf, 0x30, 0x30, 0x30, 0x30, 0x80, 0x80, 0xc0, 0xbd, 0x5f, 0xe0, 0x20, 0x69, 0xa6, 0xbd, + 0x3f, 0xb0, 0x25, 0x82, 0x89, 0x7c, 0x35, 0x87, 0x89, 0x7c, 0x11, 0x89, 0x80, 0x00, 0x99, 0xb5, + 0xe0, 0xb0, 0x25, 0x82, 0x79, 0x83, 0x79, 0x7f, 0x90, 0x79, 0x7b, 0x90, 0xe6, 0x45, 0x64, 0xa0, + 0x45, 0x60, 0xf0, 0x99, 0x5d, 0x79, 0xb3, 0xbd, 0x5f, 0xe0, 0xe2, 0x10, 0x55, 0x6d, 0xd1, 0x55, + 0xa5, 0x55, 0xa5, 0x00, 0x04, 0xad, 0xca, 0x80, 0x55, 0xa5, 0x55, 0xa5, 0x00, 0x04, 0x55, 0x35, + 0x80, 0xe2, 0x20, 0x55, 0x6d, 0xd1, 0x55, 0x9d, 0x55, 0x9d, 0x00, 0x04, 0xad, 0xba, 0x80, 0x55, + 0x9d, 0x55, 0x9d, 0x00, 0x04, 0x55, 0x45, 0x80, 0xe2, 0x50, 0x55, 0x6d, 0xd1, 0x55, 0x85, 0x55, + 0x85, 0x00, 0x04, 0xad, 0x8a, 0x80, 0x55, 0x85, 0x55, 0x85, 0x00, 0x04, 0x55, 0x75, 0x80, 0xe2, + 0xad, 0x6a, 0x55, 0x6d, 0xd1, 0xad, 0x82, 0xad, 0x82, 0x00, 0x04, 0x55, 0x85, 0x80, 0xad, 0x82, + 0xad, 0x82, 0x00, 0x04, 0xad, 0x7a, 0x80, 0xe1, 0x80, 0x81, 0xc0, 0x69, 0x86, 0x69, 0xaa, 0xb0, + 0x80, 0x79, 0x7f, 0x80, 0xf1, 0x7e, 0x45, 0x80, 0x69, 0x7e, 0x9d, 0x89, 0x80, 0x35, 0x7f, 0xcd, + 0x80, 0xad, 0x7e, 0xcd, 0x80, 0x79, 0x7f, 0x55, 0x81, 0x35, 0x7f, 0x11, 0x81, 0xbd, 0x7f, 0x99, + 0x81, 0xbd, 0x7f, 0x55, 0x81, 0x80, 0xdd, 0x81, 0x45, 0x80, 0x11, 0x81, 0x89, 0x80, 0x55, 0x81, + 0xcd, 0x80, 0x89, 0x80, 0xcd, 0x80, 0xcd, 0x80, 0x55, 0x81, 0x45, 0x80, 0x11, 0x81, 0x45, 0x80, + 0x99, 0x81, 0x80, 0x11, 0x81, 0xbd, 0x7f, 0x99, 0x81, 0x79, 0x7f, 0xcd, 0x80, 0x35, 0x7f, 0x55, + 0x81, 0x35, 0x7f, 0x89, 0x80, 0xad, 0x7e, 0xcd, 0x80, 0xf1, 0x7e, 0x45, 0x80, 0x25, 0x7e, 0x45, + 0x80, 0xad, 0x7e, 0x80, 0x69, 0x7e, 0xbd, 0x7f, 0xf1, 0x7e, 0x79, 0x7f, 0xad, 0x7e, 0x35, 0x7f, + 0x79, 0x7f, 0x35, 0x7f, 0x35, 0x7f, 0xad, 0x7e, 0x80, 0x69, 0x86, 0xf1, 0xaa, 0x69, 0x86, 0x69, + 0xaa, 0xe2, 0xdd, 0x8d, 0x25, 0xa2, 0xe7, 0xdd, 0x79, 0x00, 0xad, 0x86, 0x90, 0xe7, 0x90, 0x00, + 0xdd, 0x8d, 0x25, 0xa2, 0xe1, +} + +var Gio = []byte{ + 0x89, 0x49, 0x56, 0x47, 0x02, 0x0a, 0x00, 0x50, 0x50, 0xb0, 0xb0, 0x80, 0x80, 0xc0, 0x19, 0x88, + 0xe5, 0x69, 0xbf, 0x21, 0x80, 0x3d, 0x80, 0x31, 0x80, 0x91, 0x80, 0x29, 0x80, 0xc5, 0x80, 0xe1, + 0x7f, 0xb9, 0x80, 0x71, 0x7f, 0x05, 0x81, 0xd9, 0x7e, 0x35, 0x81, 0xc9, 0x7c, 0x0d, 0x81, 0xad, + 0x79, 0x71, 0x82, 0xc5, 0x76, 0x25, 0x84, 0x95, 0x7e, 0xd5, 0x80, 0x35, 0x7d, 0xc1, 0x81, 0xe9, + 0x7b, 0xc1, 0x82, 0xb1, 0x7c, 0x8d, 0x82, 0xcd, 0x79, 0xa5, 0x85, 0x15, 0x78, 0x6d, 0x89, 0x45, + 0x7f, 0xa1, 0x81, 0xc5, 0x7e, 0x5d, 0x83, 0x8d, 0x7e, 0x21, 0x85, 0xd9, 0x7f, 0x51, 0x81, 0xd1, + 0x7f, 0xa5, 0x82, 0xf9, 0x7f, 0xf9, 0x83, 0x2d, 0x80, 0x69, 0x81, 0x99, 0x80, 0xcd, 0x82, 0x51, + 0x81, 0x09, 0x84, 0x8d, 0x80, 0xf1, 0x80, 0x39, 0x81, 0xcd, 0x81, 0x0d, 0x82, 0x85, 0x82, 0x86, + 0x95, 0x82, 0x31, 0x87, 0xc9, 0x83, 0xdd, 0x8a, 0x39, 0x84, 0xd9, 0x82, 0x59, 0x80, 0x89, 0x85, + 0x35, 0x80, 0x1d, 0x88, 0xb1, 0x7f, 0x21, 0x83, 0x65, 0x7f, 0x99, 0x86, 0xd1, 0x7e, 0xa9, 0x88, + 0x69, 0x7c, 0xb1, 0x80, 0x35, 0x7f, 0x19, 0x81, 0x25, 0x7e, 0x82, 0x2d, 0x7d, 0xd9, 0x7f, 0x65, + 0x7e, 0xd1, 0x7e, 0x21, 0x7d, 0x95, 0x7d, 0x3d, 0x7c, 0x85, 0x7f, 0xa9, 0x7f, 0x7e, 0x5d, 0x7f, + 0x71, 0x7e, 0x31, 0x7f, 0xe5, 0x7f, 0xf9, 0x7f, 0xc9, 0x7f, 0xf1, 0x7f, 0xad, 0x7f, 0xe5, 0x7f, + 0xbf, 0x9d, 0x7f, 0xd5, 0x7f, 0x55, 0x7f, 0x71, 0x7f, 0x41, 0x7f, 0x09, 0x7f, 0xf1, 0x7f, 0xa1, + 0x7f, 0x05, 0x80, 0x45, 0x7f, 0x3d, 0x80, 0x05, 0x7f, 0x81, 0x80, 0x65, 0x7f, 0x21, 0x81, 0x69, + 0x7f, 0xc5, 0x81, 0x9d, 0x7f, 0xb9, 0x80, 0x3d, 0x80, 0x69, 0x81, 0x95, 0x80, 0x11, 0x82, 0xf9, + 0x80, 0x05, 0x81, 0x9d, 0x80, 0xfd, 0x81, 0x55, 0x81, 0xb9, 0x82, 0x41, 0x82, 0xd9, 0x80, 0x0d, + 0x81, 0x85, 0x81, 0x3d, 0x82, 0xd5, 0x81, 0x59, 0x83, 0x81, 0x80, 0xbd, 0x81, 0x49, 0x80, 0xa1, + 0x83, 0x81, 0x7f, 0x41, 0x85, 0x2d, 0x7f, 0xb1, 0x81, 0xb1, 0x7d, 0x45, 0x83, 0x61, 0x7c, 0x41, + 0x84, 0x6d, 0x7e, 0x31, 0x81, 0x95, 0x7c, 0x41, 0x82, 0xf9, 0x7a, 0xd5, 0x82, 0xf9, 0x7c, 0x19, + 0x81, 0xcd, 0x79, 0xb1, 0x81, 0x0d, 0x77, 0xa5, 0x81, 0xa9, 0x7b, 0xed, 0x7f, 0xb9, 0x76, 0x71, + 0x7e, 0x31, 0x73, 0x8d, 0x7c, 0xa9, 0x7d, 0xc1, 0x7e, 0x99, 0x7b, 0x15, 0x7d, 0x05, 0x7a, 0x09, + 0x7b, 0xcd, 0x7d, 0x2d, 0x7d, 0xcd, 0x7c, 0x29, 0x79, 0xe5, 0x7c, 0xfd, 0x75, 0x21, 0x80, 0x89, + 0x7b, 0x11, 0x82, 0xb1, 0x76, 0x61, 0x84, 0x79, 0x73, 0xf1, 0x81, 0x51, 0x7d, 0x69, 0x84, 0x76, + 0x15, 0x87, 0x09, 0x79, 0x51, 0x83, 0x91, 0x7d, 0x8e, 0x9d, 0x7b, 0xd5, 0x8a, 0x09, 0x7a, 0xb1, + 0x71, 0x81, 0x69, 0x7f, 0xe9, 0x82, 0xe1, 0x7e, 0x69, 0x84, 0x65, 0x7e, 0x9d, 0x80, 0xd1, 0x7f, + 0x49, 0x81, 0x31, 0x80, 0x8d, 0x81, 0x9d, 0x80, 0xe1, 0x80, 0x80, 0xc0, 0x45, 0x7b, 0x59, 0x82, + 0xbb, 0x75, 0x80, 0x25, 0x80, 0xb9, 0x80, 0x95, 0x80, 0xd5, 0x80, 0x05, 0x81, 0x5d, 0x80, 0x65, + 0x81, 0xed, 0x80, 0xb5, 0x82, 0x59, 0x81, 0x11, 0x84, 0x1d, 0x80, 0x59, 0x80, 0x39, 0x80, 0xb1, + 0x80, 0x3d, 0x80, 0x0d, 0x81, 0xe9, 0x7f, 0x91, 0x80, 0x7d, 0x7f, 0x82, 0x35, 0x7f, 0x7d, 0x81, + 0xc9, 0x7f, 0x61, 0x80, 0x91, 0x7f, 0xcd, 0x80, 0x35, 0x7f, 0x11, 0x81, 0xe1, 0x7f, 0x19, 0x80, + 0xbd, 0x7f, 0xe9, 0x7f, 0xa1, 0x7f, 0xd5, 0x7f, 0x75, 0x7f, 0x9d, 0x7f, 0x7e, 0x1d, 0x7f, 0x81, + 0x7e, 0xad, 0x7e, 0xc1, 0x7f, 0xc9, 0x7f, 0x8d, 0x7f, 0x7d, 0x7f, 0x95, 0x7f, 0x29, 0x7f, 0xf5, + 0x7f, 0x11, 0x7f, 0x19, 0x80, 0x21, 0x7e, 0x29, 0x80, 0x35, 0x7d, 0x0d, 0x80, 0x4d, 0x7f, 0x1d, + 0x80, 0x99, 0x7e, 0x1d, 0x80, 0xe5, 0x7d, 0x80, 0x99, 0x7f, 0x11, 0x80, 0x29, 0x7f, 0x5d, 0x80, + 0xdd, 0x7e, 0x3d, 0x80, 0xc5, 0x7f, 0x95, 0x80, 0xa9, 0x7f, 0xe5, 0x80, 0xc5, 0x7f, 0xe1, 0x80, + 0x80, 0xc0, 0x69, 0x84, 0x59, 0x84, 0xb3, 0x29, 0x7f, 0x75, 0x80, 0xc9, 0x7e, 0x89, 0x81, 0xf5, + 0x7e, 0x71, 0x82, 0x19, 0x80, 0x7d, 0x80, 0xad, 0x80, 0x91, 0x80, 0x19, 0x81, 0x95, 0x80, 0x5d, + 0x80, 0x05, 0x80, 0xc5, 0x80, 0xe5, 0x7f, 0xdd, 0x80, 0x81, 0x7f, 0x3d, 0x80, 0xf5, 0x7e, 0xd9, + 0x7f, 0x7c, 0x1d, 0x7f, 0x7d, 0x7d, 0xe3, 0xf9, 0x7f, 0xf5, 0x7d, 0xb7, 0x21, 0x81, 0xd9, 0x7f, + 0x51, 0x82, 0x05, 0x80, 0x49, 0x83, 0xa1, 0x80, 0xd9, 0x80, 0x89, 0x80, 0x79, 0x81, 0x69, 0x81, + 0x9d, 0x81, 0x65, 0x82, 0x31, 0x80, 0x81, 0x81, 0xad, 0x7f, 0x1d, 0x83, 0x95, 0x7e, 0x2d, 0x84, + 0x15, 0x7f, 0xe9, 0x80, 0xbd, 0x7d, 0x45, 0x81, 0x75, 0x7c, 0x11, 0x81, 0xbd, 0x7e, 0xcd, 0x7f, + 0x79, 0x7d, 0x29, 0x7f, 0xd9, 0x7c, 0x7c, 0x59, 0x7f, 0xcd, 0x7e, 0x85, 0x7f, 0x4d, 0x7d, 0x19, + 0x80, 0x1d, 0x7c, 0x8d, 0x80, 0xe5, 0x7e, 0x8d, 0x81, 0xe9, 0x7d, 0xcd, 0x82, 0xad, 0x7d, 0x21, + 0x80, 0xfd, 0x7f, 0x3d, 0x80, 0xf9, 0x7f, 0x5d, 0x80, 0xf9, 0x7f, 0xe1, +} + +// In total, 48958 SVG bytes in 29 files converted to 6048 IconVG bytes. diff --git a/pkg/p9icons/data_test.go b/pkg/p9icons/data_test.go new file mode 100644 index 0000000..aa5e021 --- /dev/null +++ b/pkg/p9icons/data_test.go @@ -0,0 +1,38 @@ +// generated by go run genicons.go; DO NOT EDIT + +package p9icons + +var list = []struct { + name string + data []byte +}{ + {"AddressBook", AddressBook}, + {"Balance", Balance}, + {"Blocks", Blocks}, + {"Copy", Copy}, + {"Help", Help}, + {"History", History}, + {"Info", Info}, + {"Light", Light}, + {"Link", Link}, + {"LinkOff", LinkOff}, + {"Loading", Loading}, + {"Mine", Mine}, + {"NoLight", NoLight}, + {"NoMine", NoMine}, + {"Ok", Ok}, + {"Overview", Overview}, + {"ParallelCoin", ParallelCoin}, + {"ParallelCoinRound", ParallelCoinRound}, + {"Peers", Peers}, + {"Receive", Receive}, + {"Received", Received}, + {"Search", Search}, + {"Send", Send}, + {"Sent", Sent}, + {"Settings", Settings}, + {"Terminal", Terminal}, + {"TxNumber", TxNumber}, + {"Unconfirmed", Unconfirmed}, + {"Gio", Gio}, +} diff --git a/pkg/p9icons/genicons/genicons.go b/pkg/p9icons/genicons/genicons.go new file mode 100644 index 0000000..0e06dc0 --- /dev/null +++ b/pkg/p9icons/genicons/genicons.go @@ -0,0 +1,542 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bytes" + "encoding/xml" + "flag" + "fmt" + "go/format" + "image/color" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "golang.org/x/exp/shiny/iconvg" + "golang.org/x/image/math/f32" +) + +var outDir = flag.String("o", "", "output directory") +var pkgName = flag.String("pkg", "icons", "package name") + +var ( + out = new(bytes.Buffer) + failures = []string{} + varNames = []string{} + + totalFiles int + totalIVGBytes int + totalSVGBytes int +) + +func upperCase(s string) string { + if c := s[0]; 'a' <= c && c <= 'z' { + return string(c-0x20) + s[1:] + } + return s +} + +func main() { + flag.Parse() + args := flag.Args() + if len(args) < 1 { + _, _ = fmt.Fprintf(os.Stderr, "please provide a directory to convert\n") + os.Exit(2) + } + iconsDir := args[0] + out.WriteString("//go:generate go run ./genicons/. -pkg p9icons . \n") + out.WriteString("// generated by go run gen.go; DO NOT EDIT\n\npackage ") + out.WriteString(*pkgName) + out.WriteString("\n\n") + if e := genDir(iconsDir); E.Chk(e) { + F.Ln(e) + } + _, _ = fmt.Fprintf( + out, + "// In total, %d SVG bytes in %d files converted to %d IconVG bytes.\n", + totalSVGBytes, totalFiles, totalIVGBytes, + ) + if len(failures) != 0 { + out.WriteString("\n/*\nFAILURES:\n\n") + for _, failure := range failures { + out.WriteString(failure) + out.WriteByte('\n') + } + out.WriteString("\n*/") + } + if *outDir != "" { + if e := os.MkdirAll(*outDir, 0775); e != nil && !os.IsExist(e) { + F.Ln(e) + } + } + raw := out.Bytes() + formatted, e := format.Source(raw) + if e != nil { + log.Fatalf("gofmt failed: %v\n\nGenerated code:\n%s", e, raw) + } + // formatted := raw + if e := ioutil.WriteFile(filepath.Join(*outDir, "data.go"), formatted, 0644); E.Chk(e) { + log.Fatalf("WriteFile failed: %s\n", e) + } + { + b := new(bytes.Buffer) + b.WriteString("// generated by go run genicons.go; DO NOT EDIT\n\npackage ") + b.WriteString(*pkgName) + b.WriteString("\n\n") + b.WriteString("var list = []struct{ name string; data []byte } {\n") + for _, v := range varNames { + _, _ = fmt.Fprintf(b, "{%q, %s},\n", v, v) + } + b.WriteString("}\n\n") + raw := b.Bytes() + formatted, e := format.Source(raw) + if e != nil { + log.Fatalf("gofmt failed: %v\n\nGenerated code:\n%s", e, raw) + } + if e := ioutil.WriteFile(filepath.Join(*outDir, "data_test.go"), formatted, 0644); E.Chk(e) { + log.Fatalf("WriteFile failed: %s\n", e) + } + } +} + +func genDir(dirName string) (e error) { + fqSVGDirName := filepath.FromSlash(dirName) + f, e := os.Open(fqSVGDirName) + if e != nil { + return e + } + defer func() { + if e = f.Close(); E.Chk(e) { + } + }() + + var infos []os.FileInfo + infos, e = f.Readdir(-1) + if e != nil { + F.Ln(e) + } + baseNames, fileNames, sizes := []string{}, map[string]string{}, map[string]int{} + for _, info := range infos { + name := info.Name() + + nameParts := strings.Split(name, "_") + if len(nameParts) != 3 || nameParts[0] != "ic" { + continue + } + baseName := nameParts[1] + var size int + if n, e := fmt.Sscanf(nameParts[2], "%dpx.svg", &size); e != nil || n != 1 { + continue + } + if prevSize, ok := sizes[baseName]; ok { + if size > prevSize { + fileNames[baseName] = name + sizes[baseName] = size + } + } else { + fileNames[baseName] = name + sizes[baseName] = size + baseNames = append(baseNames, baseName) + } + } + + sort.Strings(baseNames) + for _, baseName := range baseNames { + fileName := fileNames[baseName] + path := filepath.Join(dirName, fileName) + f, e := ioutil.ReadFile(path) + if e != nil { + failures = append(failures, fmt.Sprintf("%s: %v", path, e)) + continue + } + if e = genFile(f, baseName, float32(sizes[baseName])); E.Chk(e) { + failures = append(failures, fmt.Sprintf("%s: %v", path, e)) + continue + } + } + return nil +} + +type SVG struct { + Width string `xml:"width,attr"` + Height string `xml:"height,attr"` + Fill string `xml:"fill,attr"` + ViewBox string `xml:"viewBox,attr"` + Paths []*Path `xml:"path"` + // Some of the SVG files contain elements, not just + // elements. IconVG doesn't have circles per se. Instead, we convert such + // circles to paired arcTo commands, tacked on to the first path. + // + // In general, this isn't correct if the circles and the path overlap, but + // that doesn't happen in the specific case of the Material Design icons. + Circles []Circle `xml:"circle"` +} + +type Path struct { + D string `xml:"d,attr"` + Fill string `xml:"fill,attr"` + FillOpacity *float32 `xml:"fill-opacity,attr"` + Opacity *float32 `xml:"opacity,attr"` + + creg uint8 +} + +type Circle struct { + Cx float32 `xml:"cx,attr"` + Cy float32 `xml:"cy,attr"` + R float32 `xml:"r,attr"` +} + +func genFile(svgData []byte, baseName string, outSize float32) (e error) { + var varName string + for _, s := range strings.Split(baseName, "_") { + varName += upperCase(s) + } + _, _ = fmt.Fprintf(out, "var %s = []byte{", varName) + defer func() { + _, _ = fmt.Fprintf(out, "\n}\n\n") + }() + varNames = append(varNames, varName) + + g := &SVG{} + if e = xml.Unmarshal(svgData, g); E.Chk(e) { + return e + } + + var vbx, vby, vbx2, vby2 float32 + for i, v := range strings.Split(g.ViewBox, " ") { + var f float64 + f, e = strconv.ParseFloat(v, 32) + if e != nil { + return fmt.Errorf( + "genFile: failed to parse ViewBox (%q): %v", + g.ViewBox, e, + ) + } + switch i { + case 0: + vbx = float32(f) + case 1: + vby = float32(f) + case 2: + vbx2 = float32(f) + case 3: + vby2 = float32(f) + } + } + dx, dy := outSize, outSize + var size float32 + if aspect := (vbx2 - vbx) / (vby2 - vby); aspect >= 1 { + dy /= aspect + size = vbx2 - vbx + } else { + dx /= aspect + size = vby2 - vby + } + palette := iconvg.DefaultPalette + pmap := make(map[color.RGBA]uint8) + for _, p := range g.Paths { + if p.Fill == "" { + p.Fill = g.Fill + } + var c color.RGBA + c, e = parseColor(p.Fill) + if e != nil { + return e + } + var ok bool + if p.creg, ok = pmap[c]; !ok { + if len(pmap) == 64 { + panic("too many colors") + } + p.creg = uint8(len(pmap)) + palette[p.creg] = c + pmap[c] = p.creg + } + } + var enc iconvg.Encoder + enc.Reset( + iconvg.Metadata{ + ViewBox: iconvg.Rectangle{ + Min: f32.Vec2{-dx * .5, -dy * .5}, + Max: f32.Vec2{+dx * .5, +dy * .5}, + }, + Palette: palette, + }, + ) + + offset := f32.Vec2{ + vbx * outSize / size, + vby * outSize / size, + } + + // adjs maps from opacity to a cReg adj value. + adjs := map[float32]uint8{} + + for _, p := range g.Paths { + if e = genPath(&enc, p, adjs, outSize, size, offset, g.Circles); E.Chk(e) { + return e + } + g.Circles = nil + } + + if len(g.Circles) != 0 { + if e = genPath(&enc, &Path{}, adjs, outSize, size, offset, g.Circles); E.Chk(e) { + return e + } + g.Circles = nil + } + + ivgData, e := enc.Bytes() + if e != nil { + return fmt.Errorf("iconvg encoding failed: %v", e) + } + for i, x := range ivgData { + if i&0x0f == 0x00 { + out.WriteByte('\n') + } + _, _ = fmt.Fprintf(out, "%#02x, ", x) + } + + totalFiles++ + totalSVGBytes += len(svgData) + totalIVGBytes += len(ivgData) + return nil +} + +func parseColor(col string) (color.RGBA, error) { + if col == "none" { + return color.RGBA{}, nil + } + if len(col) == 0 { + return color.RGBA{A: 0xff}, nil + } + if len(col) == 0 || col[0] != '#' { + return color.RGBA{}, fmt.Errorf("invalid color: %q", col) + } + col = col[1:] + if len(col) != 6 { + return color.RGBA{}, fmt.Errorf("invalid color length: %q", col) + } + elems := make([]byte, len(col)/2) + for i := range elems { + u, e := strconv.ParseUint(col[i*2:i*2+2], 16, 8) + if e != nil { + return color.RGBA{}, e + } + elems[i] = byte(u) + } + return color.RGBA{R: elems[0], G: elems[1], B: elems[2], A: 255}, nil +} + +func genPath( + enc *iconvg.Encoder, + p *Path, + adjs map[float32]uint8, + outSize, size float32, + offset f32.Vec2, + circles []Circle, +) (e error) { + adj := uint8(0) + opacity := float32(1) + if p.Opacity != nil { + opacity = *p.Opacity + } else if p.FillOpacity != nil { + opacity = *p.FillOpacity + } + if opacity != 1 { + var ok bool + if adj, ok = adjs[opacity]; !ok { + adj = uint8(len(adjs) + 1) + adjs[opacity] = adj + // Set CREG[0-adj] to be a blend of transparent (0x7f) and the + // first custom palette color (0x80). + enc.SetCReg(adj, false, iconvg.BlendColor(uint8(opacity*0xff), 0x7f, 0x80+p.creg)) + } + } else { + enc.SetCReg(adj, false, iconvg.PaletteIndexColor(p.creg)) + } + + needStartPath := true + if p.D != "" { + needStartPath = false + if e := genPathData(enc, adj, p.D, outSize, size, offset); E.Chk(e) { + return e + } + } + + for _, c := range circles { + // Normalize. + cx := c.Cx * outSize / size + cx -= outSize/2 + offset[0] + cy := c.Cy * outSize / size + cy -= outSize/2 + offset[1] + r := c.R * outSize / size + + if needStartPath { + needStartPath = false + enc.StartPath(adj, cx-r, cy) + } else { + enc.ClosePathAbsMoveTo(cx-r, cy) + } + + // Convert a circle to two relative arcTo ops, each of 180 degrees. + // We can't use one 360 degree arcTo as the start and end point + // would be coincident and the computation is degenerate. + enc.RelArcTo(r, r, 0, false, true, +2*r, 0) + enc.RelArcTo(r, r, 0, false, true, -2*r, 0) + } + + enc.ClosePathEndPath() + return nil +} + +func genPathData(enc *iconvg.Encoder, adj uint8, pathData string, outSize, size float32, offset f32.Vec2) (e error) { + if strings.HasSuffix(pathData, "z") { + pathData = pathData[:len(pathData)-1] + } + r := strings.NewReader(pathData) + + var args [7]float32 + op, relative, started := byte(0), false, false + var count int + for { + b, e := r.ReadByte() + if e == io.EOF { + break + } + if e != nil { + return e + } + count++ + + switch { + case b == ' ' || b == '\n' || b == '\t': + continue + case 'A' <= b && b <= 'Z': + op, relative = b, false + case 'a' <= b && b <= 'z': + op, relative = b, true + default: + if e := r.UnreadByte(); E.Chk(e) { + } + } + + n := 0 + switch op { + case 'A', 'a': + n = 7 + case 'L', 'l', 'T', 't': + n = 2 + case 'Q', 'q', 'S', 's': + n = 4 + case 'C', 'c': + n = 6 + case 'H', 'h', 'V', 'v': + n = 1 + case 'M', 'm': + n = 2 + case 'Z', 'z': + default: + return fmt.Errorf("unknown opcode %c\n", b) + } + + scan(&args, r, n) + normalize(&args, n, op, outSize, size, offset, relative) + + switch op { + case 'A': + enc.AbsArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6]) + case 'a': + enc.RelArcTo(args[0], args[1], args[2], args[3] != 0, args[4] != 0, args[5], args[6]) + case 'L': + enc.AbsLineTo(args[0], args[1]) + case 'l': + enc.RelLineTo(args[0], args[1]) + case 'T': + enc.AbsSmoothQuadTo(args[0], args[1]) + case 't': + enc.RelSmoothQuadTo(args[0], args[1]) + case 'Q': + enc.AbsQuadTo(args[0], args[1], args[2], args[3]) + case 'q': + enc.RelQuadTo(args[0], args[1], args[2], args[3]) + case 'S': + enc.AbsSmoothCubeTo(args[0], args[1], args[2], args[3]) + case 's': + enc.RelSmoothCubeTo(args[0], args[1], args[2], args[3]) + case 'C': + enc.AbsCubeTo(args[0], args[1], args[2], args[3], args[4], args[5]) + case 'c': + enc.RelCubeTo(args[0], args[1], args[2], args[3], args[4], args[5]) + case 'H': + enc.AbsHLineTo(args[0]) + case 'h': + enc.RelHLineTo(args[0]) + case 'V': + enc.AbsVLineTo(args[0]) + case 'v': + enc.RelVLineTo(args[0]) + case 'M': + if !started { + started = true + enc.StartPath(adj, args[0], args[1]) + } else { + enc.ClosePathAbsMoveTo(args[0], args[1]) + } + case 'm': + enc.ClosePathRelMoveTo(args[0], args[1]) + } + } + return nil +} + +func scan(args *[7]float32, r *strings.Reader, n int) { + for i := 0; i < n; i++ { + for { + if b, _ := r.ReadByte(); b != ' ' && b != ',' && b != '\n' && b != '\t' { + if e := r.UnreadByte(); E.Chk(e) { + } + break + } + } + _, _ = fmt.Fscanf(r, "%f", &args[i]) + } +} + +func normalize(args *[7]float32, n int, op byte, outSize, size float32, offset f32.Vec2, relative bool) { + for i := 0; i < n; i++ { + if (op == 'A' || op == 'a') && (i == 3 || i == 4) { + continue + } + args[i] *= outSize / size + if relative { + continue + } + if (op == 'A' || op == 'a') && i < 5 { + // For arcs, skip everything other than x, y. + continue + } + args[i] -= outSize / 2 + switch { + case op == 'A' && i == 5: // Arc x. + args[i] -= offset[0] + case op == 'A' && i == 6: // Arc y. + args[i] -= offset[1] + case n != 1: + args[i] -= offset[i&0x01] + case op == 'H': + args[i] -= offset[0] + case op == 'V': + args[i] -= offset[1] + } + } +} diff --git a/pkg/p9icons/genicons/logmain.go b/pkg/p9icons/genicons/logmain.go new file mode 100644 index 0000000..ad256c8 --- /dev/null +++ b/pkg/p9icons/genicons/logmain.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/p9icons/ic_AddressBook_128px.svg b/pkg/p9icons/ic_AddressBook_128px.svg new file mode 100644 index 0000000..8123384 --- /dev/null +++ b/pkg/p9icons/ic_AddressBook_128px.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/pkg/p9icons/ic_Balance_128px.svg b/pkg/p9icons/ic_Balance_128px.svg new file mode 100644 index 0000000..5901adb --- /dev/null +++ b/pkg/p9icons/ic_Balance_128px.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkg/p9icons/ic_Blocks_128px.svg b/pkg/p9icons/ic_Blocks_128px.svg new file mode 100644 index 0000000..d80d82b --- /dev/null +++ b/pkg/p9icons/ic_Blocks_128px.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/pkg/p9icons/ic_Copy_128px.svg b/pkg/p9icons/ic_Copy_128px.svg new file mode 100644 index 0000000..e871cc8 --- /dev/null +++ b/pkg/p9icons/ic_Copy_128px.svg @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/pkg/p9icons/ic_Help_128px.svg b/pkg/p9icons/ic_Help_128px.svg new file mode 100644 index 0000000..67071c6 --- /dev/null +++ b/pkg/p9icons/ic_Help_128px.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/pkg/p9icons/ic_History_128px.svg b/pkg/p9icons/ic_History_128px.svg new file mode 100644 index 0000000..605192e --- /dev/null +++ b/pkg/p9icons/ic_History_128px.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkg/p9icons/ic_Info_128px.svg b/pkg/p9icons/ic_Info_128px.svg new file mode 100644 index 0000000..fc3727c --- /dev/null +++ b/pkg/p9icons/ic_Info_128px.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/pkg/p9icons/ic_Light_128px.svg b/pkg/p9icons/ic_Light_128px.svg new file mode 100644 index 0000000..4167b29 --- /dev/null +++ b/pkg/p9icons/ic_Light_128px.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/pkg/p9icons/ic_LinkOff_128px.svg b/pkg/p9icons/ic_LinkOff_128px.svg new file mode 100644 index 0000000..c07973f --- /dev/null +++ b/pkg/p9icons/ic_LinkOff_128px.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/pkg/p9icons/ic_Link_128px.svg b/pkg/p9icons/ic_Link_128px.svg new file mode 100644 index 0000000..4b5e044 --- /dev/null +++ b/pkg/p9icons/ic_Link_128px.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/pkg/p9icons/ic_Loading_128px.svg b/pkg/p9icons/ic_Loading_128px.svg new file mode 100644 index 0000000..4ba58eb --- /dev/null +++ b/pkg/p9icons/ic_Loading_128px.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/pkg/p9icons/ic_Mine_128px.svg b/pkg/p9icons/ic_Mine_128px.svg new file mode 100644 index 0000000..0d76077 --- /dev/null +++ b/pkg/p9icons/ic_Mine_128px.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/pkg/p9icons/ic_NoLight_128px.svg b/pkg/p9icons/ic_NoLight_128px.svg new file mode 100644 index 0000000..0a4c55c --- /dev/null +++ b/pkg/p9icons/ic_NoLight_128px.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/pkg/p9icons/ic_NoMine_128px.svg b/pkg/p9icons/ic_NoMine_128px.svg new file mode 100644 index 0000000..97f6583 --- /dev/null +++ b/pkg/p9icons/ic_NoMine_128px.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/pkg/p9icons/ic_Ok_128px.svg b/pkg/p9icons/ic_Ok_128px.svg new file mode 100644 index 0000000..ce308b7 --- /dev/null +++ b/pkg/p9icons/ic_Ok_128px.svg @@ -0,0 +1,6 @@ + + + + diff --git a/pkg/p9icons/ic_Overview_128px.svg b/pkg/p9icons/ic_Overview_128px.svg new file mode 100644 index 0000000..6d92188 --- /dev/null +++ b/pkg/p9icons/ic_Overview_128px.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/pkg/p9icons/ic_ParallelCoinRound_128px.svg b/pkg/p9icons/ic_ParallelCoinRound_128px.svg new file mode 100644 index 0000000..f608aa0 --- /dev/null +++ b/pkg/p9icons/ic_ParallelCoinRound_128px.svg @@ -0,0 +1,60 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/pkg/p9icons/ic_ParallelCoin_128px.svg b/pkg/p9icons/ic_ParallelCoin_128px.svg new file mode 100644 index 0000000..18758fd --- /dev/null +++ b/pkg/p9icons/ic_ParallelCoin_128px.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/pkg/p9icons/ic_Peers_128px.svg b/pkg/p9icons/ic_Peers_128px.svg new file mode 100644 index 0000000..1ba3262 --- /dev/null +++ b/pkg/p9icons/ic_Peers_128px.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/pkg/p9icons/ic_Receive_128px.svg b/pkg/p9icons/ic_Receive_128px.svg new file mode 100644 index 0000000..c820518 --- /dev/null +++ b/pkg/p9icons/ic_Receive_128px.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/pkg/p9icons/ic_Received_128px.svg b/pkg/p9icons/ic_Received_128px.svg new file mode 100644 index 0000000..a127f2c --- /dev/null +++ b/pkg/p9icons/ic_Received_128px.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/pkg/p9icons/ic_Search_128px.svg b/pkg/p9icons/ic_Search_128px.svg new file mode 100644 index 0000000..1c67ec8 --- /dev/null +++ b/pkg/p9icons/ic_Search_128px.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/pkg/p9icons/ic_Send_128px.svg b/pkg/p9icons/ic_Send_128px.svg new file mode 100644 index 0000000..c9aecfb --- /dev/null +++ b/pkg/p9icons/ic_Send_128px.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/pkg/p9icons/ic_Sent_128px.svg b/pkg/p9icons/ic_Sent_128px.svg new file mode 100644 index 0000000..42fa451 --- /dev/null +++ b/pkg/p9icons/ic_Sent_128px.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/pkg/p9icons/ic_Settings_128px.svg b/pkg/p9icons/ic_Settings_128px.svg new file mode 100644 index 0000000..ce7fe51 --- /dev/null +++ b/pkg/p9icons/ic_Settings_128px.svg @@ -0,0 +1,7 @@ + + + + diff --git a/pkg/p9icons/ic_Terminal_128px.svg b/pkg/p9icons/ic_Terminal_128px.svg new file mode 100644 index 0000000..6b8f20e --- /dev/null +++ b/pkg/p9icons/ic_Terminal_128px.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/pkg/p9icons/ic_TxNumber_128px.svg b/pkg/p9icons/ic_TxNumber_128px.svg new file mode 100644 index 0000000..9456ec4 --- /dev/null +++ b/pkg/p9icons/ic_TxNumber_128px.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/pkg/p9icons/ic_Unconfirmed_128px.svg b/pkg/p9icons/ic_Unconfirmed_128px.svg new file mode 100644 index 0000000..6a6825e --- /dev/null +++ b/pkg/p9icons/ic_Unconfirmed_128px.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/pkg/p9icons/ic_gio_48px.svg b/pkg/p9icons/ic_gio_48px.svg new file mode 100644 index 0000000..4df8e8f --- /dev/null +++ b/pkg/p9icons/ic_gio_48px.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/pkg/peer/README.md b/pkg/peer/README.md new file mode 100755 index 0000000..7e0dc60 --- /dev/null +++ b/pkg/peer/README.md @@ -0,0 +1,91 @@ +# peer + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/peer) + +Package peer provides a common base for creating and managing bitcoin network +peers. + +This package has intentionally been designed so it can be used as a standalone +package for any projects needing a full featured bitcoin peer base to build on. + +## Overview + +This package builds upon the wire package, which provides the fundamental +primitives necessary to speak the bitcoin wire protocol, in order to simplify +the process of creating fully functional peers. In essence, it provides a common +base for creating concurrent safe fully validating nodes, Simplified Payment +Verification (SPV) nodes, proxies, etc. + +A quick overview of the major features peer provides are as follows: + +- Provides a basic concurrent safe bitcoin peer for handling bitcoin + communications via the peer-to-peer protocol + +- Full duplex reading and writing of bitcoin protocol messages + +- Automatic handling of the initial handshake process including protocol version + negotiation + +- Asynchronous message queueing of outbound messages with optional channel for + notification when the message is actually sent + +- Flexible peer configuration + + - Caller is responsible for creating outgoing connections and listening for + incoming connections so they have flexibility to establish connections as + they see fit (proxies, etc) + + - User agent name and version + + - Bitcoin network + + - Service support signalling (full nodes, bloom filters, etc) + + - Maximum supported protocol version + + - Ability to register callbacks for handling bitcoin protocol messages + +- Inventory message batching and send trickling with known inventory detection + and avoidance + +- Automatic periodic keep-alive pinging and pong responses + +- Random nonce generation and self connection detection + +- Proper handling of bloom filter related commands when the caller does not + specify the related flag to signal support + + - Disconnects the peer when the protocol version is high enough + + - Does not invoke the related callbacks for older protocol versions + +- Snapshottable peer statistics such as the total number of bytes read and + written, the remote address, user agent, and negotiated protocol version + +- Helper functions pushing addresses, getblocks, getheaders, and reject messages + + - These could all be sent manually via the standard message output function, + but the helpers provide additional nice functionality such as duplicate + filtering and address randomization + +- Ability to wait for shutdown/disconnect + +- Comprehensive test coverage + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/peer +``` + +## Examples + +- [New Outbound Peer Example](https://godoc.org/github.com/p9c/p9/peer#example-package--NewOutboundPeer) + Demonstrates the basic process for initializing and creating an outbound peer. + Peers negotiate by exchanging version and verack messages. For demonstration, + a simple handler for the version message is attached to the peer. + +## License + +Package peer is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/pkg/peer/doc.go b/pkg/peer/doc.go new file mode 100644 index 0000000..bf77e33 --- /dev/null +++ b/pkg/peer/doc.go @@ -0,0 +1,138 @@ +/*Package peer provides a common base for creating and managing Bitcoin network peers. + +Overview + +This package builds upon the wire package, which provides the fundamental primitives necessary to speak the bitcoin wire +protocol, in order to simplify the process of creating fully functional peers. In essence, it provides a common base for +creating concurrent safe fully validating nodes, Simplified Payment Verification (SPV) nodes, proxies, etc. + +A quick overview of the major features peer provides are as follows: + + - Provides a basic concurrent safe bitcoin peer for handling bitcoin communications via the peer-to-peer protocol + + - Full duplex reading and writing of bitcoin protocol messages + + - Automatic handling of the initial handshake process including protocol version negotiation + + - Asynchronous message queuing of outbound messages with optional channel for notification when the message is actually + sent + + - Flexible peer configuration + + - Caller is responsible for creating outgoing connections and listening for incoming connections so they have + flexibility to establish connections asthey see fit (proxies, etc) + + - User agent name and version + + - Bitcoin network + + - Service support signalling (full nodes, bloom filters, etc) + + - Maximum supported protocol version + + - Ability to register callbacks for handling bitcoin protocol messages + + - Inventory message batching and send trickling with known inventory detection and avoidance + + - Automatic periodic keep-alive pinging and pong responses + + - Random Nonce generation and self connection detection + + - Proper handling of bloom filter related commands when the caller does not specify the related flag to signal support + + - Disconnects the peer when the protocol version is high enough + + - Does not invoke the related callbacks for older protocol versions + + - Snapshottable peer statistics such as the total number of bytes read and written, the remote address, user agent, and + negotiated protocol version + + - Helper functions pushing addresses, getblocks, getheaders, and reject messages + + - These could all be sent manually via the standard message output function, but the helpers provide additional nice + functionality such as duplicate filtering and address randomization + + - Ability to wait for shutdown/disconnect + + - Comprehensive test coverage + +Peer Configuration + +All peer configuration is handled with the Config struct. This allows the caller to specify things such as the user +agent name and version, the bitcoin network to use, which services it supports, and callbacks to invoke when bitcoin +messages are received. See the documentation for each field of the Config struct for more details. + +Inbound and Outbound Peers + +A peer can either be inbound or outbound. The caller is responsible for establishing the connection to remote peers and +listening for incoming peers. This provides high flexibility for things such as connecting via proxies, acting as a +proxy, creating bridge peers, choosing whether to listen for inbound peers, etc. + +NewOutboundPeer and NewInboundPeer functions must be followed by calling Connect with a net.Conn instance to the peer. +This will start all async I/O goroutines and initiate the protocol negotiation process. Once finished with the peer call +Disconnect to disconnect from the peer and clean up all resources. + +WaitForDisconnect can be used to block until peer disconnection and resource cleanup has completed. + +Callbacks + +In order to do anything useful with a peer, it is necessary to react to bitcoin messages. This is accomplished by +creating an instance of the MessageListeners struct with the callbacks to be invoke specified and setting the P2PListeners +field of the Config struct specified when creating a peer to it. + +For convenience, a callback hook for all of the currently supported bitcoin messages is exposed which receives the peer +instance and the concrete message type. In addition, a hook for OnRead is provided so even custom messages types for +which this package does not directly provide a hook, as long as they implement the wire.Message interface, can be used. + +Finally, the OnWrite hook is provided, which in conjunction with OnRead, can be used to track server-wide byte counts. + +It is often useful to use closures which encapsulate state when specifying the callback handlers. This provides a clean +method for accessing that state when callbacks are invoked. + +Queuing Messages and Inventory + +The QueueMessage function provides the fundamental means to send messages to the remote peer. As the name implies, this +employs a non-blocking queue. A done channel which will be notified when the message is actually sent can optionally be +specified. + +There are certain message types which are better sent using other functions which provide additional functionality. Of +special interest are inventory messages. Rather than manually sending MsgInv messages via Queuemessage, the inventory +vectors should be queued using the QueueInventory function. + +It employs batching and trickling along with intelligent known remote peer inventory detection and avoidance through the +use of a most-recently used algorithm. + +Message Sending Helper Functions + +In addition to the bare QueueMessage function previously described, the PushAddrMsg, PushGetBlocksMsg, +PushGetHeadersMsg, and PushRejectMsg functions are provided as a convenience. While it is of course possible to create +and send these message manually via QueueMessage, these helper functions provided additional useful functionality that +is typically desired. + +For example, the PushAddrMsg function automatically limits the addresses to the maximum number allowed by the message +and randomizes the chosen addresses when there are too many. This allows the caller to simply provide a slice of known +addresses, such as that returned by the addrmgr package, without having to worry about the details. + +Next, the PushGetBlocksMsg and PushGetHeadersMsg functions will construct proper messages using a block locator and +ignore back to back duplicate requests. + +Finally, the PushRejectMsg function can be used to easily create and send an appropriate reject message based on the +provided parameters as well as optionally provides a flag to cause it to block until the message is actually sent. + +Peer Statistics + +A snapshot of the current peer statistics can be obtained with the StatsSnapshot function. This includes statistics such +as the total number of bytes read and written, the remote address, user agent, and negotiated protocol version. + +Logging + +This package provides extensive logging capabilities through the UseLogger function which allows a btclog.Logger to be +specified. For example, logging at the debug level provides summaries of every message sent and received, and logging at +the trace level provides full dumps of parsed messages as well as the raw message bytes using a format similar to +hexdump -C. + +Bitcoin Improvement Proposals + +This package supports all BIPs supported by the wire package. (https://godoc.org/github.com/p9c/monorepo/wire#hdr-Bitcoin_Improvement_Proposals) +*/ +package peer diff --git a/pkg/peer/example_test.go b/pkg/peer/example_test.go new file mode 100644 index 0000000..3e58279 --- /dev/null +++ b/pkg/peer/example_test.go @@ -0,0 +1,98 @@ +package peer_test + +import ( + "fmt" + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" + "net" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/peer" + "github.com/p9c/p9/pkg/wire" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +// mockRemotePeer creates a basic inbound peer listening on the simnet port for use with Example_peerConnection. It does +// not return until the listner is active. +func mockRemotePeer() (e error) { + // Configure peer to act as a simnet node that offers no services. + peerCfg := &peer.Config{ + UserAgentName: "peer", // User agent name to advertise. + UserAgentVersion: "1.0.0", // User agent version to advertise. + ChainParams: &chaincfg.SimNetParams, + TrickleInterval: time.Second * 10, + } + // Accept connections on the simnet port. + listener, e := net.Listen("tcp", "127.0.0.1:18555") + if e != nil { + return e + } + go func() { + conn, e := listener.Accept() + if e != nil { + E.F("Accept: error %v", e) + return + } + // Create and start the inbound peer. + p := peer.NewInboundPeer(peerCfg) + p.AssociateConnection(conn) + }() + return nil +} + +// This example demonstrates the basic process for initializing and creating an outbound peer. Peers negotiate by +// exchanging version and verack messages. For demonstration, a simple handler for version message is attached to the +// peer. +func Example_newOutboundPeer() { + // Ordinarily this will not be needed since the outbound peer will be connecting to a remote peer, however, since this example is executed and tested, a mock remote peer is needed to listen for the outbound peer. + if e := mockRemotePeer(); E.Chk(e) { + E.F("mockRemotePeer: unexpected error %v", e) + return + } + // Create an outbound peer that is configured to act as a simnet node that offers no services and has listeners for the version and verack messages. The verack listener is used here to signal the code below when the handshake has been finished by signalling a channel. + verack := qu.T() + peerCfg := &peer.Config{ + UserAgentName: "peer", // User agent name to advertise. + UserAgentVersion: "1.0.0", // User agent version to advertise. + ChainParams: &chaincfg.SimNetParams, + Services: 0, + TrickleInterval: time.Second * 10, + Listeners: peer.MessageListeners{ + OnVersion: func(p *peer.Peer, msg *wire.MsgVersion) *wire.MsgReject { + fmt.Println("outbound: received version") + return nil + }, + OnVerAck: func(p *peer.Peer, msg *wire.MsgVerAck) { + verack <- struct{}{} + }, + }, + } + p, e := peer.NewOutboundPeer(peerCfg, "127.0.0.1:18555") + if e != nil { + E.F("NewOutboundPeer: error %v", e) + return + } + // Establish the connection to the peer address and mark it connected. + conn, e := net.Dial("tcp", p.Addr()) + if e != nil { + E.F("net.Dial: error %v", e) + return + } + p.AssociateConnection(conn) + // Wait for the verack message or timeout in case of failure. + select { + case <-verack.Wait(): + case <-time.After(time.Second * 1): + E.F("Example_peerConnection: verack timeout") + } + // Disconnect the peer. + p.Disconnect() + p.WaitForDisconnect() + // Output: + // outbound: received version +} diff --git a/pkg/peer/export_test.go b/pkg/peer/export_test.go new file mode 100644 index 0000000..7aa1251 --- /dev/null +++ b/pkg/peer/export_test.go @@ -0,0 +1,11 @@ +/* +This test file is part of the peer package rather than than the peer_test package so it can bridge access to the +internals to properly test cases which are either not possible or can't reliably be tested via the public interface. The +functions are only exported while the tests are being run. +*/ +package peer + +// TstAllowSelfConns allows the test package to allow self connections by disabling the detection logic. +func TstAllowSelfConns() { + AllowSelfConns = true +} diff --git a/pkg/peer/log.go b/pkg/peer/log.go new file mode 100644 index 0000000..a6d08b1 --- /dev/null +++ b/pkg/peer/log.go @@ -0,0 +1,43 @@ +package peer + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/peer/logging.go b/pkg/peer/logging.go new file mode 100644 index 0000000..88acf0d --- /dev/null +++ b/pkg/peer/logging.go @@ -0,0 +1,181 @@ +package peer + +import ( + "fmt" + "strings" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // maxRejectReasonLen is the maximum length of a sanitized reject reason that will be logged. + maxRejectReasonLen = 250 +) + +// formatLockTime returns a transaction lock time as a human-readable string. +func formatLockTime(lockTime uint32) string { + // The lock time field of a transaction is either a block height at which the transaction is finalized or a + // timestamp depending on if the value is before the lockTimeThreshold. When it is under the threshold it is a block + // height. + if lockTime < txscript.LockTimeThreshold { + return fmt.Sprintf("height %d", lockTime) + } + + return time.Unix(int64(lockTime), 0).String() +} + +// invSummary returns an inventory message as a human-readable string. +func invSummary(invList []*wire.InvVect) string { + // No inventory. + invLen := len(invList) + if invLen == 0 { + return "empty" + } + + // One inventory item. + if invLen == 1 { + iv := invList[0] + switch iv.Type { + case wire.InvTypeError: + return fmt.Sprintf("error %s", iv.Hash) + // case wire.InvTypeWitnessBlock: + // return fmt.Sprintf("witness block %s", iv.Hash) + case wire.InvTypeBlock: + return fmt.Sprintf("block %s", iv.Hash) + // case wire.InvTypeWitnessTx: + // return fmt.Sprintf("witness tx %s", iv.Hash) + case wire.InvTypeTx: + return fmt.Sprintf("tx %s", iv.Hash) + } + + return fmt.Sprintf("unknown (%d) %s", uint32(iv.Type), iv.Hash) + } + + // More than one inv item. + return fmt.Sprintf("size %d", invLen) +} + +// locatorSummary returns a block locator as a human-readable string. +func locatorSummary(locator []*chainhash.Hash, stopHash *chainhash.Hash) string { + if len(locator) > 0 { + return fmt.Sprintf("locator %s, stop %s", locator[0], stopHash) + } + + return fmt.Sprintf("no locator, stop %s", stopHash) + +} + +// sanitizeString strips any characters which are even remotely dangerous, such as html control characters, from the +// passed string. It also limits it to the passed maximum size, which can be 0 for unlimited. When the string is +// limited, it will also add "..." to the string to indicate it was truncated. +func sanitizeString(str string, maxLength uint) string { + const safeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXY" + + "Z01234567890 .,;_/:?@" + + // Strip any characters not in the safeChars string removed. + str = strings.Map(func(r rune) rune { + if strings.ContainsRune(safeChars, r) { + return r + } + return -1 + }, str, + ) + + // Limit the string to the max allowed length. + if maxLength > 0 && uint(len(str)) > maxLength { + str = str[:maxLength] + str = str + "..." + } + return str +} + +// messageSummary returns a human-readable string which summarizes a message. Not all messages have or need a summary. +// This is used for debug logging. +func messageSummary(msg wire.Message) string { + switch msg := msg.(type) { + case *wire.MsgVersion: + return fmt.Sprintf("agent %s, pver %d, block %d", + msg.UserAgent, msg.ProtocolVersion, msg.LastBlock, + ) + + case *wire.MsgVerAck: + // No summary. + + case *wire.MsgGetAddr: + // No summary. + + case *wire.MsgAddr: + return fmt.Sprintf("%d addr", len(msg.AddrList)) + + case *wire.MsgPing: + // No summary - perhaps add Nonce. + + case *wire.MsgPong: + // No summary - perhaps add Nonce. + + case *wire.MsgAlert: + // No summary. + + case *wire.MsgMemPool: + // No summary. + + case *wire.MsgTx: + return fmt.Sprintf("hash %s, %d inputs, %d outputs, lock %s", + msg.TxHash(), len(msg.TxIn), len(msg.TxOut), + formatLockTime(msg.LockTime), + ) + + case *wire.Block: + header := &msg.Header + return fmt.Sprintf("hash %s, ver %d, %d tx, %s", msg.BlockHash(), + header.Version, len(msg.Transactions), header.Timestamp, + ) + + case *wire.MsgInv: + return invSummary(msg.InvList) + + case *wire.MsgNotFound: + return invSummary(msg.InvList) + + case *wire.MsgGetData: + return invSummary(msg.InvList) + + case *wire.MsgGetBlocks: + return locatorSummary(msg.BlockLocatorHashes, &msg.HashStop) + + case *wire.MsgGetHeaders: + return locatorSummary(msg.BlockLocatorHashes, &msg.HashStop) + + case *wire.MsgHeaders: + return fmt.Sprintf("num %d", len(msg.Headers)) + + case *wire.MsgGetCFHeaders: + return fmt.Sprintf("start_height=%d, stop_hash=%v", + msg.StartHeight, msg.StopHash, + ) + + case *wire.MsgCFHeaders: + return fmt.Sprintf("stop_hash=%v, num_filter_hashes=%d", + msg.StopHash, len(msg.FilterHashes), + ) + + case *wire.MsgReject: + // Ensure the variable length strings don't contain any characters which are even remotely dangerous such as + // HTML control characters, etc. Also limit them to sane length for logging. + rejCommand := sanitizeString(msg.Cmd, wire.CommandSize) + rejReason := sanitizeString(msg.Reason, maxRejectReasonLen) + summary := fmt.Sprintf("cmd %v, code %v, reason %v", rejCommand, + msg.Code, rejReason, + ) + if rejCommand == wire.CmdBlock || rejCommand == wire.CmdTx { + summary += fmt.Sprintf(", hash %v", msg.Hash) + } + return summary + } + + // No summary for other messages. + return "" +} diff --git a/pkg/peer/mruinvmap.go b/pkg/peer/mruinvmap.go new file mode 100644 index 0000000..7adea45 --- /dev/null +++ b/pkg/peer/mruinvmap.go @@ -0,0 +1,107 @@ +package peer + +import ( + "bytes" + "container/list" + "fmt" + "sync" + + "github.com/p9c/p9/pkg/wire" +) + +// mruInventoryMap provides a concurrency safe map that is limited to a maximum number of items with eviction for the +// oldest entry when the limit is exceeded. +type mruInventoryMap struct { + invMtx sync.Mutex + invMap map[wire.InvVect]*list.Element // nearly O(1) lookups + invList *list.List // O(1) insert, update, delete + limit uint +} + +// String returns the map as a human-readable string. +// +// This function is safe for concurrent access. +func (m *mruInventoryMap) String() string { + m.invMtx.Lock() + defer m.invMtx.Unlock() + lastEntryNum := len(m.invMap) - 1 + curEntry := 0 + buf := bytes.NewBufferString("[") + for iv := range m.invMap { + buf.WriteString(fmt.Sprintf("%v", iv)) + if curEntry < lastEntryNum { + buf.WriteString(", ") + } + curEntry++ + } + buf.WriteString("]") + return fmt.Sprintf("<%d>%s", m.limit, buf.String()) +} + +// Exists returns whether or not the passed inventory item is in the map. +// +// This function is safe for concurrent access. +func (m *mruInventoryMap) Exists(iv *wire.InvVect) bool { + m.invMtx.Lock() + _, exists := m.invMap[*iv] + m.invMtx.Unlock() + return exists +} + +// Add adds the passed inventory to the map and handles eviction of the oldest item if adding the new item would exceed +// the max limit. Adding an existing item makes it the most recently used item. +// +// This function is safe for concurrent access. +func (m *mruInventoryMap) Add(iv *wire.InvVect) { + m.invMtx.Lock() + defer m.invMtx.Unlock() + // When the limit is zero, nothing can be added to the map, so just return. + if m.limit == 0 { + return + } + // When the entry already exists move it to the front of the list thereby marking it most recently used. + if node, exists := m.invMap[*iv]; exists { + m.invList.MoveToFront(node) + return + } + // Evict the least recently used entry (back of the list) if the the new entry would exceed the size limit for the + // map. Also reuse the list node so a new one doesn't have to be allocated. + if uint(len(m.invMap))+1 > m.limit { + node := m.invList.Back() + lru := node.Value.(*wire.InvVect) + // Evict least recently used item. + delete(m.invMap, *lru) + // Reuse the list node of the item that was just evicted for the new item. + node.Value = iv + m.invList.MoveToFront(node) + m.invMap[*iv] = node + return + } + // The limit hasn't been reached yet, so just add the new item. + node := m.invList.PushFront(iv) + m.invMap[*iv] = node +} + +// Delete deletes the passed inventory item from the map (if it exists). +// +// This function is safe for concurrent access. +func (m *mruInventoryMap) Delete(iv *wire.InvVect) { + m.invMtx.Lock() + if node, exists := m.invMap[*iv]; exists { + m.invList.Remove(node) + delete(m.invMap, *iv) + } + m.invMtx.Unlock() +} + +// newMruInventoryMap returns a new inventory map that is limited to the number of entries specified by limit. When the +// number of entries exceeds the limit, the oldest (least recently used) entry will be removed to make room for the new +// entry. +func newMruInventoryMap(limit uint) *mruInventoryMap { + m := mruInventoryMap{ + invMap: make(map[wire.InvVect]*list.Element), + invList: list.New(), + limit: limit, + } + return &m +} diff --git a/pkg/peer/mruinvmap_test.go b/pkg/peer/mruinvmap_test.go new file mode 100644 index 0000000..4d7bd53 --- /dev/null +++ b/pkg/peer/mruinvmap_test.go @@ -0,0 +1,146 @@ +package peer + +import ( + "crypto/rand" + "fmt" + "testing" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// TestMruInventoryMap ensures the MruInventoryMap behaves as expected including limiting, eviction of least-recently +// used entries, specific entry removal, and existence tests. +func TestMruInventoryMap(t *testing.T) { + // Create a bunch of fake inventory vectors to use in testing the mru inventory code. + numInvVects := 10 + invVects := make([]*wire.InvVect, 0, numInvVects) + for i := 0; i < numInvVects; i++ { + hash := &chainhash.Hash{byte(i)} + iv := wire.NewInvVect(wire.InvTypeBlock, hash) + invVects = append(invVects, iv) + } + tests := []struct { + name string + limit int + }{ + {name: "limit 0", limit: 0}, + {name: "limit 1", limit: 1}, + {name: "limit 5", limit: 5}, + {name: "limit 7", limit: 7}, + {name: "limit one less than available", limit: numInvVects - 1}, + {name: "limit all available", limit: numInvVects}, + } +testLoop: + for i, test := range tests { + // Create a new mru inventory map limited by the specified test limit and add all of the test inventory vectors. + // This will cause eviction since there are more test inventory vectors than the limits. + mruInvMap := newMruInventoryMap(uint(test.limit)) + for j := 0; j < numInvVects; j++ { + mruInvMap.Add(invVects[j]) + } + // Ensure the limited number of most recent entries in the inventory vector list exist. + for j := numInvVects - test.limit; j < numInvVects; j++ { + if !mruInvMap.Exists(invVects[j]) { + t.Errorf("Exists #%d (%s) entry %s does not "+ + "exist", i, test.name, *invVects[j], + ) + continue testLoop + } + } + // Ensure the entries before the limited number of most recent entries in the inventory vector list do not + // exist. + for j := 0; j < numInvVects-test.limit; j++ { + if mruInvMap.Exists(invVects[j]) { + t.Errorf("Exists #%d (%s) entry %s exists", i, + test.name, *invVects[j], + ) + continue testLoop + } + } + // Readd the entry that should currently be the least-recently used entry so it becomes the most-recently used + // entry, then force an eviction by adding an entry that doesn't exist and ensure the evicted entry is the new + // least-recently used entry. This check needs at least 2 entries. + if test.limit > 1 { + origLruIndex := numInvVects - test.limit + mruInvMap.Add(invVects[origLruIndex]) + iv := wire.NewInvVect(wire.InvTypeBlock, + &chainhash.Hash{0x00, 0x01}, + ) + mruInvMap.Add(iv) + // Ensure the original lru entry still exists since it was updated and should've have become the mru entry. + if !mruInvMap.Exists(invVects[origLruIndex]) { + t.Errorf("MRU #%d (%s) entry %s does not exist", + i, test.name, *invVects[origLruIndex], + ) + continue testLoop + } + // Ensure the entry that should've become the new lru entry was evicted. + newLruIndex := origLruIndex + 1 + if mruInvMap.Exists(invVects[newLruIndex]) { + t.Errorf("MRU #%d (%s) entry %s exists", i, + test.name, *invVects[newLruIndex], + ) + continue testLoop + } + } + // Delete all of the entries in the inventory vector list, including those that don't exist in the map, and + // ensure they no longer exist. + for j := 0; j < numInvVects; j++ { + mruInvMap.Delete(invVects[j]) + if mruInvMap.Exists(invVects[j]) { + t.Errorf("Delete #%d (%s) entry %s exists", i, + test.name, *invVects[j], + ) + continue testLoop + } + } + } +} + +// TestMruInventoryMapStringer tests the stringified output for the MruInventoryMap type. +func TestMruInventoryMapStringer(t *testing.T) { + // Create a couple of fake inventory vectors to use in testing the mru inventory stringer code. + hash1 := &chainhash.Hash{0x01} + hash2 := &chainhash.Hash{0x02} + iv1 := wire.NewInvVect(wire.InvTypeBlock, hash1) + iv2 := wire.NewInvVect(wire.InvTypeBlock, hash2) + // Create new mru inventory map and add the inventory vectors. + mruInvMap := newMruInventoryMap(uint(2)) + mruInvMap.Add(iv1) + mruInvMap.Add(iv2) + // Ensure the stringer gives the expected result. Since map iteration is not ordered, either entry could be first, + // so account for both cases. + wantStr1 := fmt.Sprintf("<%d>[%s, %s]", 2, *iv1, *iv2) + wantStr2 := fmt.Sprintf("<%d>[%s, %s]", 2, *iv2, *iv1) + gotStr := mruInvMap.String() + if gotStr != wantStr1 && gotStr != wantStr2 { + t.Fatalf("unexpected string representation - got %q, want %q "+ + "or %q", gotStr, wantStr1, wantStr2, + ) + } +} + +// BenchmarkMruInventoryList performs basic benchmarks on the most recently used inventory handling. +func BenchmarkMruInventoryList(b *testing.B) { + // Create a bunch of fake inventory vectors to use in benchmarking the mru inventory code. + b.StopTimer() + numInvVects := 100000 + invVects := make([]*wire.InvVect, 0, numInvVects) + for i := 0; i < numInvVects; i++ { + hashBytes := make([]byte, chainhash.HashSize) + _, e := rand.Read(hashBytes) + if e != nil { + } + hash, _ := chainhash.NewHash(hashBytes) + iv := wire.NewInvVect(wire.InvTypeBlock, hash) + invVects = append(invVects, iv) + } + b.StartTimer() + // Benchmark the add plus evicition code. + limit := 20000 + mruInvMap := newMruInventoryMap(uint(limit)) + for i := 0; i < b.N; i++ { + mruInvMap.Add(invVects[i%numInvVects]) + } +} diff --git a/pkg/peer/mrunoncemap.go b/pkg/peer/mrunoncemap.go new file mode 100644 index 0000000..bd89ec7 --- /dev/null +++ b/pkg/peer/mrunoncemap.go @@ -0,0 +1,102 @@ +package peer + +import ( + "bytes" + "container/list" + "fmt" + "sync" +) + +// mruNonceMap provides a concurrency safe map that is limited to a maximum number of items with eviction for the oldest +// entry when the limit is exceeded. +type mruNonceMap struct { + mtx sync.Mutex + nonceMap map[uint64]*list.Element // nearly O(1) lookups + nonceList *list.List // O(1) insert, update, delete + limit uint +} + +// String returns the map as a human-readable string. This function is safe for concurrent access. +func (m *mruNonceMap) String() string { + m.mtx.Lock() + defer m.mtx.Unlock() + lastEntryNum := len(m.nonceMap) - 1 + curEntry := 0 + buf := bytes.NewBufferString("[") + for nonce := range m.nonceMap { + buf.WriteString(fmt.Sprintf("%d", nonce)) + if curEntry < lastEntryNum { + buf.WriteString(", ") + } + curEntry++ + } + buf.WriteString("]") + return fmt.Sprintf("<%d>%s", m.limit, buf.String()) +} + +// Exists returns whether or not the passed Nonce is in the map. +// +// This function is safe for concurrent access. +func (m *mruNonceMap) Exists(nonce uint64) bool { + m.mtx.Lock() + _, exists := m.nonceMap[nonce] + m.mtx.Unlock() + return exists +} + +// Add adds the passed Nonce to the map and handles eviction of the oldest item if adding the new item would exceed the +// max limit. Adding an existing item makes it the most recently used item. +// +// This function is safe for concurrent access. +func (m *mruNonceMap) Add(nonce uint64) { + m.mtx.Lock() + defer m.mtx.Unlock() + // When the limit is zero, nothing can be added to the map, so just return. + if m.limit == 0 { + return + } + // When the entry already exists move it to the front of the list thereby marking it most recently used. + if node, exists := m.nonceMap[nonce]; exists { + m.nonceList.MoveToFront(node) + return + } + // Evict the least recently used entry (back of the list) if the the new entry would exceed the size limit for the + // map. Also reuse the list node so a new one doesn't have to be allocated. + if uint(len(m.nonceMap))+1 > m.limit { + node := m.nonceList.Back() + lru := node.Value.(uint64) + // Evict least recently used item. + delete(m.nonceMap, lru) + // Reuse the list node of the item that was just evicted for the new item. + node.Value = nonce + m.nonceList.MoveToFront(node) + m.nonceMap[nonce] = node + return + } + // The limit hasn't been reached yet, so just add the new item. + node := m.nonceList.PushFront(nonce) + m.nonceMap[nonce] = node +} + +// Delete deletes the passed Nonce from the map (if it exists). +// +// This function is safe for concurrent access. +func (m *mruNonceMap) Delete(nonce uint64) { + m.mtx.Lock() + if node, exists := m.nonceMap[nonce]; exists { + m.nonceList.Remove(node) + delete(m.nonceMap, nonce) + } + m.mtx.Unlock() +} + +// newMruNonceMap returns a new Nonce map that is limited to the number of entries specified by limit. When the number +// of entries exceeds the limit, the oldest (least recently used) entry will be removed to make room for the new entry. +func newMruNonceMap(limit uint) *mruNonceMap { + m := mruNonceMap{ + nonceMap: make(map[uint64]*list.Element), + nonceList: list.New(), + limit: limit, + } + return &m +} diff --git a/pkg/peer/mrunoncemap_test.go b/pkg/peer/mrunoncemap_test.go new file mode 100644 index 0000000..fd074ab --- /dev/null +++ b/pkg/peer/mrunoncemap_test.go @@ -0,0 +1,128 @@ +package peer + +import ( + "fmt" + "testing" +) + +// TestMruNonceMap ensures the mruNonceMap behaves as expected including limiting, eviction of least-recently used +// entries, specific entry removal, and existence tests. +func TestMruNonceMap(t *testing.T) { + // Create a bunch of fake nonces to use in testing the mru Nonce code. + numNonces := 10 + nonces := make([]uint64, 0, numNonces) + for i := 0; i < numNonces; i++ { + nonces = append(nonces, uint64(i)) + } + tests := []struct { + name string + limit int + }{ + {name: "limit 0", limit: 0}, + {name: "limit 1", limit: 1}, + {name: "limit 5", limit: 5}, + {name: "limit 7", limit: 7}, + {name: "limit one less than available", limit: numNonces - 1}, + {name: "limit all available", limit: numNonces}, + } +testLoop: + for i, test := range tests { + // Create a new mru Nonce map limited by the specified test limit and add all of the test nonces. This will + // cause evicition since there are more test nonces than the limits. + mruNonceMap := newMruNonceMap(uint(test.limit)) + for j := 0; j < numNonces; j++ { + mruNonceMap.Add(nonces[j]) + } + // Ensure the limited number of most recent entries in the list exist. + for j := numNonces - test.limit; j < numNonces; j++ { + if !mruNonceMap.Exists(nonces[j]) { + t.Errorf("Exists #%d (%s) entry %d does not "+ + "exist", i, test.name, nonces[j], + ) + continue testLoop + } + } + // Ensure the entries before the limited number of most recent entries in the list do not exist. + for j := 0; j < numNonces-test.limit; j++ { + if mruNonceMap.Exists(nonces[j]) { + t.Errorf("Exists #%d (%s) entry %d exists", i, + test.name, nonces[j], + ) + continue testLoop + } + } + // Readd the entry that should currently be the least-recently used entry so it becomes the most-recently used + // entry, then force an eviction by adding an entry that doesn't exist and ensure the evicted entry is the new + // least-recently used entry. This check needs at least 2 entries. + if test.limit > 1 { + origLruIndex := numNonces - test.limit + mruNonceMap.Add(nonces[origLruIndex]) + mruNonceMap.Add(uint64(numNonces) + 1) + // Ensure the original lru entry still exists since it was updated and should've have become the mru entry. + if !mruNonceMap.Exists(nonces[origLruIndex]) { + t.Errorf("MRU #%d (%s) entry %d does not exist", + i, test.name, nonces[origLruIndex], + ) + continue testLoop + } + // Ensure the entry that should've become the new lru entry was evicted. + newLruIndex := origLruIndex + 1 + if mruNonceMap.Exists(nonces[newLruIndex]) { + t.Errorf("MRU #%d (%s) entry %d exists", i, + test.name, nonces[newLruIndex], + ) + continue testLoop + } + } + // Delete all of the entries in the list, including those that don't exist in the map, and ensure they no longer + // exist. + for j := 0; j < numNonces; j++ { + mruNonceMap.Delete(nonces[j]) + if mruNonceMap.Exists(nonces[j]) { + t.Errorf("Delete #%d (%s) entry %d exists", i, + test.name, nonces[j], + ) + continue testLoop + } + } + } +} + +// TestMruNonceMapStringer tests the stringized output for the mruNonceMap type. +func TestMruNonceMapStringer(t *testing.T) { + // Create a couple of fake nonces to use in testing the mru Nonce stringer code. + nonce1 := uint64(10) + nonce2 := uint64(20) + // Create new mru Nonce map and add the nonces. + mruNonceMap := newMruNonceMap(uint(2)) + mruNonceMap.Add(nonce1) + mruNonceMap.Add(nonce2) + // Ensure the stringer gives the expected result. Since map iteration is not ordered, either entry could be first, + // so account for both cases. + wantStr1 := fmt.Sprintf("<%d>[%d, %d]", 2, nonce1, nonce2) + wantStr2 := fmt.Sprintf("<%d>[%d, %d]", 2, nonce2, nonce1) + gotStr := mruNonceMap.String() + if gotStr != wantStr1 && gotStr != wantStr2 { + t.Fatalf("unexpected string representation - got %q, want %q "+ + "or %q", gotStr, wantStr1, wantStr2, + ) + } +} + +// BenchmarkMruNonceList performs basic benchmarks on the most recently used Nonce handling. +func BenchmarkMruNonceList(b *testing.B) { + // Create a bunch of fake nonces to use in benchmarking the mru Nonce code. + b.StopTimer() + numNonces := 100000 + nonces := make([]uint64, 0, numNonces) + for i := 0; i < numNonces; i++ { + nonces = append(nonces, uint64(i)) + } + b.StartTimer() + // Benchmark the add plus evicition code. + limit := 20000 + mruNonceMap := newMruNonceMap(uint(limit)) + for i := 0; i < b.N; i++ { + mruNonceMap.Add(nonces[i%numNonces]) + } +} diff --git a/pkg/peer/peer.go b/pkg/peer/peer.go new file mode 100644 index 0000000..7cca1c2 --- /dev/null +++ b/pkg/peer/peer.go @@ -0,0 +1,2023 @@ +package peer + +import ( + "container/list" + "errors" + "fmt" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/log" + "io" + "math/rand" + "net" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/btcsuite/go-socks/socks" + + "github.com/p9c/p9/pkg/blockchain" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +const ( + // MaxProtocolVersion is the max protocol version the peer supports. + MaxProtocolVersion = wire.FeeFilterVersion + // DefaultTrickleInterval is the min time between attempts to send an inv message to a peer. + DefaultTrickleInterval = time.Second + // MinAcceptableProtocolVersion is the lowest protocol version that a connected peer may support. + MinAcceptableProtocolVersion = 1 + // outputBufferSize is the number of elements the output channels use. + outputBufferSize = 1000 + // invTrickleSize is the maximum amount of inventory to send in a single message when trickling inventory to remote + // peers. + maxInvTrickleSize = 5000 + // maxKnownInventory is the maximum number of items to keep in the known inventory cache. + maxKnownInventory = 30000 + // pingInterval is the interval of time to wait in between sending ping messages. + pingInterval = 1 * time.Second + // negotiateTimeout is the duration of inactivity before we timeout a peer that hasn't completed the initial version + // negotiation. + negotiateTimeout = 27 * time.Second + // idleTimeout is the duration of inactivity before we time out a peer. + idleTimeout = time.Minute + // stallTickInterval is the interval of time between each check for stalled peers. + stallTickInterval = 60 * time.Second + // stallResponseTimeout is the base maximum amount of time messages that expect a response will wait before + // disconnecting the peer for stalling. The deadlines are adjusted for callback running times and checked on each + // stall tick interval. + stallResponseTimeout = 360 * time.Second +) + +var ( + // nodeCount is the total number of peer connections made since startup and is used to assign an id to a peer. + nodeCount int32 + // zeroHash is the zero value hash (all zeros). It is defined as a convenience. + zeroHash chainhash.Hash + // SentNonces houses the unique nonces that are generated when pushing version messages that are used to detect self + // connections. + SentNonces = newMruNonceMap(50) + // AllowSelfConns is only used to allow the tests to bypass the self connection detecting and disconnect logic since + // they intentionally do so for testing purposes. + AllowSelfConns bool +) + +// MessageListeners defines callback function pointers to invoke with message listeners for a peer. Any listener which +// is not set to a concrete callback during peer initialization is ignored. +// +// Execution of multiple message listeners occurs serially, so one callback blocks the execution of the next. +// +// NOTE: Unless otherwise documented, these listeners must NOT directly call any blocking calls ( such as +// WaitForShutdown) on the peer instance since the input handler goroutine blocks until the callback has completed. +// Doing so will result in a deadlock. +type MessageListeners struct { + // OnGetAddr is invoked when a peer receives a getaddr bitcoin message. + OnGetAddr func(p *Peer, msg *wire.MsgGetAddr) + // OnAddr is invoked when a peer receives an addr bitcoin message. + OnAddr func(p *Peer, msg *wire.MsgAddr) + // OnPing is invoked when a peer receives a ping bitcoin message. + OnPing func(p *Peer, msg *wire.MsgPing) + // OnPong is invoked when a peer receives a pong bitcoin message. + OnPong func(p *Peer, msg *wire.MsgPong) + // OnAlert is invoked when a peer receives an alert bitcoin message. + OnAlert func(p *Peer, msg *wire.MsgAlert) + // OnMemPool is invoked when a peer receives a mempool bitcoin message. + OnMemPool func(p *Peer, msg *wire.MsgMemPool) + // OnTx is invoked when a peer receives a tx bitcoin message. + OnTx func(p *Peer, msg *wire.MsgTx) + // OnBlock is invoked when a peer receives a block bitcoin message. + OnBlock func(p *Peer, msg *wire.Block, buf []byte) + // OnCFilter is invoked when a peer receives a cfilter bitcoin message. + OnCFilter func(p *Peer, msg *wire.MsgCFilter) + // OnCFHeaders is invoked when a peer receives a cfheaders bitcoin message. + OnCFHeaders func(p *Peer, msg *wire.MsgCFHeaders) + // OnCFCheckpt is invoked when a peer receives a cfcheckpt bitcoin message. + OnCFCheckpt func(p *Peer, msg *wire.MsgCFCheckpt) + // OnInv is invoked when a peer receives an inv bitcoin message. + OnInv func(p *Peer, msg *wire.MsgInv) + // OnHeaders is invoked when a peer receives a headers bitcoin message. + OnHeaders func(p *Peer, msg *wire.MsgHeaders) + // OnNotFound is invoked when a peer receives a notfound bitcoin message. + OnNotFound func(p *Peer, msg *wire.MsgNotFound) + // OnGetData is invoked when a peer receives a getdata bitcoin message. + OnGetData func(p *Peer, msg *wire.MsgGetData) + // OnGetBlocks is invoked when a peer receives a getblocks bitcoin message. + OnGetBlocks func(p *Peer, msg *wire.MsgGetBlocks) + // OnGetHeaders is invoked when a peer receives a getheaders bitcoin + // message. + OnGetHeaders func(p *Peer, msg *wire.MsgGetHeaders) + // OnGetCFilters is invoked when a peer receives a getcfilters bitcoin + // message. + OnGetCFilters func(p *Peer, msg *wire.MsgGetCFilters) + // OnGetCFHeaders is invoked when a peer receives a getcfheaders bitcoin + // message. + OnGetCFHeaders func(p *Peer, msg *wire.MsgGetCFHeaders) + // OnGetCFCheckpt is invoked when a peer receives a getcfcheckpt bitcoin + // message. + OnGetCFCheckpt func(p *Peer, msg *wire.MsgGetCFCheckpt) + // OnFeeFilter is invoked when a peer receives a feefilter bitcoin message. + OnFeeFilter func(p *Peer, msg *wire.MsgFeeFilter) + // OnFilterAdd is invoked when a peer receives a filteradd bitcoin message. + OnFilterAdd func(p *Peer, msg *wire.MsgFilterAdd) + // OnFilterClear is invoked when a peer receives a filterclear bitcoin + // message. + OnFilterClear func(p *Peer, msg *wire.MsgFilterClear) + // OnFilterLoad is invoked when a peer receives a filterload bitcoin + // message. + OnFilterLoad func(p *Peer, msg *wire.MsgFilterLoad) + // OnMerkleBlock is invoked when a peer receives a merkleblock bitcoin + // message. + OnMerkleBlock func(p *Peer, msg *wire.MsgMerkleBlock) + // OnVersion is invoked when a peer receives a version bitcoin message. + // The caller may return a reject message in which case the message will + // be sent to the peer and the peer will be disconnected. + OnVersion func(p *Peer, msg *wire.MsgVersion) *wire.MsgReject + // OnVerAck is invoked when a peer receives a verack bitcoin message. + OnVerAck func(p *Peer, msg *wire.MsgVerAck) + // OnReject is invoked when a peer receives a reject bitcoin message. + OnReject func(p *Peer, msg *wire.MsgReject) + // OnSendHeaders is invoked when a peer receives a sendheaders bitcoin + // message. + OnSendHeaders func(p *Peer, msg *wire.MsgSendHeaders) + // OnRead is invoked when a peer receives a bitcoin message. + // + // It consists of the number of bytes read, the message, and whether or not an error in the read occurred. + // Typically, callers will opt to use the callbacks for the specific message types, however this can be useful for + // circumstances such as keeping track of server-wide byte counts or working with custom message types for which the + // peer does not directly provide a callback. + OnRead func(p *Peer, bytesRead int, msg wire.Message, e error) + // OnWrite is invoked when we write a bitcoin message to a peer. + // + // It consists of the number of bytes written, the message, and whether or not an error in the write occurred. This + // can be useful for circumstances such as keeping track of server -wide byte counts. + OnWrite func(p *Peer, bytesWritten int, msg wire.Message, e error) +} + +// Config is the struct to hold configuration options useful to Peer. +type Config struct { + // NewestBlock specifies a callback which provides the newest block details to the peer as needed. + // + // This can be nil in which case the peer will report a block height of 0, however it is good practice for peers to + // specify this so their currently best known is accurately reported. + NewestBlock HashFunc + // HostToNetAddress returns the netaddress for the given host. This can be nil in which case the host will be parsed + // as an IP address. + HostToNetAddress HostToNetAddrFunc + // Proxy indicates a proxy is being used for connections. The only effect this has is to prevent leaking the tor + // proxy address, so it only needs to specified if using a tor proxy. + Proxy string + // UserAgentName specifies the user agent name to advertise. It is highly recommended to specify this value. + UserAgentName string + // UserAgentVersion specifies the user agent version to advertise. It is highly recommended to specify this value + // and that it follows the form "major.minor.revision" e.g. "2.6.41". + UserAgentVersion string + // UserAgentComments specify the user agent comments to advertise. These values must not contain the illegal + // characters specified in BIP 14: '/', ':', '(', ')'. + UserAgentComments []string + // ChainParams identifies which chain parameters the peer is associated with. It is highly recommended to specify + // this field, however it can be omitted in which case the test network will be used. + ChainParams *chaincfg.Params + // Services specifies which services to advertise as supported by the local peer. This field can be omitted in which + // case it will be 0 and therefore advertise no supported services. + Services wire.ServiceFlag + // ProtocolVersion specifies the maximum protocol version to use and advertise. This field can be omitted in which + // case peer. MaxProtocolVersion will be used. + ProtocolVersion uint32 + // DisableRelayTx specifies if the remote peer should be informed to not send inv messages for transactions. + DisableRelayTx bool + // Listeners houses callback functions to be invoked on receiving peer + // messages. + Listeners MessageListeners + // TrickleInterval is the duration of the ticker which trickles down the inventory to a peer. + TrickleInterval time.Duration + IP net.IP + Port uint16 +} + +// minUint32 is a helper function to return the minimum of two uint32s. This avoids a math import and the need to cast +// to floats. +func minUint32(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +// newNetAddress attempts to extract the IP address and port from the passed net.Addr interface and create a bitcoin +// NetAddress structure using that information. +func newNetAddress(addr net.Addr, services wire.ServiceFlag) (*wire.NetAddress, error) { + // addr will be a net.TCPAddr when not using a proxy. + if tcpAddr, ok := addr.(*net.TCPAddr); ok { + ip := tcpAddr.IP + port := uint16(tcpAddr.Port) + na := wire.NewNetAddressIPPort(ip, port, services) + return na, nil + } + // addr will be a socks.ProxiedAddr when using a proxy. + if proxiedAddr, ok := addr.(*socks.ProxiedAddr); ok { + ip := net.ParseIP(proxiedAddr.Host) + if ip == nil { + ip = net.ParseIP("0.0.0.0") + } + port := uint16(proxiedAddr.Port) + na := wire.NewNetAddressIPPort(ip, port, services) + return na, nil + } + // For the most part, addr should be one of the two above cases, but to be safe, fall back to trying to parse the + // information from the address string as a last resort. + host, portStr, e := net.SplitHostPort(addr.String()) + if e != nil { + return nil, e + } + ip := net.ParseIP(host) + port, e := strconv.ParseUint(portStr, 10, 16) + if e != nil { + return nil, e + } + na := wire.NewNetAddressIPPort(ip, uint16(port), services) + return na, nil +} + +// outMsg is used to house a message to be sent along with a channel to signal when the message has been sent (or won't +// be sent due to things such as shutdown) +type outMsg struct { + msg wire.Message + doneChan chan<- struct{} + encoding wire.MessageEncoding +} + +// stallControlCmd represents the command of a stall control message. +type stallControlCmd uint8 + +// Constants for the command of a stall control message. +const ( + // sccSendMessage indicates a message is being sent to the remote peer. + sccSendMessage stallControlCmd = iota + // sccReceiveMessage indicates a message has been received from the + // remote peer. + sccReceiveMessage + // sccHandlerStart indicates a callback handler is about to be invoked. + sccHandlerStart + // sccHandlerStart indicates a callback handler has completed. + sccHandlerDone +) + +// stallControlMsg is used to signal the stall handler about specific events so it can properly detect and handle +// stalled remote peers. +type stallControlMsg struct { + command stallControlCmd + message wire.Message +} + +// StatsSnap is a snapshot of peer stats at a point in time. +type StatsSnap struct { + ID int32 + Addr string + Services wire.ServiceFlag + LastSend time.Time + LastRecv time.Time + BytesSent uint64 + BytesRecv uint64 + ConnTime time.Time + TimeOffset int64 + Version uint32 + UserAgent string + Inbound bool + StartingHeight int32 + LastBlock int32 + LastPingNonce uint64 + LastPingTime time.Time + LastPingMicros int64 +} + +// HashFunc is a function which returns a block hash, height and error It is used as a callback to get newest block +// details. +type HashFunc func() (hash *chainhash.Hash, height int32, e error) + +// AddrFunc is a func which takes an address and returns a related address. +type AddrFunc func(remoteAddr *wire.NetAddress) *wire.NetAddress + +// HostToNetAddrFunc is a func which takes a host, port, services and returns the netaddress. +type HostToNetAddrFunc func( + host string, port uint16, + services wire.ServiceFlag, +) (*wire.NetAddress, error) + +// NOTE: The overall data flow of a peer is split into 3 goroutines. +// +// Inbound messages are read via the inHandler goroutine and generally dispatched to their own handler. +// +// For inbound data-related messages such as blocks, transactions, and inventory, the data is handled by the +// corresponding message handlers. +// +// The data flow for outbound messages is split into 2 goroutines, queueHandler and outHandler. The first, queueHandler, +// is used as a way for external entities to queue messages, by way of the QueueMessage function, quickly regardless of +// whether the peer is currently sending or not. It acts as the traffic cop between the external world and the actual +// goroutine which writes to the network socket. + +// Peer provides a basic concurrent safe bitcoin peer for handling bitcoin communications via the peer-to-peer protocol. +// +// It provides full duplex reading and writing, automatic handling of the initial handshake process, querying of usage +// statistics and other information about the remote peer such as its address, user agent, and protocol version, output +// message queuing, inventory trickling, and the ability to dynamically register and unregister callbacks for handling +// bitcoin protocol messages. +// +// Outbound messages are typically queued via QueueMessage or QueueInventory. +// +// QueueMessage is intended for all messages, including responses to data such as blocks and transactions. +// +// QueueInventory, on the other hand, is only intended for relaying inventory as it employs a trickling mechanism to +// batch the inventory together. However, some helper functions for pushing messages of specific types that typically +// require common special handling are provided as a convenience. +type Peer struct { + // The following variables must only be used atomically. + bytesReceived uint64 + bytesSent uint64 + lastRecv int64 + lastSend int64 + connected int32 + disconnect int32 + conn net.Conn + // These fields are set at creation time and never modified, so they are safe to read from concurrently without a + // mutex. + Nonce uint64 + addr string + cfg Config + inbound bool + flagsMtx sync.Mutex // protects the peer flags below + na *wire.NetAddress + id int32 + userAgent string + services wire.ServiceFlag + versionKnown bool + advertisedProtoVer uint32 // protocol version advertised by remote + protocolVersion uint32 // negotiated protocol version + sendHeadersPreferred bool // peer sent a sendheaders message + verAckReceived bool + witnessEnabled bool + wireEncoding wire.MessageEncoding + knownInventory *mruInventoryMap + prevGetBlocksMtx sync.Mutex + prevGetBlocksBegin *chainhash.Hash + prevGetBlocksStop *chainhash.Hash + prevGetHdrsMtx sync.Mutex + prevGetHdrsBegin *chainhash.Hash + prevGetHdrsStop *chainhash.Hash + // These fields keep track of statistics for the peer and are protected by the statsMtx mutex. + statsMtx sync.RWMutex + timeOffset int64 + timeConnected time.Time + startingHeight int32 + lastBlock int32 + lastAnnouncedBlock *chainhash.Hash + lastPingNonce uint64 // Set to Nonce if we have a pending ping. + lastPingTime time.Time // Time we sent last ping. + lastPingMicros int64 // Time for last ping to return. + stallControl chan stallControlMsg + outputQueue chan outMsg + sendQueue chan outMsg + sendDoneQueue qu.C + outputInvChan chan *wire.InvVect + inQuit qu.C + queueQuit qu.C + outQuit qu.C + quit qu.C + IP net.IP + Port uint16 +} + +// String returns the peer's address and directionality as a human-readable string. +// +// This function is safe for concurrent access. +func (p *Peer) String() string { + return fmt.Sprintf("%s (%s)", p.addr, log.DirectionString(p.inbound)) +} + +// UpdateLastBlockHeight updates the last known block for the peer. +// +// This function is safe for concurrent access. +func (p *Peer) UpdateLastBlockHeight(newHeight int32) { + p.statsMtx.Lock() + T.F( + "updating last block height of peer %v from %v to %v", + p.addr, + p.lastBlock, + newHeight, + ) + p.lastBlock = newHeight + p.statsMtx.Unlock() +} + +// UpdateLastAnnouncedBlock updates meta-data about the last block hash this +// peer is known to have announced. +// +// This function is safe for concurrent access. +func (p *Peer) UpdateLastAnnouncedBlock(blkHash *chainhash.Hash) { + T.Ln("updating last blk for peer", p.addr, ",", blkHash) + p.statsMtx.Lock() + p.lastAnnouncedBlock = blkHash + p.statsMtx.Unlock() +} + +// AddKnownInventory adds the passed inventory to the cache of known inventory for the peer. +// +// This function is safe for concurrent access. +func (p *Peer) AddKnownInventory(invVect *wire.InvVect) { + p.knownInventory.Add(invVect) +} + +// StatsSnapshot returns a snapshot of the current peer flags and statistics. +// +// This function is safe for concurrent access. +func (p *Peer) StatsSnapshot() *StatsSnap { + p.statsMtx.RLock() + p.flagsMtx.Lock() + id := p.id + addr := p.addr + userAgent := p.userAgent + services := p.services + protocolVersion := p.advertisedProtoVer + p.flagsMtx.Unlock() + // Get a copy of all relevant flags and stats. + statsSnap := &StatsSnap{ + ID: id, + Addr: addr, + UserAgent: userAgent, + Services: services, + LastSend: p.LastSend(), + LastRecv: p.LastRecv(), + BytesSent: p.BytesSent(), + BytesRecv: p.BytesReceived(), + ConnTime: p.timeConnected, + TimeOffset: p.timeOffset, + Version: protocolVersion, + Inbound: p.inbound, + StartingHeight: p.startingHeight, + LastBlock: p.lastBlock, + LastPingNonce: p.lastPingNonce, + LastPingMicros: p.lastPingMicros, + LastPingTime: p.lastPingTime, + } + p.statsMtx.RUnlock() + return statsSnap +} + +// ID returns the peer id. +// +// This function is safe for concurrent access. +func (p *Peer) ID() int32 { + p.flagsMtx.Lock() + id := p.id + p.flagsMtx.Unlock() + return id +} + +// NA returns the peer network address. +// +// This function is safe for concurrent access. +func (p *Peer) NA() *wire.NetAddress { + p.flagsMtx.Lock() + na := p.na + p.flagsMtx.Unlock() + return na +} + +// Addr returns the peer address. +// +// This function is safe for concurrent access. +func (p *Peer) Addr() string { + // The address doesn't change after initialization, therefore it is not protected by a mutex. + return p.addr +} + +// Inbound returns whether the peer is inbound. This function is safe for concurrent access. +func (p *Peer) Inbound() bool { + return p.inbound +} + +// Services returns the services flag of the remote peer. This function is safe for concurrent access. +func (p *Peer) Services() wire.ServiceFlag { + p.flagsMtx.Lock() + services := p.services + p.flagsMtx.Unlock() + return services +} + +// UserAgent returns the user agent of the remote peer. +// +// This function is safe for concurrent access. +func (p *Peer) UserAgent() string { + p.flagsMtx.Lock() + userAgent := p.userAgent + p.flagsMtx.Unlock() + return userAgent +} + +// LastAnnouncedBlock returns the last announced block of the remote peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastAnnouncedBlock() *chainhash.Hash { + p.statsMtx.RLock() + lastAnnouncedBlock := p.lastAnnouncedBlock + p.statsMtx.RUnlock() + return lastAnnouncedBlock +} + +// LastPingNonce returns the last ping Nonce of the remote peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastPingNonce() uint64 { + p.statsMtx.RLock() + lastPingNonce := p.lastPingNonce + p.statsMtx.RUnlock() + return lastPingNonce +} + +// LastPingTime returns the last ping time of the remote peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastPingTime() time.Time { + p.statsMtx.RLock() + lastPingTime := p.lastPingTime + p.statsMtx.RUnlock() + return lastPingTime +} + +// LastPingMicros returns the last ping micros of the remote peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastPingMicros() int64 { + p.statsMtx.RLock() + lastPingMicros := p.lastPingMicros + p.statsMtx.RUnlock() + return lastPingMicros +} + +// VersionKnown returns the whether or not the version of a peer is known locally. +// +// This function is safe for concurrent access. +func (p *Peer) VersionKnown() bool { + p.flagsMtx.Lock() + versionKnown := p.versionKnown + p.flagsMtx.Unlock() + return versionKnown +} + +// VerAckReceived returns whether or not a verack message was received by the peer. +// +// This function is safe for concurrent access. +func (p *Peer) VerAckReceived() bool { + p.flagsMtx.Lock() + verAckReceived := p.verAckReceived + p.flagsMtx.Unlock() + return verAckReceived +} + +// ProtocolVersion returns the negotiated peer protocol version. +// +// This function is safe for concurrent access. +func (p *Peer) ProtocolVersion() uint32 { + p.flagsMtx.Lock() + protocolVersion := p.protocolVersion + p.flagsMtx.Unlock() + return protocolVersion +} + +// LastBlock returns the last block of the peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastBlock() int32 { + p.statsMtx.RLock() + lastBlock := p.lastBlock + p.statsMtx.RUnlock() + return lastBlock +} + +// LastSend returns the last send time of the peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastSend() time.Time { + return time.Unix(atomic.LoadInt64(&p.lastSend), 0) +} + +// LastRecv returns the last recv time of the peer. +// +// This function is safe for concurrent access. +func (p *Peer) LastRecv() time.Time { + return time.Unix(atomic.LoadInt64(&p.lastRecv), 0) +} + +// LocalAddr returns the local address of the connection. +// +// This function is safe fo concurrent access. +func (p *Peer) LocalAddr() net.Addr { + var localAddr net.Addr + if atomic.LoadInt32(&p.connected) != 0 { + localAddr = p.conn.LocalAddr() + } + return localAddr +} + +// BytesSent returns the total number of bytes sent by the peer. +// +// This function is safe for concurrent access. +func (p *Peer) BytesSent() uint64 { + return atomic.LoadUint64(&p.bytesSent) +} + +// BytesReceived returns the total number of bytes received by the peer. +// +// This function is safe for concurrent access. +func (p *Peer) BytesReceived() uint64 { + return atomic.LoadUint64(&p.bytesReceived) +} + +// TimeConnected returns the time at which the peer connected. +// +// This function is safe for concurrent access. +func (p *Peer) TimeConnected() time.Time { + p.statsMtx.RLock() + timeConnected := p.timeConnected + p.statsMtx.RUnlock() + return timeConnected +} + +// TimeOffset returns the number of seconds the local time was offset from the time the peer reported during the initial +// negotiation phase. +// +// Negative values indicate the remote peer's time is before the local time. +// +// This function is safe for concurrent access. +func (p *Peer) TimeOffset() int64 { + p.statsMtx.RLock() + timeOffset := p.timeOffset + p.statsMtx.RUnlock() + return timeOffset +} + +// StartingHeight returns the last known height the peer reported during the initial negotiation phase. This function is +// safe for concurrent access. +func (p *Peer) StartingHeight() int32 { + p.statsMtx.RLock() + startingHeight := p.startingHeight + p.statsMtx.RUnlock() + return startingHeight +} + +// WantsHeaders returns if the peer wants header messages instead of inventory vectors for blocks. This function is safe +// for concurrent access. +func (p *Peer) WantsHeaders() bool { + p.flagsMtx.Lock() + sendHeadersPreferred := p.sendHeadersPreferred + p.flagsMtx.Unlock() + return sendHeadersPreferred +} + +// // IsWitnessEnabled returns true if the peer has signalled that it supports +// // segregated witness. This function is safe for concurrent access. +// func (p *Peer) IsWitnessEnabled() bool { +// p.flagsMtx.Lock() +// witnessEnabled := p.witnessEnabled +// p.flagsMtx.Unlock() +// return witnessEnabled +// } + +// PushAddrMsg sends an addr message to the connected peer using the provided +// addresses. +// +// This function is useful over manually sending the message via QueueMessage since it automatically limits the +// addresses to the maximum number allowed by the message and randomizes the chosen addresses when there are too many. +// +// It returns the addresses that were actually sent and no message will be sent if there are no entries in the provided +// addresses slice. This function is safe for concurrent access. +func (p *Peer) PushAddrMsg(addresses []*wire.NetAddress) ([]*wire.NetAddress, error) { + addressCount := len(addresses) + // Nothing to send. + if addressCount == 0 { + return nil, nil + } + msg := wire.NewMsgAddr() + msg.AddrList = make([]*wire.NetAddress, addressCount) + copy(msg.AddrList, addresses) + // Randomize the addresses sent if there are more than the maximum allowed. + if addressCount > wire.MaxAddrPerMsg { + // Shuffle the address list. + for i := 0; i < wire.MaxAddrPerMsg; i++ { + j := i + rand.Intn(addressCount-i) + msg.AddrList[i], msg.AddrList[j] = msg.AddrList[j], msg.AddrList[i] + } + // Truncate it to the maximum size. + msg.AddrList = msg.AddrList[:wire.MaxAddrPerMsg] + } + p.QueueMessage(msg, nil) + return msg.AddrList, nil +} + +// PushGetBlocksMsg sends a getblocks message for the provided block locator and stop hash. It will ignore back-to-back +// duplicate requests. +// +// This function is safe for concurrent access. +func (p *Peer) PushGetBlocksMsg(locator blockchain.BlockLocator, stopHash *chainhash.Hash) (e error) { + // Extract the begin hash from the block locator, if one was specified, to use for filtering duplicate getblocks + // requests. + var beginHash *chainhash.Hash + if len(locator) > 0 { + beginHash = locator[0] + } + // Filter duplicate getblocks requests. + p.prevGetBlocksMtx.Lock() + isDuplicate := p.prevGetBlocksStop != nil && p.prevGetBlocksBegin != nil && + beginHash != nil && stopHash.IsEqual(p.prevGetBlocksStop) && + beginHash.IsEqual(p.prevGetBlocksBegin) + p.prevGetBlocksMtx.Unlock() + if isDuplicate { + T.F("filtering duplicate [getblocks] with begin hash %v, stop hash %v", beginHash, stopHash) + return nil + } + // Construct the getblocks request and queue it to be sent. + msg := wire.NewMsgGetBlocks(stopHash) + for _, hash := range locator { + e := msg.AddBlockLocatorHash(hash) + if e != nil { + return e + } + } + p.QueueMessage(msg, nil) + // Update the previous getblocks request information for filtering duplicates. + p.prevGetBlocksMtx.Lock() + p.prevGetBlocksBegin = beginHash + p.prevGetBlocksStop = stopHash + p.prevGetBlocksMtx.Unlock() + return nil +} + +// PushGetHeadersMsg sends a getblocks message for the provided block locator and stop hash. It will ignore back-to-back +// duplicate requests. +// +// This function is safe for concurrent access. +func (p *Peer) PushGetHeadersMsg(locator blockchain.BlockLocator, stopHash *chainhash.Hash) (e error) { + // Extract the begin hash from the block locator, if one was specified, to use for filtering duplicate getheaders + // requests. + var beginHash *chainhash.Hash + if len(locator) > 0 { + beginHash = locator[0] + } + // Filter duplicate getheaders requests. + p.prevGetHdrsMtx.Lock() + isDuplicate := p.prevGetHdrsStop != nil && p.prevGetHdrsBegin != nil && + beginHash != nil && stopHash.IsEqual(p.prevGetHdrsStop) && + beginHash.IsEqual(p.prevGetHdrsBegin) + p.prevGetHdrsMtx.Unlock() + if isDuplicate { + T.Ln( + "Filtering duplicate [getheaders] with begin hash", beginHash, + ) + return nil + } + // Construct the getheaders request and queue it to be sent. + msg := wire.NewMsgGetHeaders() + msg.HashStop = *stopHash + for _, hash := range locator { + e := msg.AddBlockLocatorHash(hash) + if e != nil { + return e + } + } + p.QueueMessage(msg, nil) + // Update the previous getheaders request information for filtering + // duplicates. + p.prevGetHdrsMtx.Lock() + p.prevGetHdrsBegin = beginHash + p.prevGetHdrsStop = stopHash + p.prevGetHdrsMtx.Unlock() + return nil +} + +// PushRejectMsg sends a reject message for the provided command, reject code, reject reason, and hash. +// +// The hash will only be used when the command is a tx or block and should be nil in other cases. +// +// The wait parameter will cause the function to block until the reject message has actually been sent. This function is +// safe for concurrent access. +func (p *Peer) PushRejectMsg(command string, code wire.RejectCode, reason string, hash *chainhash.Hash, wait bool) { + // Don't bother sending the reject message if the protocol version is too + // low. + if p.VersionKnown() && p.ProtocolVersion() < wire.RejectVersion { + return + } + msg := wire.NewMsgReject(command, code, reason) + if command == wire.CmdTx || command == wire.CmdBlock { + if hash == nil { + W.Ln( + "Sending a reject message for command type", command, + "which should have specified a hash but does not", + ) + hash = &zeroHash + } + msg.Hash = *hash + } + // Send the message without waiting if the caller has not requested it. + if !wait { + p.QueueMessage(msg, nil) + return + } + // Send the message and block until it has been sent before returning. + doneChan := qu.Ts(1) + p.QueueMessage(msg, doneChan) + <-doneChan +} + +// handlePingMsg is invoked when a peer receives a ping bitcoin message. +// For recent clients (protocol version > BIP0031Version), +// it replies with a pong message. For older clients, +// it does nothing and anything other than failure is considered a successful +// ping. +func (p *Peer) handlePingMsg(msg *wire.MsgPing) { + // Only reply with pong if the message is from a new enough client. + if p.ProtocolVersion() > wire.BIP0031Version { + // Include Nonce from ping so pong can be identified. + p.QueueMessage(wire.NewMsgPong(msg.Nonce), nil) + } +} + +// handlePongMsg is invoked when a peer receives a pong bitcoin message. It updates the ping statistics as required for +// recent clients (protocol version > BIP0031Version). There is no effect for older clients or when a ping was not +// previously sent. +func (p *Peer) handlePongMsg(msg *wire.MsgPong) { + // Arguably we could use a buffered channel here sending data in a fifo manner whenever we send a ping, or a list + // keeping track of the times of each ping. + // + // For now we just make a best effort and only record stats if it was for the last ping sent. Any preceding and + // overlapping pings will be ignored. It is unlikely to occur without large usage of the ping rpc call since we ping + // infrequently enough that if they overlap we would have timed out the peer. + if p.ProtocolVersion() > wire.BIP0031Version { + p.statsMtx.Lock() + if p.lastPingNonce != 0 && msg.Nonce == p.lastPingNonce { + p.lastPingMicros = time.Since(p.lastPingTime).Nanoseconds() + p.lastPingMicros /= 1000 // convert to microseconds. + p.lastPingNonce = 0 + } + p.statsMtx.Unlock() + } +} + +// readMessage reads the next bitcoin message from the peer with logging. +func (p *Peer) readMessage(encoding wire.MessageEncoding) (wire.Message, []byte, error) { + n, msg, buf, e := wire.ReadMessageWithEncodingN( + p.conn, + p.ProtocolVersion(), p.cfg.ChainParams.Net, encoding, + ) + atomic.AddUint64(&p.bytesReceived, uint64(n)) + if p.cfg.Listeners.OnRead != nil { + p.cfg.Listeners.OnRead(p, n, msg, e) + } + if e != nil { + T.Ln(e) + return nil, nil, e + } + // // Use closures to log expensive operations so they are only run when the logging level requires it. + T.C( + func() (o string) { + // Debug summary of message. + summary := messageSummary(msg) + if len(summary) > 0 { + summary = " (" + summary + ")" + } + o = fmt.Sprintf( + "Received %v%s from %s", + msg.Command(), summary, p, + ) + // o += spew.Sdump(msg) + // o += spew.Sdump(buf) + return o + }, + ) + return msg, buf, nil +} + +// writeMessage sends a bitcoin message to the peer with logging. +func (p *Peer) writeMessage(msg wire.Message, enc wire.MessageEncoding) (e error) { + // Don't do anything if we're disconnecting. + if atomic.LoadInt32(&p.disconnect) != 0 { + return nil + } + // // Use closures to log expensive operations so they are only run when the logging level requires it. + T.C( + func() (o string) { + // Debug summary of message. + summary := messageSummary(msg) + if len(summary) > 0 { + summary = " (" + summary + ")" + } + o = fmt.Sprintf( + "Sending %v%s to %s", msg.Command(), + summary, p, + ) + // o += spew.Sdump(msg) + // var buf bytes.Buffer + // _, e := wire.WriteMessageWithEncodingN( + // &buf, msg, p.ProtocolVersion(), + // p.cfg.ChainParams.Net, enc, + // ) + // if e != nil { + // return e.Error() + // } + // o += spew.Sdump(buf.Bytes()) + return + }, + ) + cmd := msg.Command() + if cmd != "ping" && cmd != "pong" && cmd != "inv" { + D.C( + func() string { + // Debug summary of message. + summary := messageSummary(msg) + if len(summary) > 0 { + summary = " (" + summary + ")" + } + o := fmt.Sprintf("Sending %v%s to %s", msg.Command(), summary, p) + // o += spew.Sdump(msg) + // var buf bytes.Buffer + // _, e = wire.WriteMessageWithEncodingN(&buf, msg, p.ProtocolVersion(), p.cfg.ChainParams.Net, enc) + // if e != nil { + // // return e.Error() + // } + return o // + spew.Sdump(buf.Bytes()) + }, + ) + } + // Write the message to the peer. + n, e := wire.WriteMessageWithEncodingN( + p.conn, msg, + p.ProtocolVersion(), p.cfg.ChainParams.Net, enc, + ) + atomic.AddUint64(&p.bytesSent, uint64(n)) + if p.cfg.Listeners.OnWrite != nil { + p.cfg.Listeners.OnWrite(p, n, msg, e) + } + return e +} + +// isAllowedReadError returns whether or not the passed error is allowed without disconnecting the peer. In particular, +// regression tests need to be allowed to send malformed messages without the peer being disconnected. +func (p *Peer) isAllowedReadError(e error) bool { + // Only allow read errors in regression test mode. + if p.cfg.ChainParams.Net != wire.TestNet { + return false + } + // Don't allow the error if it's not specifically a malformed message error. + if _, ok := e.(*wire.MessageError); !ok { + return false + } + // Don't allow the error if it's not coming from localhost or the hostname can't be determined for some reason. + var host string + host, _, e = net.SplitHostPort(p.addr) + if e != nil { + return false + } + if host != "127.0.0.1" && host != "localhost" { + return false + } + // Allowed if all checks passed. + return true +} + +// shouldHandleReadError returns whether or not the passed error, which is expected to have come from reading from the +// remote peer in the inHandler, should be logged and responded to with a reject message. +func (p *Peer) shouldHandleReadError(e error) bool { + // No logging or reject message when the peer is being forcibly disconnected. + if atomic.LoadInt32(&p.disconnect) != 0 { + return false + } + // No logging or reject message when the remote peer has been disconnected. + if e == io.EOF { + return false + } + if opErr, ok := e.(*net.OpError); ok && !opErr.Temporary() { + return false + } + return true +} + +// maybeAddDeadline potentially adds a deadline for the appropriate expected response for the passed wire protocol +// command to the pending responses map. +func (p *Peer) maybeAddDeadline(pendingResponses map[string]time.Time, msgCmd string) { + // Setup a deadline for each message being sent that expects a response. + // + // NOTE: Pings are intentionally ignored here since they are typically sent asynchronously and as a result of a long + // backlog of messages, such as is typical in the case of initial block download, the response won't be received in + // time. + deadline := time.Now().Add(stallResponseTimeout) + switch msgCmd { + case wire.CmdVersion: + // Expects a verack message. + pendingResponses[wire.CmdVerAck] = deadline + case wire.CmdMemPool: + // Expects an inv message. + pendingResponses[wire.CmdInv] = deadline + case wire.CmdGetBlocks: + // Expects an inv message. + pendingResponses[wire.CmdInv] = deadline + case wire.CmdGetData: + // Expects a block, merkleblock, tx, or notfound message. + pendingResponses[wire.CmdBlock] = deadline + pendingResponses[wire.CmdMerkleBlock] = deadline + pendingResponses[wire.CmdTx] = deadline + pendingResponses[wire.CmdNotFound] = deadline + case wire.CmdGetHeaders: + // Expects a headers message. + // + // Use a longer deadline since it can take a while for the remote peer to load all of the headers. + deadline = time.Now().Add(stallResponseTimeout * 3) + pendingResponses[wire.CmdHeaders] = deadline + } +} + +// stallHandler handles stall detection for the peer. +// +// This entails keeping track of expected responses and assigning them deadlines while accounting for the time spent in +// callbacks. +// +// It must be run as a goroutine. +func (p *Peer) stallHandler() { + T.Ln("starting stallHandler for", p.addr) + // These variables are used to adjust the deadline times forward by the time it takes callbacks to execute. + // + // This is done because new messages aren't read until the previous one is finished processing (which includes + // callbacks), so the deadline for receiving a response for a given message must account for the processing time as + // well. + var handlerActive bool + var handlersStartTime time.Time + var deadlineOffset time.Duration + // pendingResponses tracks the expected response deadline times. + pendingResponses := make(map[string]time.Time) + // stallTicker is used to periodically check pending responses that have exceeded the expected deadline and + // disconnect the peer due to stalling. + stallTicker := time.NewTicker(stallTickInterval) + defer stallTicker.Stop() + // ioStopped is used to detect when both the input and output handler goroutines are done. + var ioStopped bool +out: + for { + select { + case msg := <-p.stallControl: + switch msg.command { + case sccSendMessage: + // Add a deadline for the expected response message if needed. + p.maybeAddDeadline( + pendingResponses, + msg.message.Command(), + ) + case sccReceiveMessage: + // Remove received messages from the expected response map. + // + // Since certain commands expect one of a group of responses, remove everything in the expected group + // accordingly. + switch msgCmd := msg.message.Command(); msgCmd { + case wire.CmdBlock: + fallthrough + case wire.CmdMerkleBlock: + fallthrough + case wire.CmdTx: + fallthrough + case wire.CmdNotFound: + delete(pendingResponses, wire.CmdBlock) + delete(pendingResponses, wire.CmdMerkleBlock) + delete(pendingResponses, wire.CmdTx) + delete(pendingResponses, wire.CmdNotFound) + default: + delete(pendingResponses, msgCmd) + } + case sccHandlerStart: + // Warn on unbalanced callback signalling. + if handlerActive { + W.Ln( + "Received handler start control command while a handler is already active", + ) + continue + } + handlerActive = true + handlersStartTime = time.Now() + case sccHandlerDone: + // Warn on unbalanced callback signalling. + if !handlerActive { + W.Ln( + "Received handler done control command when a handler is not already active", + ) + continue + } + // Extend active deadlines by the time it took to execute the callback. + duration := time.Since(handlersStartTime) + deadlineOffset += duration + handlerActive = false + default: + W.Ln( + "Unsupported message command", msg.command, + ) + } + case <-stallTicker.C: + // Calculate the offset to apply to the deadline based on how long the handlers have taken to execute since + // the last tick. + now := time.Now() + offset := deadlineOffset + if handlerActive { + offset += now.Sub(handlersStartTime) + } + // Disconnect the peer if any of the pending responses don't arrive by their adjusted deadline. + for command, deadline := range pendingResponses { + if now.Before(deadline.Add(offset)) { + continue + } + D.F( + "Peer %s appears to be stalled or misbehaving, %s timeout -- disconnecting", + p, + command, + ) + p.Disconnect() + break + } + // Reset the deadline offset for the next tick. + deadlineOffset = 0 + case <-p.inQuit.Wait(): + // The stall handler can exit once both the input and output handler goroutines are done. + if ioStopped { + break out + } + ioStopped = true + case <-p.outQuit.Wait(): + // The stall handler can exit once both the input and output handler goroutines are done. + if ioStopped { + break out + } + ioStopped = true + } + } + // Drain any wait channels before going away so there is nothing left waiting on this goroutine. +cleanup: + for { + select { + case <-p.stallControl: + default: + break cleanup + } + } + T.Ln("peer stall handler done for", p) +} + +// inHandler handles all incoming messages for the peer. +// +// It must be run as a goroutine. +func (p *Peer) inHandler() { + T.Ln("starting inHandler for", p.addr) + // The timer is stopped when a new message is received and reset after it is processed. + idleTimer := time.AfterFunc( + idleTimeout, func() { + W.F("peer %s no answer for %s -- disconnecting", p, idleTimeout) + p.Disconnect() + }, + ) +out: + for atomic.LoadInt32(&p.disconnect) == 0 { + // Read a message and stop the idle timer as soon as the read is done. The timer is reset below for the next + // iteration if needed. + rMsg, buf, e := p.readMessage(p.wireEncoding) + idleTimer.Stop() + if e != nil { + T.Ln(e) + // In order to allow regression tests with malformed messages, don't disconnect the peer when we're in + // regression test mode and the error is one of the allowed errors. + if p.isAllowedReadError(e) { + E.F("allowed test error from %s: %v", p, e) + idleTimer.Reset(idleTimeout) + continue + } + // Only log the error and send reject message if the local peer is not forcibly disconnecting and the remote + // peer has not disconnected. + if p.shouldHandleReadError(e) { + errMsg := fmt.Sprintf("Can't read message from %s: %v", p, e) + if e != io.ErrUnexpectedEOF { + E.Ln(errMsg) + } + // Push a reject message for the malformed message and wait for the message to be sent before + // disconnecting. + // + // NOTE: Ideally this would include the command in the header if at least that much of the message was + // valid, but that is not currently exposed by wire, so just used malformed for the command. + p.PushRejectMsg("malformed", wire.RejectMalformed, errMsg, nil, true) + } + break out + } + atomic.StoreInt64(&p.lastRecv, time.Now().Unix()) + p.stallControl <- stallControlMsg{sccReceiveMessage, rMsg} + // Handle each supported message type. + p.stallControl <- stallControlMsg{sccHandlerStart, rMsg} + switch msg := rMsg.(type) { + case *wire.MsgVersion: + // Limit to one version message per peer. + p.PushRejectMsg( + msg.Command(), wire.RejectDuplicate, + "duplicate version message", nil, true, + ) + break out + case *wire.MsgVerAck: + // No read lock is necessary because verAckReceived is not written to in any other goroutine. + // + // Because of the potential for an attacker to use the UAC based node identifiers to cause a peer to + // disconnect from the attacked node, we have commented this thing out. + // + // if p.verAckReceived { + // I.F("already received 'verack' from peer %v"+ + // " -- disconnecting", p) + // break out + // } + // + // because of the commented section above, we won't run this if the peer is already marked + // VerAckReceived. This basically responds to spurious veracks by dropping them + if !p.verAckReceived { + p.flagsMtx.Lock() + p.verAckReceived = true + p.flagsMtx.Unlock() + if p.cfg.Listeners.OnVerAck != nil { + p.cfg.Listeners.OnVerAck(p, msg) + } + } + case *wire.MsgGetAddr: + if p.cfg.Listeners.OnGetAddr != nil { + p.cfg.Listeners.OnGetAddr(p, msg) + } + case *wire.MsgAddr: + if p.cfg.Listeners.OnAddr != nil { + p.cfg.Listeners.OnAddr(p, msg) + } + case *wire.MsgPing: + p.handlePingMsg(msg) + if p.cfg.Listeners.OnPing != nil { + p.cfg.Listeners.OnPing(p, msg) + } + case *wire.MsgPong: + p.handlePongMsg(msg) + if p.cfg.Listeners.OnPong != nil { + p.cfg.Listeners.OnPong(p, msg) + } + case *wire.MsgAlert: + if p.cfg.Listeners.OnAlert != nil { + p.cfg.Listeners.OnAlert(p, msg) + } + case *wire.MsgMemPool: + if p.cfg.Listeners.OnMemPool != nil { + p.cfg.Listeners.OnMemPool(p, msg) + } + case *wire.MsgTx: + if p.cfg.Listeners.OnTx != nil { + p.cfg.Listeners.OnTx(p, msg) + } + case *wire.Block: + if p.cfg.Listeners.OnBlock != nil { + p.cfg.Listeners.OnBlock(p, msg, buf) + } + case *wire.MsgInv: + if p.cfg.Listeners.OnInv != nil { + p.cfg.Listeners.OnInv(p, msg) + } + case *wire.MsgHeaders: + if p.cfg.Listeners.OnHeaders != nil { + p.cfg.Listeners.OnHeaders(p, msg) + } + case *wire.MsgNotFound: + if p.cfg.Listeners.OnNotFound != nil { + p.cfg.Listeners.OnNotFound(p, msg) + } + case *wire.MsgGetData: + if p.cfg.Listeners.OnGetData != nil { + p.cfg.Listeners.OnGetData(p, msg) + } + case *wire.MsgGetBlocks: + if p.cfg.Listeners.OnGetBlocks != nil { + p.cfg.Listeners.OnGetBlocks(p, msg) + } + case *wire.MsgGetHeaders: + if p.cfg.Listeners.OnGetHeaders != nil { + p.cfg.Listeners.OnGetHeaders(p, msg) + } + case *wire.MsgGetCFilters: + if p.cfg.Listeners.OnGetCFilters != nil { + p.cfg.Listeners.OnGetCFilters(p, msg) + } + case *wire.MsgGetCFHeaders: + if p.cfg.Listeners.OnGetCFHeaders != nil { + p.cfg.Listeners.OnGetCFHeaders(p, msg) + } + case *wire.MsgGetCFCheckpt: + if p.cfg.Listeners.OnGetCFCheckpt != nil { + p.cfg.Listeners.OnGetCFCheckpt(p, msg) + } + case *wire.MsgCFilter: + if p.cfg.Listeners.OnCFilter != nil { + p.cfg.Listeners.OnCFilter(p, msg) + } + case *wire.MsgCFHeaders: + if p.cfg.Listeners.OnCFHeaders != nil { + p.cfg.Listeners.OnCFHeaders(p, msg) + } + case *wire.MsgFeeFilter: + if p.cfg.Listeners.OnFeeFilter != nil { + p.cfg.Listeners.OnFeeFilter(p, msg) + } + case *wire.MsgFilterAdd: + if p.cfg.Listeners.OnFilterAdd != nil { + p.cfg.Listeners.OnFilterAdd(p, msg) + } + case *wire.MsgFilterClear: + if p.cfg.Listeners.OnFilterClear != nil { + p.cfg.Listeners.OnFilterClear(p, msg) + } + case *wire.MsgFilterLoad: + if p.cfg.Listeners.OnFilterLoad != nil { + p.cfg.Listeners.OnFilterLoad(p, msg) + } + case *wire.MsgMerkleBlock: + if p.cfg.Listeners.OnMerkleBlock != nil { + p.cfg.Listeners.OnMerkleBlock(p, msg) + } + case *wire.MsgReject: + if p.cfg.Listeners.OnReject != nil { + p.cfg.Listeners.OnReject(p, msg) + } + case *wire.MsgSendHeaders: + p.flagsMtx.Lock() + p.sendHeadersPreferred = true + p.flagsMtx.Unlock() + if p.cfg.Listeners.OnSendHeaders != nil { + p.cfg.Listeners.OnSendHeaders(p, msg) + } + default: + D.F( + "Received unhandled message of type %v from %v %s", + rMsg.Command(), + p, + ) + } + p.stallControl <- stallControlMsg{sccHandlerDone, rMsg} + // A message was received so reset the idle timer. + idleTimer.Reset(idleTimeout) + } + // Ensure the idle timer is stopped to avoid leaking the resource. + idleTimer.Stop() + // Ensure connection is closed. + p.Disconnect() + p.inQuit.Q() + T.Ln("peer input handler done for", p) +} + +// queueHandler handles the queuing of outgoing data for the peer. +// +// This runs as a muxer for various sources of input so we can ensure that server and peer handlers will not block on us +// sending a message. +// +// That data is then passed on outHandler to be actually written. +func (p *Peer) queueHandler() { + T.Ln("starting queueHandler for", p.addr) + pendingMsgs := list.New() + invSendQueue := list.New() + trickleTicker := time.NewTicker(p.cfg.TrickleInterval) + defer trickleTicker.Stop() + // We keep the waiting flag so that we know if we have a message queued to the outHandler or not. + // + // We could use the presence of a head of the list for this but then we have rather racy concerns about whether it + // has gotten it at cleanup time - and thus who sends on the message's done channel. + // + // To avoid such confusion we keep a different flag and pendingMsgs only contains messages that we have not yet + // passed to outHandler. + waiting := false + // To avoid duplication below. + queuePacket := func(msg outMsg, list *list.List, waiting bool) bool { + if !waiting { + p.sendQueue <- msg + } else { + list.PushBack(msg) + } + // we are always waiting now. + return true + } +out: + for { + select { + case msg := <-p.outputQueue: + waiting = queuePacket(msg, pendingMsgs, waiting) + // This channel is notified when a message has been sent across the network socket. + case <-p.sendDoneQueue.Wait(): + // No longer waiting if there are no more messages in the pending messages queue. + next := pendingMsgs.Front() + if next == nil { + waiting = false + continue + } + // Notify the outHandler about the next item to asynchronously send. + val := pendingMsgs.Remove(next) + p.sendQueue <- val.(outMsg) + case iv := <-p.outputInvChan: + // No handshake? They'll find out soon enough. + if p.VersionKnown() { + // If this is a new block, then we'll blast it out immediately, sipping the inv trickle queue. + if iv.Type == wire.InvTypeBlock { + invMsg := wire.NewMsgInvSizeHint(1) + e := invMsg.AddInvVect(iv) + if e != nil { + D.Ln(e) + } + waiting = queuePacket( + outMsg{msg: invMsg}, + pendingMsgs, waiting, + ) + } else { + invSendQueue.PushBack(iv) + } + } + case <-trickleTicker.C: + // Don't send anything if we're disconnecting or there is no queued inventory. version is known if send + // queue has any entries. + if atomic.LoadInt32(&p.disconnect) != 0 || + invSendQueue.Len() == 0 { + continue + } + // Create and send as many inv messages as needed to drain the inventory send queue. + invMsg := wire.NewMsgInvSizeHint(uint(invSendQueue.Len())) + for e := invSendQueue.Front(); e != nil; e = invSendQueue.Front() { + iv := invSendQueue.Remove(e).(*wire.InvVect) + // Don't send inventory that became known after the initial check. + if p.knownInventory.Exists(iv) { + continue + } + e := invMsg.AddInvVect(iv) + if e != nil { + D.Ln(e) + } + if len(invMsg.InvList) >= maxInvTrickleSize { + waiting = queuePacket( + outMsg{msg: invMsg}, + pendingMsgs, waiting, + ) + invMsg = wire.NewMsgInvSizeHint(uint(invSendQueue.Len())) + } + // Add the inventory that is being relayed to the known inventory for the peer. + p.AddKnownInventory(iv) + } + if len(invMsg.InvList) > 0 { + waiting = queuePacket( + outMsg{msg: invMsg}, + pendingMsgs, waiting, + ) + } + case <-p.quit.Wait(): + break out + } + } + // Drain any wait channels before we go away so we don't leave something waiting for us. + for e := pendingMsgs.Front(); e != nil; e = pendingMsgs.Front() { + val := pendingMsgs.Remove(e) + msg := val.(outMsg) + if msg.doneChan != nil { + msg.doneChan <- struct{}{} + } + } +cleanup: + for { + select { + case msg := <-p.outputQueue: + if msg.doneChan != nil { + msg.doneChan <- struct{}{} + } + case <-p.outputInvChan: + // Just drain channel sendDoneQueue is buffered so doesn't need draining. + default: + break cleanup + } + } + p.queueQuit.Q() + T.Ln("peer queue handler done for", p) +} + +// shouldLogWriteError returns whether or not the passed error, which is expected to have come from writing to the +// remote peer in the outHandler, should be logged. +func (p *Peer) shouldLogWriteError(e error) bool { + // No logging when the peer is being forcibly disconnected. + if atomic.LoadInt32(&p.disconnect) != 0 { + return false + } + // No logging when the remote peer has been disconnected. + if e == io.EOF { + return false + } + if opErr, ok := e.(*net.OpError); ok && !opErr.Temporary() { + return false + } + return true +} + +// outHandler handles all outgoing messages for the peer. +// +// It must be run as a goroutine. +// +// It uses a buffered channel to serialize output messages while allowing the sender to continue running asynchronously. +func (p *Peer) outHandler() { + T.Ln("starting outHandler for", p.addr) +out: + for { + select { + case msg := <-p.sendQueue: + switch m := msg.msg.(type) { + case *wire.MsgPing: + // Only expects a pong message in later protocol versions. Also set up statistics. + if p.ProtocolVersion() > wire.BIP0031Version { + p.statsMtx.Lock() + p.lastPingNonce = m.Nonce + p.lastPingTime = time.Now() + p.statsMtx.Unlock() + } + } + p.stallControl <- stallControlMsg{sccSendMessage, msg.msg} + e := p.writeMessage(msg.msg, msg.encoding) + if e != nil { + p.Disconnect() + if p.shouldLogWriteError(e) { + E.F("failed to send message to %s: %v", p, e) + } + if msg.doneChan != nil { + msg.doneChan <- struct{}{} + } + continue + } + // At this point, the message was successfully sent, so update the last send time, signal the sender of the + // message that it has been sent ( if requested), and signal the send queue to the deliver the next queued + // message. + atomic.StoreInt64(&p.lastSend, time.Now().Unix()) + if msg.doneChan != nil { + msg.doneChan <- struct{}{} + } + p.sendDoneQueue <- struct{}{} + case <-p.quit.Wait(): + break out + } + } + <-p.queueQuit + // Drain any wait channels before we go away so we don't leave something waiting for us. We have waited on queueQuit + // and thus we can be sure that we will not miss anything sent on sendQueue. +cleanup: + for { + select { + case msg := <-p.sendQueue: + if msg.doneChan != nil { + msg.doneChan <- struct{}{} + } + // no need to send on sendDoneQueue since queueHandler has been waited on and already exited. + default: + break cleanup + } + } + p.outQuit.Q() + T.Ln("peer output handler done for", p) +} + +// pingHandler periodically pings the peer. It must be run as a goroutine. +func (p *Peer) pingHandler() { + T.Ln("starting pingHandler for", p.addr) + pingTicker := time.NewTicker(pingInterval) + defer pingTicker.Stop() +out: + for { + select { + case <-pingTicker.C: + nonce, e := wire.RandomUint64() + if e != nil { + E.F("not sending ping to %s: %v", p, e) + continue + } + p.QueueMessage(wire.NewMsgPing(nonce), nil) + case <-p.quit.Wait(): + break out + } + } +} + +// QueueMessage adds the passed bitcoin message to the peer send queue. This function is safe for concurrent access. +func (p *Peer) QueueMessage(msg wire.Message, doneChan chan<- struct{}) { + p.QueueMessageWithEncoding(msg, doneChan, wire.BaseEncoding) +} + +// QueueMessageWithEncoding adds the passed bitcoin message to the peer send queue. This function is identical to +// QueueMessage, however it allows the caller to specify the wire encoding type that should be used when +// encoding/decoding blocks and transactions. +// +// This function is safe for concurrent access. +func (p *Peer) QueueMessageWithEncoding( + msg wire.Message, doneChan chan<- struct{}, + encoding wire.MessageEncoding, +) { + // Avoid risk of deadlock if goroutine already exited. The goroutine we will be sending to hangs around until it + // knows for a fact that it is marked as disconnected and *then* it drains the channels. + if !p.Connected() { + if doneChan != nil { + go func() { + doneChan <- struct{}{} + }() + } + return + } + p.outputQueue <- outMsg{msg: msg, encoding: encoding, doneChan: doneChan} +} + +// QueueInventory adds the passed inventory to the inventory send queue which might not be sent right away, rather it is +// trickled to the peer in batches. +// +// Inventory that the peer is already known to have is ignored. +// +// This function is safe for concurrent access. +func (p *Peer) QueueInventory(invVect *wire.InvVect) { + // Don't add the inventory to the send queue if the peer is already known to have it. + if p.knownInventory.Exists(invVect) { + return + } + // Avoid risk of deadlock if goroutine already exited. The goroutine we will be sending to hangs around until it + // knows for a fact that it is marked as disconnected and *then* it drains the channels. + if !p.Connected() { + return + } + p.outputInvChan <- invVect +} + +// Connected returns whether or not the peer is currently connected. This function is safe for concurrent access. +func (p *Peer) Connected() bool { + return atomic.LoadInt32(&p.connected) != 0 && + atomic.LoadInt32(&p.disconnect) == 0 +} + +// Disconnect disconnects the peer by closing the connection. Calling this function when the peer is already +// disconnected or in the process of disconnecting will have no effect. +func (p *Peer) Disconnect() { + if atomic.AddInt32(&p.disconnect, 1) != 1 { + return + } + T.Ln("disconnecting", p, log.Caller("from", 1)) + if atomic.LoadInt32(&p.connected) != 0 { + _ = p.conn.Close() + } + p.quit.Q() +} + +// readRemoteVersionMsg waits for the next message to arrive from the remote peer. If the next message is not a version +// message or the version is not acceptable then return an error. +func (p *Peer) readRemoteVersionMsg() (msg *wire.MsgVersion, e error) { + if p.versionKnown { + D.Ln("received version previously, dropping") + return + } + // Read their version message. + var remoteMsg wire.Message + remoteMsg, _, e = p.readMessage(wire.LatestEncoding) + if e != nil { + if e != io.EOF { + } + return + } + // Notify and disconnect clients if the first message is not a version message. + var ok bool + msg, ok = remoteMsg.(*wire.MsgVersion) + if !ok { + reason := "a version message must precede all others" + rejectMsg := wire.NewMsgReject( + msg.Command(), wire.RejectMalformed, + reason, + ) + _ = p.writeMessage(rejectMsg, wire.LatestEncoding) + e = errors.New(reason) + return + } + // Detect self connections. + if !AllowSelfConns && SentNonces.Exists(msg.Nonce) { + e = errors.New("disconnecting peer connected to self") + return + } + // Negotiate the protocol version and set the services to what the remote peer advertised. + p.flagsMtx.Lock() + p.Nonce = msg.Nonce + p.advertisedProtoVer = uint32(msg.ProtocolVersion) + p.protocolVersion = minUint32(p.protocolVersion, p.advertisedProtoVer) + p.versionKnown = true + p.services = msg.Services + p.flagsMtx.Unlock() + T.F( + "negotiated protocol version %d for peer %s", + p.protocolVersion, p, + ) + // Updating a bunch of stats including block based stats, and the peer's time offset. + p.statsMtx.Lock() + p.lastBlock = msg.LastBlock + p.startingHeight = msg.LastBlock + p.timeOffset = msg.Timestamp.Unix() - time.Now().Unix() + p.statsMtx.Unlock() + // Set the peer's ID, user agent, and potentially the flag which specifies the + // witness support is enabled. + p.flagsMtx.Lock() + p.id = atomic.AddInt32(&nodeCount, 1) + p.userAgent = msg.UserAgent + // // Determine if the peer would like to receive witness data with transactions, + // // or not. + // if p.services&wire.SFNodeWitness == wire.SFNodeWitness { + // p.witnessEnabled = true + // } + p.flagsMtx.Unlock() + // // Once the version message has been exchanged, we're able to determine if this + // // peer knows how to encode witness data over the wire protocol. If so, then + // // we'll switch to a decoding mode which is prepared for the new transaction + // // format introduced as part of BIP0144. + // if p.services&wire.SFNodeWitness == wire.SFNodeWitness { + // p.wireEncoding = wire.BaseEncoding + // } + // Invoke the callback if specified. + if p.cfg.Listeners.OnVersion != nil { + I.Ln("writing version message") + rejectMsg := p.cfg.Listeners.OnVersion(p, msg) + if rejectMsg != nil { + _ = p.writeMessage(rejectMsg, wire.LatestEncoding) + e = errors.New(rejectMsg.Reason) + return + } + } + // Notify and disconnect clients that have a protocol version that is too old. + // + // NOTE: If minAcceptableProtocolVersion is raised to be higher than wire.RejectVersion, this should send a reject + // packet before disconnecting. + if uint32(msg.ProtocolVersion) < MinAcceptableProtocolVersion { + // Send a reject message indicating the protocol version is obsolete + // and wait for the message to be sent before disconnecting. + reason := fmt.Sprintf( + "protocol version must be %d or greater", + MinAcceptableProtocolVersion, + ) + rejectMsg := wire.NewMsgReject( + msg.Command(), wire.RejectObsolete, + reason, + ) + _ = p.writeMessage(rejectMsg, wire.LatestEncoding) + e = errors.New(reason) + return + } + return +} + +// localVersionMsg creates a version message that can be used to send to the remote peer. +func (p *Peer) localVersionMsg() (mv *wire.MsgVersion, e error) { + var blockNum int32 + if p.cfg.NewestBlock != nil { + _, blockNum, e = p.cfg.NewestBlock() + if e != nil { + return nil, e + } + } + theirNA := p.na + // If we are behind a proxy and the connection comes from the proxy then we return an non routeable address as their + // address. This is to prevent leaking the tor proxy address. + if p.cfg.Proxy != "" { + var proxyAddress string + proxyAddress, _, e = net.SplitHostPort(p.cfg.Proxy) + // invalid proxy means poorly configured, be on the safe side. + if e != nil || p.na.IP.String() == proxyAddress { + theirNA = wire.NewNetAddressIPPort( + []byte{0, 0, 0, 0}, 0, + theirNA.Services, + ) + } + } + // Create a wire.NetAddress with only the services set to use as the "addrme" in the version message. + // + // Older nodes previously added the IP and port information to the address manager which proved to be unreliable as + // an inbound connection from a peer didn't necessarily mean the peer itself accepted inbound connections. + // + // Also, the timestamp is unused in the version message. + // I.Ln(p.addr) + // var h string + // var port string + // if h, port, e = net.SplitHostPort(p.addr); E.Chk(e) { + // } + // var portN int64 + // if portN, e = strconv.ParseInt(port, 10, 64); E.Chk(e) { + // } + // ipAddr := net.ParseIP(h) + ourNA := &wire.NetAddress{ + Timestamp: time.Now(), + Services: p.cfg.Services, + IP: p.IP, + Port: p.Port, + } + // Generate a unique Nonce for this peer so self connections can be detected. This is accomplished by adding it to a + // size-limited map of recently seen nonces. + nonce := uint64(rand.Int63()) + SentNonces.Add(nonce) + // Version message. + msg := wire.NewMsgVersion(ourNA, theirNA, nonce, blockNum) + e = msg.AddUserAgent( + p.cfg.UserAgentName, p.cfg.UserAgentVersion, + p.cfg.UserAgentComments..., + ) + if e != nil { + } + // Advertise local services. + msg.Services = p.cfg.Services + // Advertise our max supported protocol version. + msg.ProtocolVersion = int32(p.cfg.ProtocolVersion) + // Advertise if inv messages for transactions are desired. + msg.DisableRelayTx = p.cfg.DisableRelayTx + return msg, nil +} + +// writeLocalVersionMsg writes our version message to the remote peer. +func (p *Peer) writeLocalVersionMsg() (msg *wire.MsgVersion, e error) { + if msg, e = p.localVersionMsg(); E.Chk(e) { + return + } + return msg, p.writeMessage(msg, wire.LatestEncoding) +} + +// negotiateInboundProtocol waits to receive a version message from the peer then sends our version message. +// +// If the events do not occur in that order then it returns an error. +func (p *Peer) negotiateInboundProtocol() (msg *wire.MsgVersion, e error) { + if msg, e = p.readRemoteVersionMsg(); E.Chk(e) { + return + } + return p.writeLocalVersionMsg() +} + +// negotiateOutboundProtocol sends our version message then waits to receive a version message from the peer. +// +// If the events do not occur in that order then it returns an error. +func (p *Peer) negotiateOutboundProtocol() (msg *wire.MsgVersion, e error) { + if msg, e = p.writeLocalVersionMsg(); E.Chk(e) { + return + } + return p.readRemoteVersionMsg() +} + +// start begins processing input and output messages. +func (p *Peer) start(msgChan chan *wire.MsgVersion) (e error) { + T.Ln("starting peer", p, p.LocalAddr()) + negotiateErr := make(chan error, 1) + go func() { + var ee error + var msg *wire.MsgVersion + if p.inbound { + if msg, ee = p.negotiateInboundProtocol(); E.Chk(ee) { + negotiateErr <- ee + } + } else { + if msg, e = p.negotiateOutboundProtocol(); E.Chk(ee) { + negotiateErr <- ee + } + } + I.Ln("sending version message back") + msgChan <- msg + I.Ln("sent version message back") + negotiateErr <- nil + }() + // Negotiate the protocol within the specified negotiateTimeout. + select { + case e = <-negotiateErr: + if e != nil { + if e != io.EOF { + } + p.Disconnect() + return + } + case <-time.After(negotiateTimeout): + p.Disconnect() + e = errors.New("protocol negotiation timeout") + return + } + T.Ln("connected to", p) + // The protocol has been negotiated successfully so start processing input and output messages. + go p.stallHandler() + go p.inHandler() + go p.queueHandler() + go p.outHandler() + go p.pingHandler() + // Send our verack message now that the IO processing machinery has started. + p.QueueMessage(wire.NewMsgVerAck(), nil) + return +} + +// AssociateConnection associates the given conn to the peer. Calling this function when the peer is already connected +// will have no effect. +func (p *Peer) AssociateConnection(conn net.Conn) (msgChan chan *wire.MsgVersion) { + // Already connected? + if !atomic.CompareAndSwapInt32(&p.connected, 0, 1) { + I.Ln("already connected to peer", conn.RemoteAddr(), conn.LocalAddr()) + return + } + p.conn = conn + p.timeConnected = time.Now() + if p.inbound { + p.addr = p.conn.RemoteAddr().String() + // Set up a NetAddress for the peer to be used with AddrManager. + // + // We only do this inbound because outbound set this up at connection time and no point recomputing. + na, e := newNetAddress(p.conn.RemoteAddr(), p.services) + if e != nil { + E.Ln("cannot create remote net address:", e) + p.Disconnect() + return + } + p.na = na + } + msgChan = make(chan *wire.MsgVersion, 1) + I.Ln("starting peer", conn.RemoteAddr(), conn.LocalAddr()) + go func() { + if e := p.start(msgChan); E.Chk(e) { + D.F("cannot start peer %v: %v", p, e) + p.Disconnect() + } + I.Ln("finished starting peer", conn.RemoteAddr(), conn.LocalAddr()) + }() + I.Ln("returning meanwhile starting peer", conn.RemoteAddr(), conn.LocalAddr()) + return +} + +// WaitForDisconnect waits until the peer has completely disconnected and all resources are cleaned up. This will happen +// if either the local or remote side has been disconnected or the peer is forcibly disconnected via Disconnect. +func (p *Peer) WaitForDisconnect() { + <-p.quit +} + +// newPeerBase returns a new base bitcoin peer based on the inbound flag. This is used by the NewInboundPeer and +// NewOutboundPeer functions to perform base setup needed by both types of peers. +func newPeerBase(origCfg *Config, inbound bool) *Peer { + // Default to the max supported protocol version if not specified by the caller. + cfg := *origCfg // Copy to avoid mutating caller. + if cfg.ProtocolVersion == 0 { + cfg.ProtocolVersion = MaxProtocolVersion + } + // Set the chain parameters to testnet if the caller did not specify any. + if cfg.ChainParams == nil { + cfg.ChainParams = &chaincfg.TestNet3Params + } + // Set the trickle interval if a non-positive value is specified. + if cfg.TrickleInterval <= 0 { + cfg.TrickleInterval = DefaultTrickleInterval + } + p := Peer{ + inbound: inbound, + wireEncoding: wire.BaseEncoding, + knownInventory: newMruInventoryMap(maxKnownInventory), + stallControl: make(chan stallControlMsg, 1), // nonblocking sync + outputQueue: make(chan outMsg, outputBufferSize), + sendQueue: make(chan outMsg, 1), // nonblocking sync + sendDoneQueue: qu.Ts(1), // nonblocking sync + outputInvChan: make(chan *wire.InvVect, outputBufferSize), + inQuit: qu.T(), + queueQuit: qu.T(), + outQuit: qu.T(), + quit: qu.T(), + cfg: cfg, // Copy so caller can't mutate. + services: cfg.Services, + protocolVersion: cfg.ProtocolVersion, + IP: origCfg.IP, + Port: origCfg.Port, + } + return &p +} + +// NewInboundPeer returns a new inbound bitcoin peer. Use Start to begin processing incoming and outgoing messages. +func NewInboundPeer(cfg *Config) *Peer { + return newPeerBase(cfg, true) +} + +// NewOutboundPeer returns a new outbound bitcoin peer. +func NewOutboundPeer(cfg *Config, addr string) (*Peer, error) { + p := newPeerBase(cfg, false) + p.addr = addr + host, portStr, e := net.SplitHostPort(addr) + if e != nil { + return nil, e + } + port, e := strconv.ParseUint(portStr, 10, 16) + if e != nil { + return nil, e + } + if cfg.HostToNetAddress != nil { + na, e := cfg.HostToNetAddress(host, uint16(port), 0) + if e != nil { + return nil, e + } + p.na = na + } else { + p.na = wire.NewNetAddressIPPort(net.ParseIP(host), uint16(port), 0) + } + return p, nil +} + +func init() { + + rand.Seed(time.Now().UnixNano()) +} diff --git a/pkg/peer/peer_test.go b/pkg/peer/peer_test.go new file mode 100644 index 0000000..1c0ada0 --- /dev/null +++ b/pkg/peer/peer_test.go @@ -0,0 +1,869 @@ +package peer_test + +import ( + "errors" + "github.com/p9c/p9/pkg/chaincfg" + "io" + "net" + "strconv" + "testing" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/btcsuite/go-socks/socks" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/peer" + "github.com/p9c/p9/pkg/wire" +) + +// conn mocks a network connection by implementing the net.Conn interface. It is used to test peer connection without +// actually opening a network connection. +type conn struct { + io.Reader + io.Writer + io.Closer + // local network, address for the connection. + lnet, laddr string + // remote network, address for the connection. + rnet, raddr string + // mocks socks proxy if true + proxy bool +} + +// LocalAddr returns the local address for the connection. +func (c conn) LocalAddr() net.Addr { + return &addr{c.lnet, c.laddr} +} + +// Remote returns the remote address for the connection. +func (c conn) RemoteAddr() net.Addr { + if !c.proxy { + return &addr{c.rnet, c.raddr} + } + host, strPort, _ := net.SplitHostPort(c.raddr) + port, _ := strconv.Atoi(strPort) + return &socks.ProxiedAddr{ + Net: c.rnet, + Host: host, + Port: port, + } +} + +// Close handles closing the connection. +func (c conn) Close() (e error) { + if c.Closer == nil { + return nil + } + return c.Closer.Close() +} +func (c conn) SetDeadline(t time.Time) error { return nil } +func (c conn) SetReadDeadline(t time.Time) error { return nil } +func (c conn) SetWriteDeadline(t time.Time) (e error) { return nil } + +// addr mocks a network address +type addr struct { + net, address string +} + +func (m addr) Network() string { return m.net } +func (m addr) String() string { return m.address } + +// pipe turns two mock connections into a full-duplex connection similar to net.Pipe to allow pipe's with (fake) +// addresses. +func pipe(c1, c2 *conn) (*conn, *conn) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + c1.Writer = w1 + c1.Closer = w1 + c2.Reader = r1 + c1.Reader = r2 + c2.Writer = w2 + c2.Closer = w2 + return c1, c2 +} + +// peerStats holds the expected peer stats used for testing peer. +type peerStats struct { + wantUserAgent string + wantServices wire.ServiceFlag + wantProtocolVersion uint32 + wantConnected bool + wantVersionKnown bool + wantVerAckReceived bool + wantLastBlock int32 + wantStartingHeight int32 + wantLastPingTime time.Time + wantLastPingNonce uint64 + wantLastPingMicros int64 + wantTimeOffset int64 + wantBytesSent uint64 + wantBytesReceived uint64 + wantWitnessEnabled bool +} + +// testPeer tests the given peer's flags and stats +func testPeer(t *testing.T, p *peer.Peer, s peerStats) { + if p.UserAgent() != s.wantUserAgent { + t.Errorf("testPeer: wrong UserAgent - got %v, want %v", p.UserAgent(), s.wantUserAgent) + return + } + if p.Services() != s.wantServices { + t.Errorf("testPeer: wrong Services - got %v, want %v", p.Services(), s.wantServices) + return + } + if !p.LastPingTime().Equal(s.wantLastPingTime) { + t.Errorf("testPeer: wrong LastPingTime - got %v, want %v", p.LastPingTime(), s.wantLastPingTime) + return + } + if p.LastPingNonce() != s.wantLastPingNonce { + t.Errorf("testPeer: wrong LastPingNonce - got %v, want %v", p.LastPingNonce(), s.wantLastPingNonce) + return + } + if p.LastPingMicros() != s.wantLastPingMicros { + t.Errorf("testPeer: wrong LastPingMicros - got %v, want %v", p.LastPingMicros(), s.wantLastPingMicros) + return + } + if p.VerAckReceived() != s.wantVerAckReceived { + t.Errorf("testPeer: wrong VerAckReceived - got %v, want %v", p.VerAckReceived(), s.wantVerAckReceived) + return + } + if p.VersionKnown() != s.wantVersionKnown { + t.Errorf("testPeer: wrong VersionKnown - got %v, want %v", p.VersionKnown(), s.wantVersionKnown) + return + } + if p.ProtocolVersion() != s.wantProtocolVersion { + t.Errorf("testPeer: wrong ProtocolVersion - got %v, want %v", p.ProtocolVersion(), s.wantProtocolVersion) + return + } + if p.LastBlock() != s.wantLastBlock { + t.Errorf("testPeer: wrong LastBlock - got %v, want %v", p.LastBlock(), s.wantLastBlock) + return + } + // Allow for a deviation of 1s, as the second may tick when the message is in transit and the protocol doesn't support any further precision. + if p.TimeOffset() != s.wantTimeOffset && p.TimeOffset() != s.wantTimeOffset-1 { + t.Errorf( + "testPeer: wrong TimeOffset - got %v, want %v or %v", p.TimeOffset(), + s.wantTimeOffset, s.wantTimeOffset-1, + ) + return + } + if p.BytesSent() != s.wantBytesSent { + t.Errorf("testPeer: wrong BytesSent - got %v, want %v", p.BytesSent(), s.wantBytesSent) + return + } + if p.BytesReceived() != s.wantBytesReceived { + t.Errorf("testPeer: wrong BytesReceived - got %v, want %v", p.BytesReceived(), s.wantBytesReceived) + return + } + if p.StartingHeight() != s.wantStartingHeight { + t.Errorf("testPeer: wrong StartingHeight - got %v, want %v", p.StartingHeight(), s.wantStartingHeight) + return + } + if p.Connected() != s.wantConnected { + t.Errorf("testPeer: wrong Connected - got %v, want %v", p.Connected(), s.wantConnected) + return + } + if p.IsWitnessEnabled() != s.wantWitnessEnabled { + t.Errorf( + "testPeer: wrong WitnessEnabled - got %v, want %v", + p.IsWitnessEnabled(), s.wantWitnessEnabled, + ) + return + } + stats := p.StatsSnapshot() + if p.ID() != stats.ID { + t.Errorf("testPeer: wrong ID - got %v, want %v", p.ID(), stats.ID) + return + } + if p.Addr() != stats.Addr { + t.Errorf("testPeer: wrong Addr - got %v, want %v", p.Addr(), stats.Addr) + return + } + if p.LastSend() != stats.LastSend { + t.Errorf("testPeer: wrong LastSend - got %v, want %v", p.LastSend(), stats.LastSend) + return + } + if p.LastRecv() != stats.LastRecv { + t.Errorf("testPeer: wrong LastRecv - got %v, want %v", p.LastRecv(), stats.LastRecv) + return + } +} + +// TestPeerConnection tests connection between inbound and outbound peers. +func TestPeerConnection(t *testing.T) { + verack := qu.T() + peer1Cfg := &peer.Config{ + Listeners: peer.MessageListeners{ + OnVerAck: func(p *peer.Peer, msg *wire.MsgVerAck) { + verack <- struct{}{} + }, + OnWrite: func( + p *peer.Peer, bytesWritten int, msg wire.Message, + e error, + ) { + if _, ok := msg.(*wire.MsgVerAck); ok { + verack <- struct{}{} + } + }, + }, + UserAgentName: "peer", + UserAgentVersion: "1.0", + UserAgentComments: []string{"comment"}, + ChainParams: &chaincfg.MainNetParams, + ProtocolVersion: wire.RejectVersion, // Configure with older version + Services: 0, + TrickleInterval: time.Second * 10, + } + peer2Cfg := &peer.Config{ + Listeners: peer1Cfg.Listeners, + UserAgentName: "peer", + UserAgentVersion: "1.0", + UserAgentComments: []string{"comment"}, + ChainParams: &chaincfg.MainNetParams, + Services: wire.SFNodeNetwork, // | wire.SFNodeWitness, + TrickleInterval: time.Second * 10, + } + wantStats1 := peerStats{ + wantUserAgent: wire.DefaultUserAgent + "peer:1.0(comment)/", + wantServices: 0, + wantProtocolVersion: wire.RejectVersion, + wantConnected: true, + wantVersionKnown: true, + wantVerAckReceived: true, + wantLastPingTime: time.Time{}, + wantLastPingNonce: uint64(0), + wantLastPingMicros: int64(0), + wantTimeOffset: int64(0), + wantBytesSent: 167, // 143 version + 24 verack + wantBytesReceived: 167, + wantWitnessEnabled: false, + } + wantStats2 := peerStats{ + wantUserAgent: wire.DefaultUserAgent + "peer:1.0(comment)/", + wantServices: wire.SFNodeNetwork, // | wire.SFNodeWitness, + wantProtocolVersion: wire.RejectVersion, + wantConnected: true, + wantVersionKnown: true, + wantVerAckReceived: true, + wantLastPingTime: time.Time{}, + wantLastPingNonce: uint64(0), + wantLastPingMicros: int64(0), + wantTimeOffset: int64(0), + wantBytesSent: 167, // 143 version + 24 verack + wantBytesReceived: 167, + wantWitnessEnabled: true, + } + tests := []struct { + name string + setup func() (*peer.Peer, *peer.Peer, error) + }{ + { + "basic handshake", + func() (*peer.Peer, *peer.Peer, error) { + inConn, outConn := pipe( + &conn{raddr: "10.0.0.1:11047"}, + &conn{raddr: "10.0.0.2:11047"}, + ) + inPeer := peer.NewInboundPeer(peer1Cfg) + inPeer.AssociateConnection(inConn) + outPeer, e := peer.NewOutboundPeer(peer2Cfg, "10.0.0.2:11047") + if e != nil { + return nil, nil, e + } + outPeer.AssociateConnection(outConn) + for i := 0; i < 4; i++ { + select { + case <-verack.Wait(): + case <-time.After(time.Second): + return nil, nil, errors.New("verack timeout") + } + } + return inPeer, outPeer, nil + }, + }, + { + "socks proxy", + func() (*peer.Peer, *peer.Peer, error) { + inConn, outConn := pipe( + &conn{raddr: "10.0.0.1:11047", proxy: true}, + &conn{raddr: "10.0.0.2:11047"}, + ) + inPeer := peer.NewInboundPeer(peer1Cfg) + inPeer.AssociateConnection(inConn) + outPeer, e := peer.NewOutboundPeer(peer2Cfg, "10.0.0.2:11047") + if e != nil { + return nil, nil, e + } + outPeer.AssociateConnection(outConn) + for i := 0; i < 4; i++ { + select { + case <-verack.Wait(): + case <-time.After(time.Second): + return nil, nil, errors.New("verack timeout") + } + } + return inPeer, outPeer, nil + }, + }, + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + inPeer, outPeer, e := test.setup() + if e != nil { + t.Errorf("TestPeerConnection setup #%d: unexpected err %v", i, e) + return + } + testPeer(t, inPeer, wantStats2) + testPeer(t, outPeer, wantStats1) + inPeer.Disconnect() + outPeer.Disconnect() + inPeer.WaitForDisconnect() + outPeer.WaitForDisconnect() + } +} + +// TestPeerListeners tests that the peer listeners are called as expected. +func TestPeerListeners(t *testing.T) { + verack := qu.Ts(1) + ok := make(chan wire.Message, 20) + peerCfg := &peer.Config{ + Listeners: peer.MessageListeners{ + OnGetAddr: func(p *peer.Peer, msg *wire.MsgGetAddr) { + ok <- msg + }, + OnAddr: func(p *peer.Peer, msg *wire.MsgAddr) { + ok <- msg + }, + OnPing: func(p *peer.Peer, msg *wire.MsgPing) { + ok <- msg + }, + OnPong: func(p *peer.Peer, msg *wire.MsgPong) { + ok <- msg + }, + OnAlert: func(p *peer.Peer, msg *wire.MsgAlert) { + ok <- msg + }, + OnMemPool: func(p *peer.Peer, msg *wire.MsgMemPool) { + ok <- msg + }, + OnTx: func(p *peer.Peer, msg *wire.MsgTx) { + ok <- msg + }, + OnBlock: func(p *peer.Peer, msg *wire.Block, buf []byte) { + ok <- msg + }, + OnInv: func(p *peer.Peer, msg *wire.MsgInv) { + ok <- msg + }, + OnHeaders: func(p *peer.Peer, msg *wire.MsgHeaders) { + ok <- msg + }, + OnNotFound: func(p *peer.Peer, msg *wire.MsgNotFound) { + ok <- msg + }, + OnGetData: func(p *peer.Peer, msg *wire.MsgGetData) { + ok <- msg + }, + OnGetBlocks: func(p *peer.Peer, msg *wire.MsgGetBlocks) { + ok <- msg + }, + OnGetHeaders: func(p *peer.Peer, msg *wire.MsgGetHeaders) { + ok <- msg + }, + OnGetCFilters: func(p *peer.Peer, msg *wire.MsgGetCFilters) { + ok <- msg + }, + OnGetCFHeaders: func(p *peer.Peer, msg *wire.MsgGetCFHeaders) { + ok <- msg + }, + OnGetCFCheckpt: func(p *peer.Peer, msg *wire.MsgGetCFCheckpt) { + ok <- msg + }, + OnCFilter: func(p *peer.Peer, msg *wire.MsgCFilter) { + ok <- msg + }, + OnCFHeaders: func(p *peer.Peer, msg *wire.MsgCFHeaders) { + ok <- msg + }, + OnFeeFilter: func(p *peer.Peer, msg *wire.MsgFeeFilter) { + ok <- msg + }, + OnFilterAdd: func(p *peer.Peer, msg *wire.MsgFilterAdd) { + ok <- msg + }, + OnFilterClear: func(p *peer.Peer, msg *wire.MsgFilterClear) { + ok <- msg + }, + OnFilterLoad: func(p *peer.Peer, msg *wire.MsgFilterLoad) { + ok <- msg + }, + OnMerkleBlock: func(p *peer.Peer, msg *wire.MsgMerkleBlock) { + ok <- msg + }, + OnVersion: func(p *peer.Peer, msg *wire.MsgVersion) *wire.MsgReject { + ok <- msg + return nil + }, + OnVerAck: func(p *peer.Peer, msg *wire.MsgVerAck) { + verack <- struct{}{} + }, + OnReject: func(p *peer.Peer, msg *wire.MsgReject) { + ok <- msg + }, + OnSendHeaders: func(p *peer.Peer, msg *wire.MsgSendHeaders) { + ok <- msg + }, + }, + UserAgentName: "peer", + UserAgentVersion: "1.0", + UserAgentComments: []string{"comment"}, + ChainParams: &chaincfg.MainNetParams, + Services: wire.SFNodeBloom, + TrickleInterval: time.Second * 10, + } + inConn, outConn := pipe( + &conn{raddr: "10.0.0.1:11047"}, + &conn{raddr: "10.0.0.2:11047"}, + ) + inPeer := peer.NewInboundPeer(peerCfg) + inPeer.AssociateConnection(inConn) + peerCfg.Listeners = peer.MessageListeners{ + OnVerAck: func(p *peer.Peer, msg *wire.MsgVerAck) { + verack <- struct{}{} + }, + } + outPeer, e := peer.NewOutboundPeer(peerCfg, "10.0.0.1:11047") + if e != nil { + t.Errorf("NewOutboundPeer: unexpected err %v\n", e) + return + } + outPeer.AssociateConnection(outConn) + for i := 0; i < 2; i++ { + select { + case <-verack.Wait(): + case <-time.After(time.Second * 1): + t.Errorf("TestPeerListeners: verack timeout\n") + return + } + } + tests := []struct { + listener string + msg wire.Message + }{ + { + "OnGetAddr", + wire.NewMsgGetAddr(), + }, + { + "OnAddr", + wire.NewMsgAddr(), + }, + { + "OnPing", + wire.NewMsgPing(42), + }, + { + "OnPong", + wire.NewMsgPong(42), + }, + { + "OnAlert", + wire.NewMsgAlert([]byte("payload"), []byte("signature")), + }, + { + "OnMemPool", + wire.NewMsgMemPool(), + }, + { + "OnTx", + wire.NewMsgTx(wire.TxVersion), + }, + { + "OnBlock", + wire.NewMsgBlock( + wire.NewBlockHeader( + 1, + &chainhash.Hash{}, &chainhash.Hash{}, 1, 1, + ), + ), + }, + { + "OnInv", + wire.NewMsgInv(), + }, + { + "OnHeaders", + wire.NewMsgHeaders(), + }, + { + "OnNotFound", + wire.NewMsgNotFound(), + }, + { + "OnGetData", + wire.NewMsgGetData(), + }, + { + "OnGetBlocks", + wire.NewMsgGetBlocks(&chainhash.Hash{}), + }, + { + "OnGetHeaders", + wire.NewMsgGetHeaders(), + }, + { + "OnGetCFilters", + wire.NewMsgGetCFilters(wire.GCSFilterRegular, 0, &chainhash.Hash{}), + }, + { + "OnGetCFHeaders", + wire.NewMsgGetCFHeaders(wire.GCSFilterRegular, 0, &chainhash.Hash{}), + }, + { + "OnGetCFCheckpt", + wire.NewMsgGetCFCheckpt(wire.GCSFilterRegular, &chainhash.Hash{}), + }, + { + "OnCFilter", + wire.NewMsgCFilter( + wire.GCSFilterRegular, &chainhash.Hash{}, + []byte("payload"), + ), + }, + { + "OnCFHeaders", + wire.NewMsgCFHeaders(), + }, + { + "OnFeeFilter", + wire.NewMsgFeeFilter(15000), + }, + { + "OnFilterAdd", + wire.NewMsgFilterAdd([]byte{0x01}), + }, + { + "OnFilterClear", + wire.NewMsgFilterClear(), + }, + { + "OnFilterLoad", + wire.NewMsgFilterLoad([]byte{0x01}, 10, 0, wire.BloomUpdateNone), + }, + { + "OnMerkleBlock", + wire.NewMsgMerkleBlock( + wire.NewBlockHeader( + 1, + &chainhash.Hash{}, &chainhash.Hash{}, 1, 1, + ), + ), + }, + // only one verack message is allowed + { + "OnReject", + wire.NewMsgReject("block", wire.RejectDuplicate, "dupe block"), + }, + { + "OnSendHeaders", + wire.NewMsgSendHeaders(), + }, + } + t.Logf("Running %d tests", len(tests)) + for _, test := range tests { + // Queue the test message + outPeer.QueueMessage(test.msg, nil) + select { + case <-ok: + case <-time.After(time.Second * 1): + t.Errorf("TestPeerListeners: %s timeout", test.listener) + return + } + } + inPeer.Disconnect() + outPeer.Disconnect() +} + +// TestOutboundPeer tests that the outbound peer works as expected. +func TestOutboundPeer(t *testing.T) { + peerCfg := &peer.Config{ + NewestBlock: func() (*chainhash.Hash, int32, error) { + return nil, 0, errors.New("newest block not found") + }, + UserAgentName: "peer", + UserAgentVersion: "1.0", + UserAgentComments: []string{"comment"}, + ChainParams: &chaincfg.MainNetParams, + Services: 0, + TrickleInterval: time.Second * 10, + } + r, w := io.Pipe() + c := &conn{raddr: "10.0.0.1:11047", Writer: w, Reader: r} + p, e := peer.NewOutboundPeer(peerCfg, "10.0.0.1:11047") + if e != nil { + t.Errorf("NewOutboundPeer: unexpected err - %v\n", e) + return + } + // Test trying to connect twice. + p.AssociateConnection(c) + p.AssociateConnection(c) + disconnected := qu.T() + go func() { + p.WaitForDisconnect() + disconnected <- struct{}{} + }() + select { + case <-disconnected.Wait(): + disconnected.Q() + case <-time.After(time.Second): + t.Fatal("Peer did not automatically disconnect.") + } + if p.Connected() { + t.Fatalf("Should not be connected as NewestBlock produces error.") + } + // Test Queue Inv + fakeBlockHash := &chainhash.Hash{0: 0x00, 1: 0x01} + fakeInv := wire.NewInvVect(wire.InvTypeBlock, fakeBlockHash) + // Should be noops as the peer could not connect. + p.QueueInventory(fakeInv) + p.AddKnownInventory(fakeInv) + p.QueueInventory(fakeInv) + fakeMsg := wire.NewMsgVerAck() + p.QueueMessage(fakeMsg, nil) + done := qu.T() + p.QueueMessage(fakeMsg, done) + <-done + p.Disconnect() + // Test NewestBlock + var newestBlock = func() (*chainhash.Hash, int32, error) { + hashStr := "14a0810ac680a3eb3f82edc878cea25ec41d6b790744e5daeef" + var hash *chainhash.Hash + hash, e = chainhash.NewHashFromStr(hashStr) + if e != nil { + return nil, 0, e + } + return hash, 234439, nil + } + peerCfg.NewestBlock = newestBlock + r1, w1 := io.Pipe() + c1 := &conn{raddr: "10.0.0.1:11047", Writer: w1, Reader: r1} + p1, e := peer.NewOutboundPeer(peerCfg, "10.0.0.1:11047") + if e != nil { + t.Errorf("NewOutboundPeer: unexpected err - %v\n", e) + return + } + p1.AssociateConnection(c1) + // Test update latest block + latestBlockHash, e := chainhash.NewHashFromStr("1a63f9cdff1752e6375c8c76e543a71d239e1a2e5c6db1aa679") + if e != nil { + t.Errorf("NewHashFromStr: unexpected err %v\n", e) + return + } + p1.UpdateLastAnnouncedBlock(latestBlockHash) + p1.UpdateLastBlockHeight(234440) + if p1.LastAnnouncedBlock() != latestBlockHash { + t.Errorf( + "LastAnnouncedBlock: wrong block - got %v, want %v", + p1.LastAnnouncedBlock(), latestBlockHash, + ) + return + } + // Test Queue Inv after connection + p1.QueueInventory(fakeInv) + p1.Disconnect() + // Test regression + peerCfg.ChainParams = &chaincfg.RegressionTestParams + peerCfg.Services = wire.SFNodeBloom + r2, w2 := io.Pipe() + c2 := &conn{raddr: "10.0.0.1:11047", Writer: w2, Reader: r2} + p2, e := peer.NewOutboundPeer(peerCfg, "10.0.0.1:11047") + if e != nil { + t.Errorf("NewOutboundPeer: unexpected err - %v\n", e) + return + } + p2.AssociateConnection(c2) + // Test PushXXX + var addrs []*wire.NetAddress + for i := 0; i < 5; i++ { + na := wire.NetAddress{} + addrs = append(addrs, &na) + } + if _, e = p2.PushAddrMsg(addrs); e != nil { + t.Errorf("PushAddrMsg: unexpected err %v\n", e) + return + } + if e := p2.PushGetBlocksMsg(nil, &chainhash.Hash{}); e != nil { + t.Errorf("PushGetBlocksMsg: unexpected err %v\n", e) + return + } + if e := p2.PushGetHeadersMsg(nil, &chainhash.Hash{}); e != nil { + t.Errorf("PushGetHeadersMsg: unexpected err %v\n", e) + return + } + p2.PushRejectMsg("block", wire.RejectMalformed, "malformed", nil, false) + p2.PushRejectMsg("block", wire.RejectInvalid, "invalid", nil, false) + // Test Queue Messages + p2.QueueMessage(wire.NewMsgGetAddr(), nil) + p2.QueueMessage(wire.NewMsgPing(1), nil) + p2.QueueMessage(wire.NewMsgMemPool(), nil) + p2.QueueMessage(wire.NewMsgGetData(), nil) + p2.QueueMessage(wire.NewMsgGetHeaders(), nil) + p2.QueueMessage(wire.NewMsgFeeFilter(20000), nil) + p2.Disconnect() +} + +// // Tests that the node disconnects from peers with an unsupported protocol version. +// func TestUnsupportedVersionPeer(// t *testing.T) { +// peerCfg := &peer.Config{ +// UserAgentName: "peer", +// UserAgentVersion: "1.0", +// UserAgentComments: []string{"comment"}, +// ChainParams: &chaincfg.MainNetParams, +// Services: 0, +// TrickleInterval: time.Second * 10, +// } +// localNA := wire.NewNetAddressIPPort( +// net.ParseIP("10.0.0.1"), +// uint16(11047), +// wire.SFNodeNetwork, +// ) +// remoteNA := wire.NewNetAddressIPPort( +// net.ParseIP("10.0.0.2"), +// uint16(11047), +// wire.SFNodeNetwork, +// ) +// localConn, remoteConn := pipe( +// &conn{laddr: "10.0.0.1:11047", raddr: "10.0.0.2:11047"}, +// &conn{laddr: "10.0.0.2:11047", raddr: "10.0.0.1:11047"}, +// ) +// p, e := peer.NewOutboundPeer(peerCfg, "10.0.0.1:11047") +// if e != nil { +// t.Fatalf("NewOutboundPeer: unexpected err - %v\n", e) +// } +// p.AssociateConnection(localConn) +// // Read outbound messages to peer into a channel +// outboundMessages := make(chan wire.Message) +// go func() { +// for { +// _, msg, _, e = wire.ReadMessageN( +// remoteConn, +// p.ProtocolVersion(), +// peerCfg.ChainParams.Net, +// ) +// if e == io.EOF { +// close(outboundMessages) +// return +// } +// if e != nil { +// t.Errorf("Error reading message from local node: %v\n", e) +// return +// } +// outboundMessages <- msg +// } +// }() +// // Read version message sent to remote peer +// select { +// case msg := <-outboundMessages: +// if _, ok := msg.(*wire.MsgVersion); !ok { +// t.Fatalf("Expected version message, got [%s]", msg.Command()) +// } +// case <-time.After(time.Second): +// t.Fatal("Peer did not send version message") +// } +// // Remote peer writes version message advertising invalid protocol version 1 +// invalidVersionMsg := wire.NewMsgVersion(remoteNA, localNA, 0, 0) +// invalidVersionMsg.ProtocolVersion = 1 +// _, e = wire.WriteMessageN( +// remoteConn.Writer, +// invalidVersionMsg, +// uint32(invalidVersionMsg.ProtocolVersion), +// peerCfg.ChainParams.Net, +// ) +// if e != nil { +// t.Fatalf("wire.WriteMessageN: unexpected err - %v\n", e) +// } +// // Expect peer to disconnect automatically +// disconnected := qu.T() +// go func() { +// p.WaitForDisconnect() +// disconnected <- struct{}{} +// }() +// select { +// case <-disconnected: +// close(disconnected) +// case <-time.After(time.Second): +// t.Fatal("Peer did not automatically disconnect") +// } +// // Expect no further outbound messages from peer +// select { +// case msg, chanOpen := <-outboundMessages: +// if chanOpen { +// t.Fatalf("Expected no further messages, received [%s]", msg.Command()) +// } +// case <-time.After(time.Second): +// t.Fatal("Timeout waiting for remote reader to close") +// } +// } + +// TestDuplicateVersionMsg ensures that receiving a version message after one has already been received results in the +// peer being disconnected. +func TestDuplicateVersionMsg(t *testing.T) { + // Create a pair of peers that are connected to each other using a fake connection. + verack := qu.T() + peerCfg := &peer.Config{ + Listeners: peer.MessageListeners{ + OnVerAck: func(p *peer.Peer, msg *wire.MsgVerAck) { + verack <- struct{}{} + }, + }, + UserAgentName: "peer", + UserAgentVersion: "1.0", + ChainParams: &chaincfg.MainNetParams, + Services: 0, + } + inConn, outConn := pipe( + &conn{laddr: "10.0.0.1:9108", raddr: "10.0.0.2:9108"}, + &conn{laddr: "10.0.0.2:9108", raddr: "10.0.0.1:9108"}, + ) + outPeer, e := peer.NewOutboundPeer(peerCfg, inConn.laddr) + if e != nil { + t.Fatalf("NewOutboundPeer: unexpected err: %v\n", e) + } + outPeer.AssociateConnection(outConn) + inPeer := peer.NewInboundPeer(peerCfg) + inPeer.AssociateConnection(inConn) + // Wait for the veracks from the initial protocol version negotiation. + for i := 0; i < 2; i++ { + select { + case <-verack.Wait(): + case <-time.After(time.Second): + t.Fatal("verack timeout") + } + } + // Queue a duplicate version message from the outbound peer and wait until it is sent. + done := qu.T() + outPeer.QueueMessage(&wire.MsgVersion{}, done) + select { + case <-done.Wait(): + case <-time.After(time.Second): + t.Fatal("send duplicate version timeout") + } + // Ensure the peer that is the recipient of the duplicate version closes the connection. + disconnected := qu.Ts(1) + go func() { + inPeer.WaitForDisconnect() + disconnected <- struct{}{} + }() + select { + case <-disconnected.Wait(): + case <-time.After(time.Second): + t.Fatal("peer did not disconnect") + } +} +func init() { + + // Allow self connection when running the tests. + peer.TstAllowSelfConns() +} diff --git a/pkg/pipe/cmd/example/main.go b/pkg/pipe/cmd/example/main.go new file mode 100644 index 0000000..cf389dc --- /dev/null +++ b/pkg/pipe/cmd/example/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/pipe" +) + +func main() { + quit := qu.T() + p := pipe.Consume( + quit, func(b []byte) (e error) { + fmt.Println("from child:", string(b)) + return + }, "go", "run", "serve/main.go", + ) + for { + _, e := p.StdConn.Write([]byte("ping")) + if e != nil { + fmt.Println("err:", e) + } + time.Sleep(time.Second) + } +} diff --git a/pkg/pipe/cmd/example/serve/main.go b/pkg/pipe/cmd/example/serve/main.go new file mode 100644 index 0000000..d964f02 --- /dev/null +++ b/pkg/pipe/cmd/example/serve/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/pipe" +) + +func main() { + p := pipe.Serve(qu.T(), func(b []byte) (e error) { + fmt.Print("from parent: ", string(b)) + return + }, + ) + for { + _, e := p.Write([]byte("ping")) + if e != nil { + fmt.Println("err:", e) + } + time.Sleep(time.Second) + } +} diff --git a/pkg/pipe/cmd/pipelog/log.go b/pkg/pipe/cmd/pipelog/log.go new file mode 100644 index 0000000..da27aa5 --- /dev/null +++ b/pkg/pipe/cmd/pipelog/log.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/pipe/cmd/pipelog/pipelog.go b/pkg/pipe/cmd/pipelog/pipelog.go new file mode 100644 index 0000000..e5ced88 --- /dev/null +++ b/pkg/pipe/cmd/pipelog/pipelog.go @@ -0,0 +1,33 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/pkg/pipe" + + "os" + "time" + + "github.com/p9c/p9/pkg/qu" +) + +func main() { + // var e error + log.SetLogLevel("trace") + // command := "pod -D test0 -n testnet -l trace --solo --lan --pipelog node" + quit := qu.T() + // splitted := strings.Split(command, " ") + splitted := os.Args[1:] + w := pipe.LogConsume(quit, pipe.SimpleLog(splitted[len(splitted)-1]), pipe.FilterNone, splitted...) + D.Ln("\n\n>>> >>> >>> >>> >>> >>> >>> >>> >>> starting") + pipe.Start(w) + D.Ln("\n\n>>> >>> >>> >>> >>> >>> >>> >>> >>> started") + time.Sleep(time.Second * 4) + D.Ln("\n\n>>> >>> >>> >>> >>> >>> >>> >>> >>> stopping") + pipe.Kill(w) + D.Ln("\n\n>>> >>> >>> >>> >>> >>> >>> >>> >>> stopped") + // time.Sleep(time.Second * 5) + // D.Ln(interrupt.GoroutineDump()) + // if e = w.Wait(); E.Chk(e) { + // } + // time.Sleep(time.Second * 3) +} diff --git a/pkg/pipe/log.go b/pkg/pipe/log.go new file mode 100644 index 0000000..78e4097 --- /dev/null +++ b/pkg/pipe/log.go @@ -0,0 +1,9 @@ +package pipe + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) diff --git a/pkg/pipe/logging.go b/pkg/pipe/logging.go new file mode 100644 index 0000000..d3c9759 --- /dev/null +++ b/pkg/pipe/logging.go @@ -0,0 +1,199 @@ +package pipe + +import ( + "github.com/niubaoshu/gotiny" + "github.com/p9c/p9/pkg/interrupt" + "github.com/p9c/p9/pkg/qu" + "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/log" +) + +func LogConsume( + quit qu.C, handler func(ent *log.Entry) (e error,), + filter func(pkg string) (out bool), args ...string, +) *Worker { + D.Ln("starting log consumer") + return Consume( + quit, func(b []byte) (e error) { + // we are only listening for entries + if len(b) >= 4 { + magic := string(b[:4]) + switch magic { + case "entr": + var ent log.Entry + n := gotiny.Unmarshal(b, &ent) + D.Ln("consume", n) + if filter(ent.Package) { + // if the worker filter is out of sync this stops it printing + return + } + switch ent.Level { + case log.Fatal: + case log.Error: + case log.Warn: + case log.Info: + case log.Check: + case log.Debug: + case log.Trace: + default: + D.Ln("got an empty log entry") + return + } + if e = handler(&ent); E.Chk(e) { + } + } + } + return + }, args..., + ) +} + +func Start(w *Worker) { + D.Ln("sending start signal") + var n int + var e error + if n, e = w.StdConn.Write([]byte("run ")); n < 1 || E.Chk(e) { + D.Ln("failed to write", w.Args) + } +} + +// Stop running the worker +func Stop(w *Worker) { + D.Ln("sending stop signal") + var n int + var e error + if n, e = w.StdConn.Write([]byte("stop")); n < 1 || E.Chk(e) { + D.Ln("failed to write", w.Args) + } +} + +// Kill sends a kill signal via the pipe logger +func Kill(w *Worker) { + var e error + if w == nil { + D.Ln("asked to kill worker that is already nil") + return + } + var n int + D.Ln("sending kill signal") + if n, e = w.StdConn.Write([]byte("kill")); n < 1 || E.Chk(e) { + D.Ln("failed to write") + return + } + if e = w.Cmd.Wait(); E.Chk(e) { + } + D.Ln("sent kill signal") +} + +// SetLevel sets the level of logging from the worker +func SetLevel(w *Worker, level string) { + if w == nil { + return + } + D.Ln("sending set level", level) + lvl := 0 + for i := range log.Levels { + if level == log.Levels[i] { + lvl = i + } + } + var n int + var e error + if n, e = w.StdConn.Write([]byte("slvl" + string(byte(lvl)))); n < 1 || + E.Chk(e) { + D.Ln("failed to write") + } +} + +// LogServe starts up a handler to listen to logs from the child process worker +func LogServe(quit qu.C, appName string) { + D.Ln("starting log server") + lc := log.AddLogChan() + var logOn atomic.Bool + logOn.Store(false) + p := Serve( + quit, func(b []byte) (e error) { + // listen for commands to enable/disable logging + if len(b) >= 4 { + magic := string(b[:4]) + switch magic { + case "run ": + D.Ln("setting to run") + logOn.Store(true) + case "stop": + D.Ln("stopping") + logOn.Store(false) + case "slvl": + D.Ln("setting level", log.Levels[b[4]]) + log.SetLogLevel(log.Levels[b[4]]) + case "kill": + D.Ln("received kill signal from pipe, shutting down", appName) + interrupt.Request() + quit.Q() + } + } + return + }, + ) + go func() { + out: + for { + select { + case <-quit.Wait(): + if !log.LogChanDisabled.Load() { + log.LogChanDisabled.Store(true) + } + D.Ln("quitting pipe logger") + interrupt.Request() + logOn.Store(false) + out2: + // drain log channel + for { + select { + case <-lc: + break + default: + break out2 + } + } + break out + case ent := <-lc: + if !logOn.Load() { + break out + } + var n int + var e error + if n, e = p.Write(gotiny.Marshal(&ent)); !E.Chk(e) { + if n < 1 { + E.Ln("short write") + } + } else { + break out + } + } + } + <-interrupt.HandlersDone + D.Ln("finished pipe logger") + }() +} + +// FilterNone is a filter that doesn't +func FilterNone(string) bool { + return false +} + +// SimpleLog is a very simple log printer +func SimpleLog(name string) func(ent *log.Entry) (e error) { + return func(ent *log.Entry) (e error) { + D.F( + "%s[%s] %s %s", + name, + ent.Level, + // ent.Time.Format(time.RFC3339), + ent.Text, + ent.CodeLocation, + ) + return + } +} diff --git a/pkg/pipe/pipe.go b/pkg/pipe/pipe.go new file mode 100644 index 0000000..97c7a2b --- /dev/null +++ b/pkg/pipe/pipe.go @@ -0,0 +1,85 @@ +package pipe + +import ( + "io" + "os" + + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/pkg/interrupt" + "github.com/p9c/p9/pkg/qu" +) + +// Consume listens for messages from a child process over a stdio pipe. +func Consume( + quit qu.C, + handler func([]byte) error, + args ...string, +) *Worker { + var n int + var e error + D.Ln("spawning worker process", args) + var w *Worker + if w, e = Spawn(quit, args...); E.Chk(e) { + } + data := make([]byte, 8192) + go func() { + out: + for { + select { + case <-interrupt.HandlersDone.Wait(): + D.Ln("quitting log consumer") + break out + case <-quit.Wait(): + D.Ln("breaking on quit signal") + break out + default: + } + n, e = w.StdConn.Read(data) + if n == 0 { + F.Ln("read zero from pipe", args) + log.LogChanDisabled.Store(true) + break out + } + if E.Chk(e) && e != io.EOF { + // Probably the child process has died, so quit + E.Ln("err:", e) + break out + } else if n > 0 { + if e = handler(data[:n]); E.Chk(e) { + } + } + } + }() + return w +} + +// Serve runs a goroutine processing the FEC encoded packets, gathering them and +// decoding them to be delivered to a handler function +func Serve(quit qu.C, handler func([]byte) error) *StdConn { + var n int + var e error + data := make([]byte, 8192) + go func() { + D.Ln("starting pipe server") + out: + for { + select { + case <-quit.Wait(): + break out + default: + } + n, e = os.Stdin.Read(data) + if e != nil && e != io.EOF { + break out + } + if n > 0 { + if e = handler(data[:n]); E.Chk(e) { + break out + } + } + } + D.Ln("pipe server shut down") + }() + return New(os.Stdin, os.Stdout, quit) +} diff --git a/pkg/pipe/stdconn.go b/pkg/pipe/stdconn.go new file mode 100644 index 0000000..438e52f --- /dev/null +++ b/pkg/pipe/stdconn.go @@ -0,0 +1,63 @@ +package pipe + +import ( + "fmt" + "io" + "net" + "runtime" + "time" + + "github.com/p9c/p9/pkg/qu" +) + +type StdConn struct { + io.ReadCloser + io.WriteCloser + Quit qu.C +} + +func New(in io.ReadCloser, out io.WriteCloser, quit qu.C) (s *StdConn) { + s = &StdConn{in, out, quit} + _, file, line, _ := runtime.Caller(1) + o := fmt.Sprintf("%s:%d", file, line) + T.Ln("new StdConn at", o) + return +} + +func (s *StdConn) Read(b []byte) (n int, e error) { + return s.ReadCloser.Read(b) +} + +func (s *StdConn) Write(b []byte) (n int, e error) { + return s.WriteCloser.Write(b) +} + +func (s *StdConn) Close() (e error) { + s.Quit.Q() + return +} + +func (s *StdConn) LocalAddr() (addr net.Addr) { + // this is a no-op as it is not relevant to the type of connection + return +} + +func (s *StdConn) RemoteAddr() (addr net.Addr) { + // this is a no-op as it is not relevant to the type of connection + return +} + +func (s *StdConn) SetDeadline(t time.Time) (e error) { + // this is a no-op as it is not relevant to the type of connection + return +} + +func (s *StdConn) SetReadDeadline(t time.Time) (e error) { + // this is a no-op as it is not relevant to the type of connection + return +} + +func (s *StdConn) SetWriteDeadline(t time.Time) (e error) { + // this is a no-op as it is not relevant to the type of connection + return +} diff --git a/pkg/pipe/worker.go b/pkg/pipe/worker.go new file mode 100644 index 0000000..dc9998a --- /dev/null +++ b/pkg/pipe/worker.go @@ -0,0 +1,78 @@ +package pipe + +import ( + "io" + "os" + "os/exec" + "runtime" + "syscall" + + "github.com/p9c/p9/pkg/qu" +) + +type Worker struct { + Cmd *exec.Cmd + Args []string + // Stderr io.WriteCloser + // StdPipe io.ReadCloser + StdConn *StdConn +} + +// Spawn starts up an arbitrary executable file with given arguments and +// attaches a connection to its stdin/stdout +func Spawn(quit qu.C, args ...string) (w *Worker, e error) { + w = &Worker{ + Cmd: exec.Command(args[0], args[1:]...), + Args: args, + } + var cmdOut io.ReadCloser + if cmdOut, e = w.Cmd.StdoutPipe(); E.Chk(e) { + return + } + var cmdIn io.WriteCloser + if cmdIn, e = w.Cmd.StdinPipe(); E.Chk(e) { + return + } + w.StdConn = New(cmdOut, cmdIn, quit) + w.Cmd.Stderr = os.Stderr + if e = w.Cmd.Start(); E.Chk(e) { + } + return +} + +// Wait for the process to finish running +func (w *Worker) Wait() (e error) { + return w.Cmd.Wait() +} + +// Interrupt the child process. +// This invokes kill on windows because windows doesn't have an interrupt +// signal. +func (w *Worker) Interrupt() (e error) { + if runtime.GOOS == "windows" { + if e = w.Cmd.Process.Kill(); E.Chk(e) { + } + return + } + if e = w.Cmd.Process.Signal(syscall.SIGINT); !E.Chk(e) { + D.Ln("interrupted") + } + return +} + +// Kill forces the child process to shut down without cleanup +func (w *Worker) Kill() (e error) { + if e = w.Cmd.Process.Kill(); !E.Chk(e) { + D.Ln("killed") + } + return +} + +// Stop signals the worker to shut down cleanly. +// +// Note that the worker must have handlers for os.Signal messages. +// +// It is possible and neater to put a quit method in the IPC API and use the quit channel built into the StdConn +func (w *Worker) Stop() (e error) { + return w.Cmd.Process.Signal(os.Interrupt) +} diff --git a/pkg/pipe/workerpause.go b/pkg/pipe/workerpause.go new file mode 100644 index 0000000..7fe25cf --- /dev/null +++ b/pkg/pipe/workerpause.go @@ -0,0 +1,23 @@ +// +build !windows + +package pipe + +import ( + "syscall" +) + +// Pause sends a signal to the worker process to stop +func (w *Worker) Pause() (e error) { + if e = w.Cmd.Process.Signal(syscall.SIGSTOP); !E.Chk(e) { + D.Ln("paused") + } + return +} + +// Continue sends a signal to a worker process to resume work +func (w *Worker) Continue() (e error) { + if e = w.Cmd.Process.Signal(syscall.SIGCONT); !E.Chk(e) { + D.Ln("resumed") + } + return +} diff --git a/pkg/pipe/workerpause_windows.go b/pkg/pipe/workerpause_windows.go new file mode 100644 index 0000000..969d04d --- /dev/null +++ b/pkg/pipe/workerpause_windows.go @@ -0,0 +1,8 @@ +package pipe + +func (w *Worker) Pause() (e error) { + return +} +func (w *Worker) Resume() (e error) { + return +} diff --git a/pkg/qrcode/log.go b/pkg/qrcode/log.go new file mode 100644 index 0000000..7c77e6d --- /dev/null +++ b/pkg/qrcode/log.go @@ -0,0 +1,43 @@ +package qrcode + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/qrcode/qr.go b/pkg/qrcode/qr.go new file mode 100644 index 0000000..88b563c --- /dev/null +++ b/pkg/qrcode/qr.go @@ -0,0 +1,1130 @@ +package qrcode + +import ( + "bytes" + "errors" + "image" + "image/color" + "math" + "strconv" +) + +var positionAdjustPatternTable = [][]int{ + {}, + {}, // version 1 + {6, 18}, + {6, 22}, + {6, 26}, + {6, 30}, // version 5 + {6, 34}, + {6, 22, 38}, + {6, 24, 42}, + {6, 26, 46}, + {6, 28, 50}, // version 10 + {6, 30, 54}, + {6, 32, 58}, + {6, 34, 62}, + {6, 26, 46, 66}, + {6, 26, 48, 70}, + {6, 26, 50, 74}, + {6, 30, 54, 78}, + {6, 30, 56, 82}, + {6, 30, 58, 86}, + {6, 34, 62, 90}, // version 20 + {6, 28, 50, 72, 94}, + {6, 26, 50, 74, 98}, + {6, 30, 54, 78, 102}, + {6, 28, 54, 80, 106}, + {6, 32, 58, 84, 110}, + {6, 30, 58, 86, 114}, + {6, 34, 62, 90, 118}, + {6, 26, 50, 74, 98, 122}, + {6, 30, 54, 78, 102, 126}, + {6, 26, 52, 78, 104, 130}, // version 30 + {6, 30, 56, 82, 108, 134}, + {6, 34, 60, 86, 112, 138}, + {6, 30, 58, 86, 114, 142}, + {6, 34, 62, 90, 118, 146}, + {6, 30, 54, 78, 102, 126, 150}, + {6, 24, 50, 76, 102, 128, 154}, + {6, 28, 54, 80, 106, 132, 158}, + {6, 32, 58, 84, 110, 136, 162}, + {6, 26, 54, 82, 110, 138, 166}, + {6, 30, 58, 86, 114, 142, 170}} // version 40 + +type ECLevel int + +const ( + ECLevelL ECLevel = 1 << iota + ECLevelM ECLevel = 1 << iota + ECLevelQ ECLevel = 1 << iota + ECLevelH ECLevel = 1 << iota +) + +var typeInformationTable = map[ECLevel][]int{ + ECLevelL: {0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976}, + ECLevelM: {0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0}, + ECLevelQ: {0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed}, + ECLevelH: {0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b}, +} + +var versionInformationTable = []int{ + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, + 0x177ec, 0x18ec4, 0x191e1, 0x1Afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ef75, + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, + 0x27541, 0x28c69, +} + +var errorCorrectionTable = []map[ECLevel][]int{ + // ec code word per block + // block 1 count + // block 1 data code words + // block 2 count + // block 2 data code words + {}, + {ECLevelL: {7, 1, 19, 0, 0}, ECLevelM: {10, 1, 16, 0, 0}, ECLevelQ: {13, 1, 13, 0, 0}, ECLevelH: {17, 1, 9, 0, 0}}, // version 1 + {ECLevelL: {10, 1, 34, 0, 0}, ECLevelM: {16, 1, 28, 0, 0}, ECLevelQ: {22, 1, 22, 0, 0}, ECLevelH: {28, 1, 16, 0, 0}}, + {ECLevelL: {15, 1, 55, 0, 0}, ECLevelM: {26, 1, 44, 0, 0}, ECLevelQ: {18, 2, 17, 0, 0}, ECLevelH: {22, 2, 13, 0, 0}}, + {ECLevelL: {20, 1, 80, 0, 0}, ECLevelM: {18, 2, 32, 0, 0}, ECLevelQ: {26, 2, 24, 0, 0}, ECLevelH: {16, 4, 9, 0, 0}}, + {ECLevelL: {26, 1, 108, 0, 0}, ECLevelM: {24, 2, 43, 0, 0}, ECLevelQ: {18, 2, 15, 2, 16}, ECLevelH: {22, 2, 11, 2, 12}}, // version 5 + {ECLevelL: {18, 2, 68, 0, 0}, ECLevelM: {16, 4, 27, 0, 0}, ECLevelQ: {24, 4, 19, 0, 0}, ECLevelH: {28, 4, 15, 0, 0}}, + {ECLevelL: {20, 2, 78, 0, 0}, ECLevelM: {18, 4, 31, 0, 0}, ECLevelQ: {18, 2, 14, 4, 15}, ECLevelH: {26, 4, 13, 1, 14}}, + {ECLevelL: {24, 2, 97, 0, 0}, ECLevelM: {22, 2, 38, 2, 39}, ECLevelQ: {22, 4, 18, 2, 19}, ECLevelH: {26, 4, 14, 2, 15}}, + {ECLevelL: {30, 2, 116, 0, 0}, ECLevelM: {22, 3, 36, 2, 37}, ECLevelQ: {20, 4, 16, 4, 17}, ECLevelH: {24, 4, 12, 4, 13}}, + {ECLevelL: {18, 2, 68, 2, 69}, ECLevelM: {26, 4, 43, 1, 44}, ECLevelQ: {24, 6, 19, 2, 20}, ECLevelH: {28, 6, 15, 2, 16}}, // version 10 + {ECLevelL: {20, 4, 81, 0, 0}, ECLevelM: {31, 1, 50, 4, 51}, ECLevelQ: {28, 4, 22, 4, 23}, ECLevelH: {24, 3, 12, 8, 13}}, + {ECLevelL: {24, 2, 92, 2, 93}, ECLevelM: {22, 6, 36, 2, 37}, ECLevelQ: {26, 4, 20, 6, 21}, ECLevelH: {28, 7, 14, 4, 15}}, + {ECLevelL: {26, 4, 107, 0, 0}, ECLevelM: {22, 8, 37, 1, 38}, ECLevelQ: {24, 8, 20, 4, 21}, ECLevelH: {22, 12, 11, 4, 12}}, + {ECLevelL: {30, 3, 115, 1, 116}, ECLevelM: {24, 4, 40, 5, 41}, ECLevelQ: {20, 11, 16, 5, 17}, ECLevelH: {24, 11, 12, 5, 13}}, + {ECLevelL: {22, 5, 87, 1, 88}, ECLevelM: {24, 5, 41, 5, 42}, ECLevelQ: {30, 5, 24, 7, 25}, ECLevelH: {30, 5, 24, 7, 25}}, + {ECLevelL: {24, 5, 98, 1, 99}, ECLevelM: {28, 7, 45, 3, 46}, ECLevelQ: {24, 15, 19, 2, 20}, ECLevelH: {30, 3, 15, 13, 16}}, + {ECLevelL: {28, 1, 107, 5, 108}, ECLevelM: {28, 10, 46, 1, 47}, ECLevelQ: {28, 1, 22, 15, 23}, ECLevelH: {28, 2, 14, 17, 15}}, + {ECLevelL: {30, 5, 120, 1, 121}, ECLevelM: {26, 9, 43, 4, 44}, ECLevelQ: {28, 17, 22, 1, 23}, ECLevelH: {28, 2, 14, 19, 15}}, + {ECLevelL: {28, 3, 113, 4, 114}, ECLevelM: {26, 3, 44, 11, 45}, ECLevelQ: {26, 17, 21, 4, 22}, ECLevelH: {26, 9, 13, 16, 14}}, + {ECLevelL: {28, 3, 107, 6, 108}, ECLevelM: {26, 3, 41, 13, 42}, ECLevelQ: {30, 15, 24, 5, 25}, ECLevelH: {28, 15, 15, 10, 16}}, // version 20 + {ECLevelL: {28, 4, 116, 4, 117}, ECLevelM: {26, 17, 42, 0, 0}, ECLevelQ: {28, 17, 22, 6, 23}, ECLevelH: {30, 19, 16, 6, 17}}, + {ECLevelL: {28, 2, 111, 7, 112}, ECLevelM: {28, 17, 46, 0, 0}, ECLevelQ: {30, 7, 24, 16, 25}, ECLevelH: {24, 34, 13, 0, 0}}, + {ECLevelL: {30, 4, 121, 5, 122}, ECLevelM: {28, 4, 47, 14, 48}, ECLevelQ: {30, 11, 24, 14, 25}, ECLevelH: {30, 16, 15, 14, 16}}, + {ECLevelL: {30, 6, 117, 4, 118}, ECLevelM: {28, 6, 45, 14, 46}, ECLevelQ: {30, 11, 24, 16, 25}, ECLevelH: {30, 30, 16, 2, 17}}, + {ECLevelL: {26, 8, 106, 4, 107}, ECLevelM: {28, 8, 47, 13, 48}, ECLevelQ: {230, 7, 24, 22, 25}, ECLevelH: {30, 22, 15, 13, 16}}, + {ECLevelL: {28, 10, 114, 2, 115}, ECLevelM: {28, 19, 46, 4, 47}, ECLevelQ: {28, 28, 22, 6, 23}, ECLevelH: {30, 33, 16, 4, 17}}, + {ECLevelL: {30, 8, 122, 4, 123}, ECLevelM: {28, 22, 45, 3, 46}, ECLevelQ: {30, 8, 23, 26, 24}, ECLevelH: {30, 12, 15, 28, 16}}, + {ECLevelL: {30, 3, 117, 10, 118}, ECLevelM: {28, 3, 45, 23, 46}, ECLevelQ: {30, 4, 24, 31, 25}, ECLevelH: {30, 11, 15, 31, 16}}, + {ECLevelL: {30, 7, 116, 7, 117}, ECLevelM: {28, 21, 45, 7, 46}, ECLevelQ: {30, 1, 23, 37, 24}, ECLevelH: {30, 19, 15, 26, 16}}, + {ECLevelL: {30, 5, 115, 10, 116}, ECLevelM: {28, 19, 47, 10, 48}, ECLevelQ: {30, 15, 24, 25, 25}, ECLevelH: {30, 23, 15, 25, 16}}, // version 30 + {ECLevelL: {30, 13, 115, 3, 116}, ECLevelM: {28, 2, 46, 29, 47}, ECLevelQ: {30, 42, 24, 1, 25}, ECLevelH: {30, 23, 15, 28, 16}}, + {ECLevelL: {30, 17, 115, 0, 0}, ECLevelM: {28, 10, 46, 23, 47}, ECLevelQ: {30, 10, 24, 35, 25}, ECLevelH: {30, 19, 15, 35, 16}}, + {ECLevelL: {30, 17, 115, 1, 116}, ECLevelM: {28, 14, 46, 21, 47}, ECLevelQ: {30, 29, 24, 19, 25}, ECLevelH: {30, 11, 15, 46, 16}}, + {ECLevelL: {30, 13, 115, 6, 116}, ECLevelM: {28, 14, 46, 13, 47}, ECLevelQ: {30, 44, 24, 7, 25}, ECLevelH: {30, 59, 16, 1, 17}}, + {ECLevelL: {30, 12, 121, 7, 122}, ECLevelM: {28, 12, 47, 26, 48}, ECLevelQ: {30, 39, 24, 14, 25}, ECLevelH: {30, 22, 15, 41, 16}}, + {ECLevelL: {30, 6, 121, 14, 122}, ECLevelM: {28, 6, 47, 34, 48}, ECLevelQ: {30, 46, 24, 10, 25}, ECLevelH: {30, 2, 15, 64, 16}}, + {ECLevelL: {30, 17, 122, 4, 123}, ECLevelM: {28, 29, 46, 14, 47}, ECLevelQ: {30, 49, 24, 10, 25}, ECLevelH: {30, 24, 15, 46, 16}}, + {ECLevelL: {30, 4, 122, 18, 123}, ECLevelM: {28, 13, 46, 32, 47}, ECLevelQ: {30, 48, 24, 14, 25}, ECLevelH: {30, 42, 15, 32, 16}}, + {ECLevelL: {30, 20, 117, 4, 118}, ECLevelM: {28, 40, 47, 7, 48}, ECLevelQ: {30, 43, 24, 22, 25}, ECLevelH: {30, 10, 15, 67, 16}}, + {ECLevelL: {30, 19, 118, 6, 119}, ECLevelM: {28, 18, 47, 31, 48}, ECLevelQ: {30, 34, 24, 34, 25}, ECLevelH: {30, 20, 15, 61, 16}}, // version 40 +} + +// Galois field antilog to log convertion table +var int2exp = []int{ + 0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, + 0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71, + 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, + 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6, + 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88, + 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, + 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d, + 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57, + 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18, + 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e, + 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, + 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2, + 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, + 0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a, + 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7, + 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf, +} + +// Galois field log to antilog convertion table +var exp2int = []int{ + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, + 0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, + 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, + 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1, + 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0, + 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, + 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce, + 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc, + 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54, + 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73, + 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, + 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41, + 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, + 0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09, + 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16, + 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00, +} + +var alphanumTable = map[byte]int{ + '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19, + 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, + 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, ' ': 36, '$': 37, '%': 38, '*': 39, + '+': 40, '-': 41, '.': 42, '/': 43, ':': 44, +} + +// Errors introduced by this package +var ( + errInvalidVersion = errors.New("goqr: invalid qrcode version (1-40)") + errInvalidLevel = errors.New("goqr: invalid qrcode error correctionlevel (ECLevelL,M,Q,H)") + errInvalidModuleSize = errors.New("goqr: invalid qrcode module size (>=1)") + errInvalidQuietZoneWidth = errors.New("goqr: invalid qrcode quiet zone width (>=0)") + errInvalidDataSize = errors.New("goqr: invalid data size") +) + +type Qrcode struct { + Version int // qrcode version 1-40 (0:auto) + Level ECLevel // error correction Level (0:auto) + mode string // + module [][]int // 2-D matrix [row][col] ( +-1 : data, +-2 : pattern ) + data string // data to encode + ModuleSize int // module size (default 1) + QuietZoneWidth int // quiet zone width + encodedData []byte // encoded data + penalty []int // calculated mask penalty (0-7) +} + +func Encode(data string, version int, level ECLevel, moduleSize int) (image.Image, error) { + + qr := new(Qrcode) + qr.data = data + qr.Version = version + qr.Level = level + qr.ModuleSize = moduleSize + qr.QuietZoneWidth = 4 + + return qr.Encode() +} + +// Module count on a side +func (qr *Qrcode) len() int { return qr.Version*4 + (7+1)*2 + 1 } + +func (qr *Qrcode) Encode() (image.Image, error) { + + // check version + if qr.Version < 0 || 40 < qr.Version { + return nil, errInvalidVersion + } + + // check level + if qr.Level != 0 && len(errorCorrectionTable[1][qr.Level]) == 0 { + return nil, errInvalidLevel + } + + // check module size + if qr.ModuleSize < 1 { + return nil, errInvalidModuleSize + } + + // check quiet zone width + if qr.QuietZoneWidth < 0 { + return nil, errInvalidQuietZoneWidth + } + + // set encoding mode (kanji mode not supported) + qr.selectMode() + + // set version & level + qr.selectVersionLevel() + if qr.Version == 0 || qr.Level == 0 { + return nil, errInvalidDataSize + } + + // initialize qrcode + for i := 0; i < qr.len(); i++ { + s := make([]int, qr.len()) + qr.module = append(qr.module, s) + } + + // place pattern to qr.module + qr.placePatterns() + + // encode data -> qr.encodedData + qr.encodeData() + + // place encoded data to qr.module + qr.mapData() + + // mask + qr.maskData() + + // [][]int to [][]bool + module := make([][]bool, 0) + for _, row := range qr.module { + r := make([]bool, qr.len()) + for c, cell := range row { + if cell > 0 { + r[c] = true + } else { + r[c] = false + } + } + module = append(module, r) + } + + // quiet zone + for i := 0; i < qr.QuietZoneWidth; i++ { + module = append(module, make([]bool, qr.len())) + module = append([][]bool{make([]bool, qr.len())}, module...) + } + for i, row := range module { + row = append(make([]bool, qr.QuietZoneWidth), row...) + row = append(row, make([]bool, qr.QuietZoneWidth)...) + module[i] = row + } + + // module size + if qr.ModuleSize > 1 { + l := len(module) * qr.ModuleSize + m := make([][]bool, l) + + for i := 0; i < l; i++ { + m[i] = make([]bool, l) + } + + for r, row := range module { + for c, cell := range row { + if !cell { + continue + } + for x := 0; x < qr.ModuleSize; x++ { + for y := 0; y < qr.ModuleSize; y++ { + m[r*qr.ModuleSize+x][c*qr.ModuleSize+y] = true + } + } + } + } + module = m + } + + rgba := image.NewRGBA(image.Rect(0, 0, len(module), len(module))) + + for r, row := range module { + for c, cell := range row { + if cell { + rgba.SetRGBA(c, r, color.RGBA{A: 255, R: 0, G: 0, B: 0}) + } else { + rgba.SetRGBA(c, r, color.RGBA{}) + } + } + } + + return rgba, nil +} + +// qrcode version information table +func (qr *Qrcode) table() []int { return errorCorrectionTable[qr.Version][qr.Level] } +func (qr *Qrcode) ecCodeWords() int { return qr.table()[0] } +func (qr *Qrcode) dataCodeWords() []int { return []int{qr.table()[2], qr.table()[4]} } +func (qr *Qrcode) blkCount() []int { return []int{qr.table()[1], qr.table()[3]} } + +// total code words +func (qr *Qrcode) totalCodeWords() int { + table := qr.table() + return (table[0]+table[2])*table[1] + (table[0]+table[4])*table[3] +} + +// total data code words +func (qr *Qrcode) totalDataCodeWords() int { + table := qr.table() + return table[1]*table[2] + table[3]*table[4] +} + +// total data code bits +func (qr *Qrcode) totalDataCodeBits() int { + return qr.totalDataCodeWords() * 8 +} + +func (qr *Qrcode) selectMode() { + qr.mode = "numeric" + for _, c := range qr.data { + if c >= '0' && c <= '9' { + continue + } + if alphanumTable[byte(c)] != 0 { + qr.mode = "alphanum" + continue + } + qr.mode = "8bitbyte" + return + } +} + +func (qr *Qrcode) selectVersionLevel() { + firstVer, lastVer := 1, 40 + errorCorrectionLevel := []ECLevel{ECLevelH, ECLevelQ, ECLevelM, ECLevelL} + + if qr.Version > 0 { + firstVer, lastVer = qr.Version, qr.Version + } + + if qr.Level > 0 { + errorCorrectionLevel = []ECLevel{qr.Level} + } + + for i := firstVer; i <= lastVer; i++ { + for _, j := range errorCorrectionLevel { + if maxDataSize(i, j, qr.mode) >= len(qr.data) { + qr.Version, qr.Level = i, j + return + } + } + } + qr.Version, qr.Level = 0, 0 +} + +func (qr *Qrcode) setTypeBits(mask int) { + qr.module = setTypeBits(qr.module, qr.Level, mask) +} + +func (qr *Qrcode) placePatterns() { + + isBlank := func(r, c int) bool { return qr.module[r][c] == 0 } + + // finder pattern + for _, p := range [3][2]int{{0, 0}, {qr.len() - 7, 0}, {0, qr.len() - 7}} { + for r := -1; r <= 7; r++ { + if p[0]+r < 0 || qr.len() <= p[0]+r { + continue + } + for c := -1; c <= 7; c++ { + if p[1]+c < 0 || qr.len() <= p[1]+c { + continue + } + if (0 <= r && r < 7 && (c == 0 || c == 6)) || + (0 <= c && c < 7 && (r == 0 || r == 6)) || + (2 <= r && r < 5 && 2 <= c && c < 5) { + qr.module[p[0]+r][p[1]+c] = 2 + } else { + qr.module[p[0]+r][p[1]+c] = -2 + } + } + } + } + + // alignment pattern + pat := positionAdjustPatternTable[qr.Version] + + for i := 0; i < len(pat); i++ { + for j := 0; j < len(pat); j++ { + row, col := pat[i], pat[j] + if !isBlank(row, col) { + continue + } + + for r := -2; r <= 2; r++ { + for c := -2; c <= 2; c++ { + if r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0) { + qr.module[row+r][col+c] = 2 + } else { + qr.module[row+r][col+c] = -2 + } + } + } + } + } + + // timing pattern + for i := 0; i < qr.len(); i++ { + if !isBlank(i, 6) { + continue + } + if i%2 == 0 { + qr.module[i][6] = -2 + } else { + qr.module[i][6] = 2 + } + } + for i := 0; i < qr.len(); i++ { + if !isBlank(6, i) { + continue + } + if i%2 == 0 { + qr.module[6][i] = -2 + } else { + qr.module[6][i] = 2 + } + } + + // version information + if qr.Version >= 7 { + versionBits := versionInformationTable[qr.Version-7] + + for j := 0; j < 6; j++ { + for k := qr.len() - 11; k < qr.len()-8; k++ { + if (versionBits & 1) > 0 { + qr.module[k][j] = 2 + qr.module[j][k] = 2 + } else { + qr.module[k][j] = -2 + qr.module[j][k] = -2 + } + versionBits = versionBits >> 1 + } + } + } + + // single bit + qr.module[qr.len()-8][8] = 2 + + // dummy format infomation (mask v0) + qr.setTypeBits(0) + +} + +func (qr *Qrcode) mapData() { + + // byte slice to bit slice + bitbuf := new(bitBuffer) + for _, b := range qr.encodedData { + bitbuf.appendByte(b) + } + + // reminder bits + switch qr.Version { + case 2, 3, 4, 5, 6: + bitbuf.append(0, 7) + case 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34: + bitbuf.append(0, 3) + case 21, 22, 23, 24, 25, 26, 27: + bitbuf.append(0, 4) + } + + r := qr.len() - 1 // row + c := qr.len() - 1 // column + v := 1 // direction 1:up, -1:down + h := 1 // 1:right, -1:left + i := 0 // index + + for { + if qr.module[r][c] == 0 { + if bitbuf.get(i) == true { + qr.module[r][c] = 1 + } else { + qr.module[r][c] = -1 + } + i++ + } + if i >= bitbuf.len() { + break + } + if c == 6 { // skip + c-- + h = 1 + } else if h == 1 { // right + if c != 0 { + c-- + h *= -1 + } + } else { // left + if (v > 0 && r == 0) || (v < 0 && r == qr.len()-1) { + v *= -1 + c-- + h *= -1 + } else { + c++ + h *= -1 + r -= v + } + } + } +} + +func (qr *Qrcode) maskData() { + + qr.penalty = make([]int, 8) + c := make(chan int, 8) // channel + + for i := 0; i < 8; i++ { + // Penalty + go qr.calcPenalty(i, c) + } + + for k := 0; k < 8; k++ { + <-c + } + + mask := 0 + min := qr.penalty[0] + + for i := 1; i < 8; i++ { + if qr.penalty[i] < min { + min = qr.penalty[i] + mask = i + } + } + + qr.setTypeBits(mask) + qr.module = maskData(mask, qr.module) +} + +func (qr *Qrcode) errorCorrectionCode(data []byte, block, blockIndex int, c chan int) { + + eccw := qr.ecCodeWords() // ec code word per block + totalBlkCount := qr.blkCount()[0] + qr.blkCount()[1] // total blocks + dcw := qr.totalDataCodeWords() // total data code words + + // message polynominal + msg := polynominal{} + for i := 0; i < len(data); i++ { + x := len(data) + eccw - 1 - i + msg[x] = int2exp[data[i]] + } + + // generator polynominal + gen := polynominal{0: 0} + for i := 0; i < eccw; i++ { + p := polynominal{1: 0, 0: i} + gen = gen.multiply(p) + } + + // error collection code words + for i := len(data) - 1; i >= 0; i-- { + p := polynominal{i: msg[i+eccw]} + g := gen.multiply(p) + msg = msg.xor(g) + delete(msg, i+eccw) + } + + ec := make([]byte, eccw) + for i := 0; i < eccw; i++ { + ec[i] = byte(exp2int[msg[eccw-1-i]]) + } + + for i, v := range data { + j := blockIndex + i*totalBlkCount + if j >= dcw { + j -= qr.blkCount()[0] + } + qr.encodedData[j] = v + } + + for i, v := range ec { + qr.encodedData[dcw+blockIndex+i*totalBlkCount] = v + } + + c <- 1 +} + +func (qr *Qrcode) encodeData() { + + // total data code words + dcw := qr.totalDataCodeWords() + + // total code words + cw := qr.totalCodeWords() + + // total blocks + totalBlkCount := qr.blkCount()[0] + qr.blkCount()[1] + + // encode data + var bytebuf *bytes.Buffer + switch qr.mode { + case "numeric": + bytebuf = bytes.NewBuffer(qr.encodeNumeric()) + case "alphanum": + bytebuf = bytes.NewBuffer(qr.encodeAlphanum()) + default: // default + bytebuf = bytes.NewBuffer(qr.encodeByte()) + } + + // pad codeword + for bytebuf.Len() < dcw { + bytebuf.WriteByte(0xEC) + if bytebuf.Len() < dcw { + bytebuf.WriteByte(0x11) + } + } + + // 1. split encoded data into blocks + // 2. generate error correction code for each block + // 3. reallocate data + qr.encodedData = make([]byte, cw) + + // goroutine + c := make(chan int, totalBlkCount) + + for i, k := 0, 0; i < 2; i++ { // rs block 1, 2 + for j := 0; j < qr.blkCount()[i]; j, k = j+1, k+1 { + go qr.errorCorrectionCode(bytebuf.Next(qr.dataCodeWords()[i]), i, k, c) + } + } + + // wait + for k := 0; k < totalBlkCount; k++ { + <-c + } +} + +// encode data ( numeric mode ) +func (qr *Qrcode) encodeNumeric() []byte { + + // working bit array + bitbuf := new(bitBuffer) + + // mode indicator + bitbuf.append(1, 4) + + // character count indicator + switch { + case qr.Version <= 9: + bitbuf.append(len(qr.data), 10) + case qr.Version <= 26: + bitbuf.append(len(qr.data), 12) + default: + bitbuf.append(len(qr.data), 14) + } + + // string to bitstream + bytebuf := bytes.NewBufferString(qr.data) + for b := bytebuf.Next(3); len(b) > 0; b = bytebuf.Next(3) { + switch len(b) { + case 2: + n, _ := strconv.Atoi(string(b)) + bitbuf.append(n, 7) + case 1: + n, _ := strconv.Atoi(string(b)) + bitbuf.append(n, 4) + default: // 3 + n, _ := strconv.Atoi(string(b)) + bitbuf.append(n, 10) + } + } + + // terminator + for i := 0; i < 4 && bitbuf.len() < qr.totalDataCodeBits(); i++ { + bitbuf.append(0, 1) + } + + return bitbuf.bytes() + +} + +// encode data ( alphanumeric mode ) +func (qr *Qrcode) encodeAlphanum() []byte { + + // working bit array + bitbuf := new(bitBuffer) + + // mode indicator + bitbuf.append(1<<1, 4) + + // character count indicator + switch { + case qr.Version <= 9: + bitbuf.append(len(qr.data), 9) + case qr.Version <= 26: + bitbuf.append(len(qr.data), 11) + default: + bitbuf.append(len(qr.data), 13) + } + + // string to bitstream + bytebuf := bytes.NewBufferString(qr.data) + for b := bytebuf.Next(2); len(b) > 0; b = bytebuf.Next(2) { + switch len(b) { + case 1: + bitbuf.append(alphanumTable[b[0]], 6) + default: // 2 + bitbuf.append(alphanumTable[b[0]]*45+alphanumTable[b[1]], 11) + } + } + + // terminator + for i := 0; i < 4 && bitbuf.len() < qr.totalDataCodeBits(); i++ { + bitbuf.append(0, 1) + } + + return bitbuf.bytes() +} + +// encode data ( 8bitbyte mode ) +func (qr *Qrcode) encodeByte() []byte { + + // working bit array + bitbuf := new(bitBuffer) + + // mode indicator + bitbuf.append(1<<2, 4) + + // character count indicator + switch { + case qr.Version <= 9: + bitbuf.append(len(qr.data), 8) + case qr.Version <= 26: + bitbuf.append(len(qr.data), 16) + default: + bitbuf.append(len(qr.data), 16) + } + + // string to bitstream + bytebuf := bytes.NewBufferString(qr.data) + for c, e := bytebuf.ReadByte(); e == nil; c, e = bytebuf.ReadByte() { + bitbuf.appendByte(c) + } + + // terminator + bitbuf.append(0, 4) + + return bitbuf.bytes() +} + +func (qr *Qrcode) calcPenalty(mask int, c chan int) { + + penalty := 0 + + module := maskData(mask, setTypeBits(qr.module, qr.Level, mask)) + + isColored := func(r, c int) bool { return module[r][c] > 0 } + + // Rule #1: five or more same colored pixels + for r := 0; r < qr.len(); r++ { // horizontal scan + for i := 0; i < qr.len()-1; i++ { + for j := 1; ; j++ { + if !(j < qr.len()-i && + isColored(r, i) == isColored(r, i+j)) { + i += j - 1 + break + } + if j == 4 { + penalty += 3 + } + if j > 4 { + penalty++ + } + } + } + } + + for c := 0; c < qr.len(); c++ { // vertical scan + for i := 0; i < qr.len()-1; i++ { + for j := 1; ; j++ { + if !(j < qr.len()-i && + isColored(i, c) == isColored(i+j, c)) { + i += j - 1 + break + } + if j == 4 { + penalty += 3 + } + if j > 4 { + penalty++ + } + } + } + } + + // Rule #2: 2x2 block of same-colored pixels + for r := 0; r < qr.len()-1; r++ { + for c := 0; c < qr.len()-1; c++ { + if isColored(r, c) == isColored(r+1, c) && + isColored(r, c) == isColored(r, c+1) && + isColored(r, c) == isColored(r+1, c+1) { + penalty += 3 + } + } + } + + // Rule #3: o-x-o-o-o-x-o that have four light pixels on either or both sides + for r := 0; r < qr.len(); r++ { + for c := 0; c < qr.len()-6; c++ { + if isColored(r, c) && !isColored(r, c+1) && isColored(r, c+2) && + isColored(r, c+3) && isColored(r, c+4) && !isColored(r, c+5) && + isColored(r, c+6) { + if c < qr.len()-10 && + !isColored(r, c+7) && !isColored(r, c+8) && + !isColored(r, c+9) && !isColored(r, c+10) { + penalty += 40 + } else if c >= 4 && + !isColored(r, c-1) && !isColored(r, c-2) && + !isColored(r, c-3) && !isColored(r, c-4) { + penalty += 40 + } + c += 4 + } + } + } + + for c := 0; c < qr.len(); c++ { + for r := 0; r < qr.len()-6; r++ { + if isColored(r, c) && !isColored(r+1, c) && isColored(r+2, c) && + isColored(r+3, c) && isColored(r+4, c) && !isColored(r+5, c) && + isColored(r+6, c) { + if r < qr.len()-10 && + !isColored(r+7, c) && !isColored(r+8, c) && + !isColored(r+9, c) && !isColored(r+10, c) { + penalty += 40 + } else if r >= 4 && + !isColored(r-1, c) && !isColored(r-2, c) && + !isColored(r-3, c) && !isColored(r-4, c) { + penalty += 40 + } + r += 4 + } + } + } + + // Rule #4: Color Ratio + total := 0.0 + black := 0.0 + + for r, row := range module { + for c := range row { + if isColored(r, c) { + black++ + } + total++ + } + } + penalty += int(float64(int(math.Abs(black/total*100-50))) / 5 * 10) + + qr.penalty[mask] = penalty + + c <- 1 +} + +func maskData(mask int, data [][]int) [][]int { + + // mask 0-7 + maskFuncs := map[int]func(int, int) bool{ + 0: func(row, col int) bool { return (row+col)%2 == 0 }, + 1: func(row, col int) bool { return row%2 == 0 }, + 2: func(row, col int) bool { return col%3 == 0 }, + 3: func(row, col int) bool { return (row+col)%3 == 0 }, + 4: func(row, col int) bool { return (row/2+col/3)%2 == 0 }, + 5: func(row, col int) bool { return row*col%2+row*col%3 == 0 }, + 6: func(row, col int) bool { return (row*col%2+row*col%3)%2 == 0 }, + 7: func(row, col int) bool { return (row*col%3+(row+col)%2)%2 == 0 }, + } + + // copy + module := make([][]int, 0) + for _, row := range data { + c := make([]int, len(row)) + copy(c, row) + module = append(module, c) + } + + for r, row := range module { + for c := range row { + if !(module[r][c] == 1 || module[r][c] == -1) { + continue + } + if maskFuncs[mask](r, c) { + module[r][c] *= -1 + } + } + } + return module +} + +func setTypeBits(data [][]int, level ECLevel, mask int) [][]int { + + module := make([][]int, 0) + for _, row := range data { + c := make([]int, len(row)) + copy(c, row) + module = append(module, c) + } + + bits := new(bitBuffer) + bits.append(typeInformationTable[level][mask], 15) + + length := len(module) + + for i, b := range *bits { + bit := 2 + if b { + bit = 2 + } else { + bit = -2 + } + + switch { + case i < 6: + module[8][i] = bit + module[length-1-i][8] = bit + case i == 6: + module[8][i+1] = bit + module[length-1-i][8] = bit + case i == 7: + module[8][length-15+i] = bit + module[8][8] = bit + case i == 8: + module[8][length-15+i] = bit + module[7][8] = bit + case i == 9: + module[8][length-15+i] = bit + module[5][8] = bit + case i > 9: + module[8][length-15+i] = bit + module[(7*2)-i][8] = bit + } + } + return module +} + +func maxDataSize(version int, level ECLevel, mode string) (max int) { + + table := errorCorrectionTable[version][level] + dcw := table[1]*table[2] + table[3]*table[4] + + switch mode { + case "numeric": + bitsCount := dcw*8 - 4 // mode bits(4 bits) + + switch { // data length bits + case version <= 9: + bitsCount -= 10 + case version <= 26: + bitsCount -= 12 + default: + bitsCount -= 14 + } + + max = bitsCount / 10 * 3 // 10bits for 3 chars + if bitsCount%10 >= 7 { // 7bits for 2 chars + max += 2 + } else if bitsCount%10 >= 4 { // 4bits for 1 chars + max++ + } + case "alphanum": + bitsCount := dcw*8 - 4 // mode bits(4 bits) + + switch { // data length bits + case version <= 9: + bitsCount -= 9 + case version <= 26: + bitsCount -= 11 + default: + bitsCount -= 13 + } + + max = bitsCount / 11 * 2 // 11bits for 2 characters + if bitsCount%11 >= 6 { + max++ + } + case "8bitbyte": + max = dcw - 1 // mode(4bit) + terminal(4bit) = 1byte + + switch { // data length bits + case version <= 9: + max -= 1 + default: + max -= 2 + } + } + return +} + +// bitBuffer +type bitBuffer []bool + +func (p *bitBuffer) len() int { + return len(*p) +} + +func (p *bitBuffer) append(c, l int) { + for i := 0; i < l; i++ { + if c&(1< 0 { + *p = append(*p, true) + } else { + *p = append(*p, false) + } + } +} + +func (p *bitBuffer) get(i int) bool { + return (*p)[i] +} + +func (p *bitBuffer) appendByte(b byte) { + p.append(int(b), 8) +} + +func (p *bitBuffer) bytes() []byte { + + // copy + s := make(bitBuffer, p.len()) + copy(s, *p) + + for s.len()%8 != 0 { + s.append(0, 1) + } + + l := len(s) / 8 + bytebuf := bytes.NewBuffer([]byte{}) + + for i := 0; i < l; i++ { + bits := s[(i * 8):(i*8 + 8)] + c := byte(0) + for j := 0; j < 8; j++ { + if bits[j] { + c += 1 << uint(8-j-1) + } + } + bytebuf.WriteByte(c) + } + return bytebuf.Bytes() +} + +// polynominal ( Galois field 256 ) +// used to calculate error correction code +// (a^x)*(x^25) + (a^y)*(x^24) + (a^z)*(x^23) + .... => {25:x,24:y,23:z,.....} +type polynominal map[int]int + +func (p0 polynominal) multiply(p1 polynominal) polynominal { + poly := polynominal{} + for x1, a1 := range p1 { + for x0, a0 := range p0 { + x := x1 + x0 + for x >= 255 { + x -= 255 + x = (x >> 8) + (x & 255) + } + a := a1 + a0 + for a >= 255 { + a -= 255 + a = (a >> 8) + (a & 255) + } + if poly[x] == 0 { + poly[x] = exp2int[a] + } else { + anti := poly[x] ^ exp2int[a] + poly[x] = anti + } + } + } + for x, a := range poly { + poly[x] = int2exp[a] + } + return poly +} + +func (p0 polynominal) xor(p1 polynominal) polynominal { + poly := polynominal{} + for x0, a0 := range p0 { + poly[x0] = exp2int[a0] + } + for x1, a1 := range p1 { + if poly[x1] == 0 { + poly[x1] = exp2int[a1] + } else { + poly[x1] = poly[x1] ^ exp2int[a1] + } + } + for x, a := range poly { + poly[x] = int2exp[a] + } + return poly +} diff --git a/pkg/qu/LICENSE b/pkg/qu/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/qu/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/qu/README.md b/pkg/qu/README.md new file mode 100644 index 0000000..c961883 --- /dev/null +++ b/pkg/qu/README.md @@ -0,0 +1,8 @@ +# qu +### observable signal channels + +This is a wrapper around `chan struct{}` that forgives some common mistakes +like sending on closed channels and closing cllosed channels, as well as +printing logs about when channels are created, waited on, sent to and closed. + +This library makes debugging concurrent code a lot easier. IMHO. YMMV. diff --git a/pkg/qu/log.go b/pkg/qu/log.go new file mode 100644 index 0000000..e53aec6 --- /dev/null +++ b/pkg/qu/log.go @@ -0,0 +1,44 @@ +package qu + +import ( + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, _T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/qu/quit.go b/pkg/qu/quit.go new file mode 100644 index 0000000..3a3c679 --- /dev/null +++ b/pkg/qu/quit.go @@ -0,0 +1,178 @@ +package qu + +import ( + "strings" + "sync" + "time" + + "github.com/p9c/p9/pkg/log" + "go.uber.org/atomic" +) + +// C is your basic empty struct signalling channel +type C chan struct{} + +var ( + createdList []string + createdChannels []C + mx sync.Mutex + logEnabled = atomic.NewBool(false) +) + +// SetLogging switches on and off the channel logging +func SetLogging(on bool) { + logEnabled.Store(on) +} + +func l(a ...interface{}) { + if logEnabled.Load() { + D.Ln(a...) + } +} + +// T creates an unbuffered chan struct{} for trigger and quit signalling (momentary and breaker switches) +func T() C { + mx.Lock() + defer mx.Unlock() + msg := log.Caller("chan from", 1) + l("created", msg) + createdList = append(createdList, msg) + o := make(C) + createdChannels = append(createdChannels, o) + return o +} + +// Ts creates a buffered chan struct{} which is specifically intended for signalling without blocking, generally one is +// the size of buffer to be used, though there might be conceivable cases where the channel should accept more signals +// without blocking the caller +func Ts(n int) C { + mx.Lock() + defer mx.Unlock() + msg := log.Caller("buffered chan from", 1) + l("created", msg) + createdList = append(createdList, msg) + o := make(C, n) + createdChannels = append(createdChannels, o) + return o +} + +// Q closes the channel, which makes it emit a nil every time it is selected +func (c C) Q() { + l(func() (o string) { + loc := getLocForChan(c) + mx.Lock() + defer mx.Unlock() + if !testChanIsClosed(c) { + close(c) + return "closing chan from " + loc + log.Caller("\n"+strings.Repeat(" ", 48)+"from", 1) + } else { + return "from" + log.Caller("", 1) + "\n" + strings.Repeat(" ", 48) + + "channel " + loc + " was already closed" + } + }(), + ) +} + +// Signal sends struct{}{} on the channel which functions as a momentary switch, useful in pairs for stop/start +func (c C) Signal() { + l(func() (o string) { return "signalling " + getLocForChan(c) }()) + c <- struct{}{} +} + +// Wait should be placed with a `<-` in a select case in addition to the channel variable name +func (c C) Wait() <-chan struct{} { + l(func() (o string) { return "waiting on " + getLocForChan(c) + log.Caller("at", 1) }()) + return c +} + +// testChanIsClosed allows you to see whether the channel has been closed so you can avoid a panic by trying to close or +// signal on it +func testChanIsClosed(ch C) (o bool) { + if ch == nil { + return true + } + select { + case <-ch: + o = true + default: + } + return +} + +// getLocForChan finds which record connects to the channel in question +func getLocForChan(c C) (s string) { + s = "not found" + mx.Lock() + for i := range createdList { + if i >= len(createdChannels) { + break + } + if createdChannels[i] == c { + s = createdList[i] + } + } + mx.Unlock() + return +} + +// once a minute clean up the channel cache to remove closed channels no longer in use +func init() { + go func() { + for { + <-time.After(time.Minute) + D.Ln("cleaning up closed channels") + var c []C + var ll []string + mx.Lock() + for i := range createdChannels { + if i >= len(createdList) { + break + } + if testChanIsClosed(createdChannels[i]) { + } else { + c = append(c, createdChannels[i]) + ll = append(ll, createdList[i]) + } + } + createdChannels = c + createdList = ll + mx.Unlock() + } + }() +} + +// PrintChanState creates an output showing the current state of the channels being monitored +// This is a function for use by the programmer while debugging +func PrintChanState() { + mx.Lock() + for i := range createdChannels { + if i >= len(createdList) { + break + } + if testChanIsClosed(createdChannels[i]) { + _T.Ln(">>> closed", createdList[i]) + } else { + _T.Ln("<<< open", createdList[i]) + } + } + mx.Unlock() +} + +// GetOpenChanCount returns the number of qu channels that are still open +// todo: this needs to only apply to unbuffered type +func GetOpenChanCount() (o int) { + mx.Lock() + var c int + for i := range createdChannels { + if i >= len(createdChannels) { + break + } + if testChanIsClosed(createdChannels[i]) { + c++ + } else { + o++ + } + } + mx.Unlock() + return +} diff --git a/pkg/ring/entry.go b/pkg/ring/entry.go new file mode 100644 index 0000000..6a79f34 --- /dev/null +++ b/pkg/ring/entry.go @@ -0,0 +1,167 @@ +package ring + +import ( + "context" + "github.com/p9c/p9/pkg/log" + + "github.com/marusama/semaphore" +) + +type Entry struct { + Sem semaphore.Semaphore + Buf []*log.Entry + Cursor int + Full bool + Clicked int + // Buttons []gel.Button + // Hiders []gel.Button +} + +// NewEntry creates a new entry ring buffer +func NewEntry(size int) *Entry { + return &Entry{ + Sem: semaphore.New(1), + Buf: make([]*log.Entry, size), + Cursor: 0, + Clicked: -1, + // Buttons: make([]gel.Button, size), + // Hiders: make([]gel.Button, size), + } +} + +// Clear sets the buffer back to initial state +func (b *Entry) Clear() { + var e error + if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { + defer b.Sem.Release(1) + b.Cursor = 0 + b.Clicked = -1 + b.Full = false + } +} + +// Len returns the length of the buffer +func (b *Entry) Len() (out int) { + var e error + if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { + defer b.Sem.Release(1) + if b.Full { + out = len(b.Buf) + } else { + out = b.Cursor + } + } + return +} + +// Get returns the value at the given index or nil if nothing +func (b *Entry) Get(i int) (out *log.Entry) { + var e error + if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { + defer b.Sem.Release(1) + bl := len(b.Buf) + cursor := i + if i < bl { + if b.Full { + cursor = i + b.Cursor + if cursor >= bl { + cursor -= bl + } + } + // D.Ln("get entry", i, "len", bl, "cursor", b.Cursor, "position", + // cursor) + out = b.Buf[cursor] + } + } + return +} + +// +// // GetButton returns the gel.Button of the entry +// func (b *Entry) GetButton(i int) (out *gel.Button) { +// if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { +// defer b.Sem.Release(1) +// bl := len(b.Buf) +// cursor := i +// if i < bl { +// if b.Full { +// cursor = i + b.Cursor +// if cursor >= bl { +// cursor -= bl +// } +// } +// // D.Ln("get entry", i, "len", bl, "cursor", b.Cursor, "position", +// // cursor) +// out = &b.Buttons[cursor] +// } +// } +// return +// } +// +// // GetHider returns the gel.Button of the entry +// func (b *Entry) GetHider(i int) (out *gel.Button) { +// if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { +// defer b.Sem.Release(1) +// bl := len(b.Buf) +// cursor := i +// if i < bl { +// if b.Full { +// cursor = i + b.Cursor +// if cursor >= bl { +// cursor -= bl +// } +// } +// // D.Ln("get entry", i, "len", bl, "cursor", b.Cursor, "position", +// // cursor) +// out = &b.Hiders[cursor] +// } +// } +// return +// } + +func (b *Entry) Add(value *log.Entry) { + var e error + if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { + defer b.Sem.Release(1) + if b.Cursor == len(b.Buf) { + b.Cursor = 0 + if !b.Full { + b.Full = true + } + } + b.Buf[b.Cursor] = value + b.Cursor++ + } +} + +func (b *Entry) ForEach(fn func(v *log.Entry) error) (e error) { + if e = b.Sem.Acquire(context.Background(), 1); !E.Chk(e) { + c := b.Cursor + i := c + 1 + if i == len(b.Buf) { + // D.Ln("hit the end") + i = 0 + } + if !b.Full { + // D.Ln("buffer not yet full") + i = 0 + } + // D.Ln(b.Buf) + for ; ; i++ { + if i == len(b.Buf) { + // D.Ln("passed the end") + i = 0 + } + if i == c { + // D.Ln("reached cursor again") + break + } + // D.Ln(i, b.Cursor) + if e = fn(b.Buf[i]); E.Chk(e) { + break + } + } + b.Sem.Release(1) + } + return +} diff --git a/pkg/ring/float64.go b/pkg/ring/float64.go new file mode 100644 index 0000000..c531236 --- /dev/null +++ b/pkg/ring/float64.go @@ -0,0 +1,80 @@ +package ring + +type BufferFloat64 struct { + Buf []float64 + Cursor int + Full bool +} + +// NewBufferFloat64 creates a new ring buffer of float64 values +func NewBufferFloat64(size int) *BufferFloat64 { + return &BufferFloat64{ + Buf: make([]float64, size), + Cursor: -1, + } +} + +// Get returns the value at the given index or nil if nothing +func (b *BufferFloat64) Get(index int) (out *float64) { + bl := len(b.Buf) + if index < bl { + cursor := b.Cursor + index + if cursor > bl { + cursor = cursor - bl + } + return &b.Buf[cursor] + } + return +} + +// Len returns the length of the buffer, which grows until it fills, after which +// this will always return the size of the buffer +func (b *BufferFloat64) Len() (length int) { + if b.Full { + return len(b.Buf) + } + return b.Cursor +} + +// Add a new value to the cursor position of the ring buffer +func (b *BufferFloat64) Add(value float64) { + b.Cursor++ + if b.Cursor == len(b.Buf) { + b.Cursor = 0 + if !b.Full { + b.Full = true + } + } + b.Buf[b.Cursor] = value +} + +// ForEach is an iterator that can be used to process every element in the +// buffer +func (b *BufferFloat64) ForEach(fn func(v float64) error) (e error) { + c := b.Cursor + i := c + 1 + if i == len(b.Buf) { + // D.Ln("hit the end") + i = 0 + } + if !b.Full { + // D.Ln("buffer not yet full") + i = 0 + } + // D.Ln(b.Buf) + for ; ; i++ { + if i == len(b.Buf) { + // D.Ln("passed the end") + i = 0 + } + if i == c { + // D.Ln("reached cursor again") + break + } + // D.Ln(i, b.Cursor) + if e = fn(b.Buf[i]); e != nil { + break + } + } + return +} diff --git a/pkg/ring/log.go b/pkg/ring/log.go new file mode 100644 index 0000000..dfe3b6e --- /dev/null +++ b/pkg/ring/log.go @@ -0,0 +1,43 @@ +package ring + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/ring/uint64.go b/pkg/ring/uint64.go new file mode 100644 index 0000000..531b51c --- /dev/null +++ b/pkg/ring/uint64.go @@ -0,0 +1,67 @@ +package ring + +type BufferUint64 struct { + Buf []uint64 + Cursor int + Full bool +} + +func NewBufferUint64(size int) *BufferUint64 { + return &BufferUint64{ + Buf: make([]uint64, size), + Cursor: -1, + } +} + +// Get returns the value at the given index or nil if nothing +func (b *BufferUint64) Get(index int) (out *uint64) { + bl := len(b.Buf) + if index < bl { + cursor := b.Cursor + index + if cursor > bl { + cursor = cursor - bl + } + return &b.Buf[cursor] + } + return +} + +func (b *BufferUint64) Add(value uint64) { + b.Cursor++ + if b.Cursor == len(b.Buf) { + b.Cursor = 0 + if !b.Full { + b.Full = true + } + } + b.Buf[b.Cursor] = value +} + +func (b *BufferUint64) ForEach(fn func(v uint64) error) (e error) { + c := b.Cursor + i := c + 1 + if i == len(b.Buf) { + // D.Ln("hit the end") + i = 0 + } + if !b.Full { + // D.Ln("buffer not yet full") + i = 0 + } + // D.Ln(b.Buf) + for ; ; i++ { + if i == len(b.Buf) { + // D.Ln("passed the end") + i = 0 + } + if i == c { + // D.Ln("reached cursor again") + break + } + // D.Ln(i, b.Cursor) + if e = fn(b.Buf[i]); E.Chk(e) { + break + } + } + return +} diff --git a/pkg/rpcclient/CONTRIBUTORS b/pkg/rpcclient/CONTRIBUTORS new file mode 100755 index 0000000..4c5763d --- /dev/null +++ b/pkg/rpcclient/CONTRIBUTORS @@ -0,0 +1,13 @@ +# This is the list of people who have contributed code to the repository. +# +# Names should be added to this file only after verifying that the individual +# or the individual's organization has agreed to the LICENSE. +# +# Names should be added to this file like so: +# Name +Dave Collins +Geert-Johan Riemer +Josh Rickmar +Michalis Kargakis +Ruben de Vries \ No newline at end of file diff --git a/pkg/rpcclient/README.md b/pkg/rpcclient/README.md new file mode 100755 index 0000000..d63a8ec --- /dev/null +++ b/pkg/rpcclient/README.md @@ -0,0 +1,66 @@ +# rpcclient + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/rpcclient) + +rpcclient implements a Websocket-enabled Bitcoin JSON-RPC client package written +in [Go](http://golang.org/). It provides a robust and easy to use client for +interfacing with a Bitcoin RPC server that uses a pod/bitcoin core compatible +Bitcoin JSON-RPC API. + +## Status + +This package is currently under active development. It is already stable and the +infrastructure is complete. However, there are still several RPCs left to +implement and the API is not stable yet. + +## Documentation + +- [API Reference](http://godoc.org/github.com/p9c/p9/rpcclient) + +- [pod Websockets Example](https://github.com/p9c/p9/tree/master/rpcclient/examples/podwebsockets) + Connects to a pod RPC server using TLS-secured websockets, registers for block + connected and block disconnected notifications, and gets the current block + count + +- [btcwallet Websockets Example](https://github.com/p9c/p9/tree/master/rpcclient/examples/btcwalletwebsockets) + Connects to a btcwallet RPC server using TLS-secured websockets, registers for + notifications about changes to account balances, and gets a list of unspent + transaction outputs (utxos) the wallet can sign + +- [Bitcoin Core HTTP POST Example](https://github.com/p9c/p9/tree/master/rpcclient/examples/bitcoincorehttp) + Connects to a bitcoin core RPC server using HTTP POST mode with TLS disabled + and gets the current block count + +## Major Features + +- Supports Websockets (pod/btcwallet) and HTTP POST mode (bitcoin core) + +- Provides callback and registration functions for pod/btcwallet notifications + +- Supports pod extensions + +- Translates to and from higher-level and easier to use Go types + +- Offers a synchronous (blocking) and asynchronous API + +- When running in Websockets mode (the default): + + - Automatic reconnect handling (can be disabled) + + - Outstanding commands are automatically reissued + + - Registered notifications are automatically reregistered + + - Back-off support on reconnect attempts + +## Installation + +```bash +$ go get -u github.com/p9c/p9/rpcclient +``` + +## License + +Package rpcclient is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/rpcclient/chain.go b/pkg/rpcclient/chain.go new file mode 100644 index 0000000..dd480c5 --- /dev/null +++ b/pkg/rpcclient/chain.go @@ -0,0 +1,774 @@ +package rpcclient + +import ( + "bytes" + "encoding/hex" + js "encoding/json" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// FutureGetBestBlockHashResult is a future promise to deliver the result of a GetBestBlockAsync RPC invocation (or an +// applicable error). +type FutureGetBestBlockHashResult chan *response + +// Receive waits for the response promised by the future and returns the hash of the best block in the longest block +// chain. +func (r FutureGetBestBlockHashResult) Receive() (*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var txHashStr string + e = js.Unmarshal(res, &txHashStr) + if e != nil { + return nil, e + } + return chainhash.NewHashFromStr(txHashStr) +} + +// GetBestBlockHashAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetBestBlockHash for the blocking version and more +// details. +func (c *Client) GetBestBlockHashAsync() FutureGetBestBlockHashResult { + cmd := btcjson.NewGetBestBlockHashCmd() + return c.sendCmd(cmd) +} + +// GetBestBlockHash returns the hash of the best block in the longest block chain. +func (c *Client) GetBestBlockHash() (*chainhash.Hash, error) { + return c.GetBestBlockHashAsync().Receive() +} + +// FutureGetBlockResult is a future promise to deliver the result of a GetBlockAsync RPC invocation (or an applicable +// error). +type FutureGetBlockResult chan *response + +// Receive waits for the response promised by the future and returns the raw block requested from the server given its +// hash. +func (r FutureGetBlockResult) Receive() (*wire.Block, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var blockHex string + e = js.Unmarshal(res, &blockHex) + if e != nil { + return nil, e + } + // Decode the serialized block hex to raw bytes. + serializedBlock, e := hex.DecodeString(blockHex) + if e != nil { + return nil, e + } + // Deserialize the block and return it. + var msgBlock wire.Block + e = msgBlock.Deserialize(bytes.NewReader(serializedBlock)) + if e != nil { + return nil, e + } + return &msgBlock, nil +} + +// GetBlockAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetBlock for the blocking version and more details. +func (c *Client) GetBlockAsync(blockHash *chainhash.Hash) FutureGetBlockResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetBlockCmd(hash, btcjson.Bool(false), nil) + return c.sendCmd(cmd) +} + +// GetBlock returns a raw block from the server given its hash. GetBlockVerbose to retrieve a data structure with +// information about the block instead. +func (c *Client) GetBlock(blockHash *chainhash.Hash) (*wire.Block, error) { + return c.GetBlockAsync(blockHash).Receive() +} + +// FutureGetBlockVerboseResult is a future promise to deliver the result of a GetBlockVerboseAsync RPC invocation (or an +// applicable error). +type FutureGetBlockVerboseResult chan *response + +// Receive waits for the response promised by the future and returns the data structure from the server with information about the requested block. +func (r FutureGetBlockVerboseResult) Receive() (*btcjson.GetBlockVerboseResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal the raw result into a BlockResult. + var blockResult btcjson.GetBlockVerboseResult + e = js.Unmarshal(res, &blockResult) + if e != nil { + return nil, e + } + return &blockResult, nil +} + +// GetBlockVerboseAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetBlockVerbose for the blocking version and more +// details. +func (c *Client) GetBlockVerboseAsync(blockHash *chainhash.Hash) FutureGetBlockVerboseResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetBlockCmd(hash, btcjson.Bool(true), nil) + return c.sendCmd(cmd) +} + +// GetBlockVerbose returns a data structure from the server with information about a block given its hash. See +// GetBlockVerboseTx to retrieve transaction data structures as well. See GetBlock to retrieve a raw block instead. +func (c *Client) GetBlockVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { + return c.GetBlockVerboseAsync(blockHash).Receive() +} + +// GetBlockVerboseTxAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetBlockVerboseTx or the blocking version and +// more details. +func (c *Client) GetBlockVerboseTxAsync(blockHash *chainhash.Hash) FutureGetBlockVerboseResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetBlockCmd(hash, btcjson.Bool(true), btcjson.Bool(true)) + return c.sendCmd(cmd) +} + +// GetBlockVerboseTx returns a data structure from the server with information about a block and its transactions given +// its hash. See GetBlockVerbose if only transaction hashes are preferred. See GetBlock to retrieve a raw block instead. +func (c *Client) GetBlockVerboseTx(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { + return c.GetBlockVerboseTxAsync(blockHash).Receive() +} + +// FutureGetBlockCountResult is a future promise to deliver the result of a GetBlockCountAsync RPC invocation (or an +// applicable error). +type FutureGetBlockCountResult chan *response + +// Receive waits for the response promised by the future and returns the number of blocks in the longest block chain. +func (r FutureGetBlockCountResult) Receive() (int64, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal the result as an int64. + var count int64 + e = js.Unmarshal(res, &count) + if e != nil { + return 0, e + } + return count, nil +} + +// GetBlockCountAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetBlockCount for the blocking version and more details. +func (c *Client) GetBlockCountAsync() FutureGetBlockCountResult { + cmd := btcjson.NewGetBlockCountCmd() + return c.sendCmd(cmd) +} + +// GetBlockCount returns the number of blocks in the longest block chain. +func (c *Client) GetBlockCount() (int64, error) { + return c.GetBlockCountAsync().Receive() +} + +// FutureGetDifficultyResult is a future promise to deliver the result of a GetDifficultyAsync RPC invocation (or an +// applicable error). +type FutureGetDifficultyResult chan *response + +// Receive waits for the response promised by the future and returns the proof-of-work difficulty as a multiple of the +// minimum difficulty. +func (r FutureGetDifficultyResult) Receive() (float64, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal the result as a float64. + var difficulty float64 + e = js.Unmarshal(res, &difficulty) + if e != nil { + return 0, e + } + return difficulty, nil +} + +// GetDifficultyAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetDifficulty for the blocking version and more details. +func (c *Client) GetDifficultyAsync(algo string) FutureGetDifficultyResult { + cmd := btcjson.NewGetDifficultyCmd(algo) + return c.sendCmd(cmd) +} + +// GetDifficulty returns the proof-of-work difficulty as a multiple of the minimum difficulty. +func (c *Client) GetDifficulty(algo string) (float64, error) { + return c.GetDifficultyAsync(algo).Receive() +} + +// FutureGetBlockChainInfoResult is a promise to deliver the result of a GetBlockChainInfoAsync RPC invocation (or an +// applicable error). +type FutureGetBlockChainInfoResult chan *response + +// Receive waits for the response promised by the future and returns chain info result provided by the server. +func (r FutureGetBlockChainInfoResult) Receive() (*btcjson.GetBlockChainInfoResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + var chainInfo btcjson.GetBlockChainInfoResult + if e := js.Unmarshal(res, &chainInfo); E.Chk(e) { + return nil, e + } + return &chainInfo, nil +} + +// GetBlockChainInfoAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. GetBlockChainInfo for the blocking version and more +// details. +func (c *Client) GetBlockChainInfoAsync() FutureGetBlockChainInfoResult { + cmd := btcjson.NewGetBlockChainInfoCmd() + return c.sendCmd(cmd) +} + +// GetBlockChainInfo returns information related to the processing state of various chain-specific details such as the +// current difficulty from the tip of the main chain. +func (c *Client) GetBlockChainInfo() (*btcjson.GetBlockChainInfoResult, error) { + return c.GetBlockChainInfoAsync().Receive() +} + +// FutureGetBlockHashResult is a future promise to deliver the result of a GetBlockHashAsync RPC invocation (or an +// applicable error). +type FutureGetBlockHashResult chan *response + +// Receive waits for the response promised by the future and returns the hash of the block in the best block chain at +// the given height. +func (r FutureGetBlockHashResult) Receive() (*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal the result as a string-encoded sha. + var txHashStr string + e = js.Unmarshal(res, &txHashStr) + if e != nil { + return nil, e + } + return chainhash.NewHashFromStr(txHashStr) +} + +// GetBlockHashAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetBlockHash for the blocking version and more details. +func (c *Client) GetBlockHashAsync(blockHeight int64) FutureGetBlockHashResult { + cmd := btcjson.NewGetBlockHashCmd(blockHeight) + return c.sendCmd(cmd) +} + +// GetBlockHash returns the hash of the block in the best block chain at the given height. +func (c *Client) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { + return c.GetBlockHashAsync(blockHeight).Receive() +} + +// FutureGetBlockHeaderResult is a future promise to deliver the result of a GetBlockHeaderAsync RPC invocation (or an +// applicable error). +type FutureGetBlockHeaderResult chan *response + +// Receive waits for the response promised by the future and returns the blockheader requested from the server given its +// hash. +func (r FutureGetBlockHeaderResult) Receive() (*wire.BlockHeader, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var bhHex string + e = js.Unmarshal(res, &bhHex) + if e != nil { + return nil, e + } + serializedBH, e := hex.DecodeString(bhHex) + if e != nil { + return nil, e + } + // Deserialize the blockheader and return it. + var bh wire.BlockHeader + e = bh.Deserialize(bytes.NewReader(serializedBH)) + if e != nil { + return nil, e + } + return &bh, e +} + +// GetBlockHeaderAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetBlockHeader for the blocking version and more +// details. +func (c *Client) GetBlockHeaderAsync(blockHash *chainhash.Hash) FutureGetBlockHeaderResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetBlockHeaderCmd(hash, btcjson.Bool(false)) + return c.sendCmd(cmd) +} + +// GetBlockHeader returns the blockheader from the server given its hash. See GetBlockHeaderVerbose to retrieve a data +// structure with information about the block instead. +func (c *Client) GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) { + return c.GetBlockHeaderAsync(blockHash).Receive() +} + +// FutureGetBlockHeaderVerboseResult is a future promise to deliver the result of a GetBlockAsync RPC invocation (or an +// applicable error). +type FutureGetBlockHeaderVerboseResult chan *response + +// Receive waits for the response promised by the future and returns the data structure of the blockheader requested +// from the server given its hash. +func (r FutureGetBlockHeaderVerboseResult) Receive() (*btcjson.GetBlockHeaderVerboseResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var bh btcjson.GetBlockHeaderVerboseResult + e = js.Unmarshal(res, &bh) + if e != nil { + return nil, e + } + return &bh, nil +} + +// GetBlockHeaderVerboseAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetBlockHeader for the blocking version and more +// details. +func (c *Client) GetBlockHeaderVerboseAsync(blockHash *chainhash.Hash) FutureGetBlockHeaderVerboseResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetBlockHeaderCmd(hash, btcjson.Bool(true)) + return c.sendCmd(cmd) +} + +// GetBlockHeaderVerbose returns a data structure with information about the blockheader from the server given its hash. +// See GetBlockHeader to retrieve a blockheader instead. +func (c *Client) GetBlockHeaderVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockHeaderVerboseResult, error) { + return c.GetBlockHeaderVerboseAsync(blockHash).Receive() +} + +// FutureGetMempoolEntryResult is a future promise to deliver the result of a GetMempoolEntryAsync RPC invocation (or an +// applicable error). +type FutureGetMempoolEntryResult chan *response + +// Receive waits for the response promised by the future and returns a data structure with information about the +// transaction in the memory pool given its hash. +func (r FutureGetMempoolEntryResult) Receive() (*btcjson.GetMempoolEntryResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal the result as an array of strings. + var mempoolEntryResult btcjson.GetMempoolEntryResult + e = js.Unmarshal(res, &mempoolEntryResult) + if e != nil { + return nil, e + } + return &mempoolEntryResult, nil +} + +// GetMempoolEntryAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetMempoolEntry for the blocking version and more +// details. +func (c *Client) GetMempoolEntryAsync(txHash string) FutureGetMempoolEntryResult { + cmd := btcjson.NewGetMempoolEntryCmd(txHash) + return c.sendCmd(cmd) +} + +// GetMempoolEntry returns a data structure with information about the transaction in the memory pool given its hash. +func (c *Client) GetMempoolEntry(txHash string) (*btcjson.GetMempoolEntryResult, error) { + return c.GetMempoolEntryAsync(txHash).Receive() +} + +// FutureGetRawMempoolResult is a future promise to deliver the result of a GetRawMempoolAsync RPC invocation (or an +// applicable error). +type FutureGetRawMempoolResult chan *response + +// Receive waits for the response promised by the future and returns the hashes of all transactions in the memory pool. +func (r FutureGetRawMempoolResult) Receive() ([]*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal the result as an array of strings. + var txHashStrs []string + e = js.Unmarshal(res, &txHashStrs) + if e != nil { + return nil, e + } + // Create a slice of ShaHash arrays from the string slice. + txHashes := make([]*chainhash.Hash, 0, len(txHashStrs)) + for _, hashStr := range txHashStrs { + txHash, e := chainhash.NewHashFromStr(hashStr) + if e != nil { + return nil, e + } + txHashes = append(txHashes, txHash) + } + return txHashes, nil +} + +// GetRawMempoolAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetRawMempool for the blocking version and more details. +func (c *Client) GetRawMempoolAsync() FutureGetRawMempoolResult { + cmd := btcjson.NewGetRawMempoolCmd(btcjson.Bool(false)) + return c.sendCmd(cmd) +} + +// GetRawMempool returns the hashes of all transactions in the memory pool. See GetRawMempoolVerbose to retrieve data +// structures with information about the transactions instead. +func (c *Client) GetRawMempool() ([]*chainhash.Hash, error) { + return c.GetRawMempoolAsync().Receive() +} + +// FutureGetRawMempoolVerboseResult is a future promise to deliver the result of a GetRawMempoolVerboseAsync RPC +// invocation (or an applicable error). +type FutureGetRawMempoolVerboseResult chan *response + +// Receive waits for the response promised by the future and returns a map of transaction hashes to an associated data +// structure with information about the transaction for all transactions in the memory pool. +func (r FutureGetRawMempoolVerboseResult) Receive() (map[string]btcjson.GetRawMempoolVerboseResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal the result as a map of strings (tx shas) to their detailed results. + var mempoolItems map[string]btcjson.GetRawMempoolVerboseResult + e = js.Unmarshal(res, &mempoolItems) + if e != nil { + return nil, e + } + return mempoolItems, nil +} + +// GetRawMempoolVerboseAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetRawMempoolVerbose for the blocking version and +// more details. +func (c *Client) GetRawMempoolVerboseAsync() FutureGetRawMempoolVerboseResult { + cmd := btcjson.NewGetRawMempoolCmd(btcjson.Bool(true)) + return c.sendCmd(cmd) +} + +// GetRawMempoolVerbose returns a map of transaction hashes to an associated data structure with information about the +// transaction for all transactions in the memory pool. See GetRawMempool to retrieve only the transaction hashes +// instead. +func (c *Client) GetRawMempoolVerbose() (map[string]btcjson.GetRawMempoolVerboseResult, error) { + return c.GetRawMempoolVerboseAsync().Receive() +} + +// FutureEstimateFeeResult is a future promise to deliver the result of a EstimateFeeAsync RPC invocation (or an +// applicable error). +type FutureEstimateFeeResult chan *response + +// Receive waits for the response promised by the future and returns the info provided by the server. +func (r FutureEstimateFeeResult) Receive() (float64, error) { + res, e := receiveFuture(r) + if e != nil { + return -1, e + } + // Unmarshal result as a getinfo result object. + var fee float64 + e = js.Unmarshal(res, &fee) + if e != nil { + return -1, e + } + return fee, nil +} + +// EstimateFeeAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See EstimateFee for the blocking version and more details. +func (c *Client) EstimateFeeAsync(numBlocks int64) FutureEstimateFeeResult { + cmd := btcjson.NewEstimateFeeCmd(numBlocks) + return c.sendCmd(cmd) +} + +// EstimateFee provides an estimated fee in bitcoins per kilobyte. +func (c *Client) EstimateFee(numBlocks int64) (float64, error) { + return c.EstimateFeeAsync(numBlocks).Receive() +} + +// FutureVerifyChainResult is a future promise to deliver the result of a VerifyChainAsync, VerifyChainLevelAsyncRPC, or +// VerifyChainBlocksAsync invocation (or an applicable error). +type FutureVerifyChainResult chan *response + +// Receive waits for the response promised by the future and returns whether or not the chain verified based on the +// check level and number of blocks to verify specified in the original call. +func (r FutureVerifyChainResult) Receive() (bool, error) { + res, e := receiveFuture(r) + if e != nil { + return false, e + } + // Unmarshal the result as a boolean. + var verified bool + e = js.Unmarshal(res, &verified) + if e != nil { + return false, e + } + return verified, nil +} + +// VerifyChainAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See VerifyChain for the blocking version and more details. +func (c *Client) VerifyChainAsync() FutureVerifyChainResult { + cmd := btcjson.NewVerifyChainCmd(nil, nil) + return c.sendCmd(cmd) +} + +// VerifyChain requests the server to verify the block chain database using the default check level and number of blocks +// to verify. See VerifyChainLevel and VerifyChainBlocks to override the defaults. +func (c *Client) VerifyChain() (bool, error) { + return c.VerifyChainAsync().Receive() +} + +// VerifyChainLevelAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See VerifyChainLevel for the blocking version and more +// details. +func (c *Client) VerifyChainLevelAsync(checkLevel int32) FutureVerifyChainResult { + cmd := btcjson.NewVerifyChainCmd(&checkLevel, nil) + return c.sendCmd(cmd) +} + +// VerifyChainLevel requests the server to verify the block chain database using the passed check level and default +// number of blocks to verify. The check level controls how thorough the verification is with higher numbers increasing +// the amount of checks done as consequently how long the verification takes. See VerifyChain to use the default check +// level and VerifyChainBlocks to override the number of blocks to verify. +func (c *Client) VerifyChainLevel(checkLevel int32) (bool, error) { + return c.VerifyChainLevelAsync(checkLevel).Receive() +} + +// VerifyChainBlocksAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See VerifyChainBlocks for the blocking version and +// more details. +func (c *Client) VerifyChainBlocksAsync(checkLevel, numBlocks int32) FutureVerifyChainResult { + cmd := btcjson.NewVerifyChainCmd(&checkLevel, &numBlocks) + return c.sendCmd(cmd) +} + +// VerifyChainBlocks requests the server to verify the block chain database using the passed check level and number of +// blocks to verify. The check level controls how thorough the verification is with higher numbers increasing the amount +// of checks done as consequently how long the verification takes. The number of blocks refers to the number of blocks +// from the end of the current longest chain. See VerifyChain and VerifyChainLevel to use defaults. +func (c *Client) VerifyChainBlocks(checkLevel, numBlocks int32) (bool, error) { + return c.VerifyChainBlocksAsync(checkLevel, numBlocks).Receive() +} + +// FutureGetTxOutResult is a future promise to deliver the result of a GetTxOutAsync RPC invocation (or an applicable +// error). +type FutureGetTxOutResult chan *response + +// Receive waits for the response promised by the future and returns a transaction given its hash. +func (r FutureGetTxOutResult) Receive() (*btcjson.GetTxOutResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // take care of the special case where the output has been spent already it should return the string "null" + if string(res) == "null" { + return nil, nil + } + // Unmarshal result as an gettxout result object. + var txOutInfo *btcjson.GetTxOutResult + e = js.Unmarshal(res, &txOutInfo) + if e != nil { + return nil, e + } + return txOutInfo, nil +} + +// GetTxOutAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetTxOut for the blocking version and more details. +func (c *Client) GetTxOutAsync(txHash *chainhash.Hash, index uint32, mempool bool) FutureGetTxOutResult { + hash := "" + if txHash != nil { + hash = txHash.String() + } + cmd := btcjson.NewGetTxOutCmd(hash, index, &mempool) + return c.sendCmd(cmd) +} + +// GetTxOut returns the transaction output info if it's unspent and nil, otherwise. +func (c *Client) GetTxOut(txHash *chainhash.Hash, index uint32, mempool bool) (*btcjson.GetTxOutResult, error) { + return c.GetTxOutAsync(txHash, index, mempool).Receive() +} + +// FutureRescanBlocksResult is a future promise to deliver the result of a RescanBlocksAsync RPC invocation (or an +// applicable error). +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +type FutureRescanBlocksResult chan *response + +// Receive waits for the response promised by the future and returns the discovered rescanblocks data. NOTE: This is a +// btcsuite extension ported from github.com/decred/dcrrpcclient. +func (r FutureRescanBlocksResult) Receive() ([]btcjson.RescannedBlock, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + var rescanBlocksResult []btcjson.RescannedBlock + e = js.Unmarshal(res, &rescanBlocksResult) + if e != nil { + return nil, e + } + return rescanBlocksResult, nil +} + +// RescanBlocksAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See RescanBlocks for the blocking version and more details. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (c *Client) RescanBlocksAsync(blockHashes []chainhash.Hash) FutureRescanBlocksResult { + strBlockHashes := make([]string, len(blockHashes)) + for i := range blockHashes { + strBlockHashes[i] = blockHashes[i].String() + } + cmd := btcjson.NewRescanBlocksCmd(strBlockHashes) + return c.sendCmd(cmd) +} + +// RescanBlocks rescans the blocks identified by blockHashes, in order, using the client's loaded transaction filter. +// The blocks do not need to be on the main chain, but they do need to be adjacent to each other. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (c *Client) RescanBlocks(blockHashes []chainhash.Hash) ([]btcjson.RescannedBlock, error) { + return c.RescanBlocksAsync(blockHashes).Receive() +} + +// FutureInvalidateBlockResult is a future promise to deliver the result of a InvalidateBlockAsync RPC invocation (or an +// applicable error). +type FutureInvalidateBlockResult chan *response + +// Receive waits for the response promised by the future and returns the raw block requested from the server given its +// hash. +func (r FutureInvalidateBlockResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// InvalidateBlockAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See InvalidateBlock for the blocking version and more +// details. +func (c *Client) InvalidateBlockAsync(blockHash *chainhash.Hash) FutureInvalidateBlockResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewInvalidateBlockCmd(hash) + return c.sendCmd(cmd) +} + +// InvalidateBlock invalidates a specific block. +func (c *Client) InvalidateBlock(blockHash *chainhash.Hash) (e error) { + return c.InvalidateBlockAsync(blockHash).Receive() +} + +// FutureGetCFilterResult is a future promise to deliver the result of a GetCFilterAsync RPC invocation (or an +// applicable error). +type FutureGetCFilterResult chan *response + +// Receive waits for the response promised by the future and returns the raw filter requested from the server given its +// block hash. +func (r FutureGetCFilterResult) Receive() (*wire.MsgCFilter, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var filterHex string + e = js.Unmarshal(res, &filterHex) + if e != nil { + return nil, e + } + // Decode the serialized cf hex to raw bytes. + serializedFilter, e := hex.DecodeString(filterHex) + if e != nil { + return nil, e + } + // Assign the filter bytes to the correct field of the wire message. We aren't going to set the block hash or + // extended flag, since we don't actually get that back in the RPC response. + var msgCFilter wire.MsgCFilter + msgCFilter.Data = serializedFilter + return &msgCFilter, nil +} + +// GetCFilterAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetCFilter for the blocking version and more details. +func (c *Client) GetCFilterAsync( + blockHash *chainhash.Hash, + filterType wire.FilterType, +) FutureGetCFilterResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetCFilterCmd(hash, filterType) + return c.sendCmd(cmd) +} + +// GetCFilter returns a raw filter from the server given its block hash. +func (c *Client) GetCFilter( + blockHash *chainhash.Hash, + filterType wire.FilterType, +) (*wire.MsgCFilter, error) { + return c.GetCFilterAsync(blockHash, filterType).Receive() +} + +// FutureGetCFilterHeaderResult is a future promise to deliver the result of a GetCFilterHeaderAsync RPC invocation (or +// an applicable error). +type FutureGetCFilterHeaderResult chan *response + +// Receive waits for the response promised by the future and returns the raw filter header requested from the server +// given its block hash. +func (r FutureGetCFilterHeaderResult) Receive() (*wire.MsgCFHeaders, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var headerHex string + e = js.Unmarshal(res, &headerHex) + if e != nil { + return nil, e + } + // Assign the decoded header into a hash + headerHash, e := chainhash.NewHashFromStr(headerHex) + if e != nil { + return nil, e + } + // Assign the hash to a headers message and return it. + msgCFHeaders := wire.MsgCFHeaders{PrevFilterHeader: *headerHash} + return &msgCFHeaders, nil +} + +// GetCFilterHeaderAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetCFilterHeader for the blocking version and more +// details. +func (c *Client) GetCFilterHeaderAsync( + blockHash *chainhash.Hash, + filterType wire.FilterType, +) FutureGetCFilterHeaderResult { + hash := "" + if blockHash != nil { + hash = blockHash.String() + } + cmd := btcjson.NewGetCFilterHeaderCmd(hash, filterType) + return c.sendCmd(cmd) +} + +// GetCFilterHeader returns a raw filter header from the server given its block hash. +func (c *Client) GetCFilterHeader( + blockHash *chainhash.Hash, + filterType wire.FilterType, +) (*wire.MsgCFHeaders, error) { + return c.GetCFilterHeaderAsync(blockHash, filterType).Receive() +} diff --git a/pkg/rpcclient/doc.go b/pkg/rpcclient/doc.go new file mode 100644 index 0000000..4fed8e5 --- /dev/null +++ b/pkg/rpcclient/doc.go @@ -0,0 +1,165 @@ +/*Package rpcclient implements a websocket-enabled Bitcoin JSON-RPC client. + +Overview + +This client provides a robust and easy to use client for interfacing with a Bitcoin RPC server that uses a pod/bitcoin +core compatible Bitcoin JSON-RPC API. This client has been tested with pod (https://github.com/p9c/monorepo), btcwallet +(https://github.com/p9c/monorepo/wallet), and bitcoin core (https://github.com/bitcoin). + +In addition to the compatible standard HTTP POST JSON-RPC API, pod and btcwallet provide a websocket interface that is +more efficient than the standard HTTP POST method of accessing RPC. The section below discusses the differences between +HTTP POST and websockets. + +**TODO:** The sense of TLS is reversed because many third party apps don't understand it and it complicates setup, so this next paragraph is wrong: + +~~By default, this client assumes the RPC server supports websockets and has TLS enabled. In practice, this currently +means it assumes you are talking to pod or btcwallet by default. However, configuration options are provided to fall +back to HTTP POST and disable TLS to support talking with inferior bitcoin core style RPC servers.~~ + +SSL third party certification security is deeply antiquated and outdated model, and irrelevant for developers and +servers running all on localhost or connected via already secured VPNs including Tor. In the future a protocol will be +developed based on elliptic curve cryptographic accounts, Diffie Hellman Perfect Forward Secrecy session negotiation (as +used in OTR and other messaging protocols), more along the lines of SSH. + +Websockets vs HTTP POST + +In HTTP POST-based JSON-RPC, every request creates a new HTTP connection, issues the call, waits for the response, and +closes the connection. This adds quite a bit of overhead to every call and lacks flexibility for features such as +notifications. + +In contrast, the websocket-based JSON-RPC interface provided by pod and btcwallet only uses a single connection that +remains open and allows asynchronous bi-directional communication. The websocket interface supports all of the same +commands as HTTP POST, but they can be invoked without having to go through a connect/disconnect cycle for every call. + +In addition, the websocket interface provides other nice features such as the ability to register for asynchronous +notifications of various events. + +Synchronous vs Asynchronous API + +The client provides both a synchronous (blocking) and asynchronous API. The synchronous (blocking) API is typically +sufficient for most use cases. It works by issuing the RPC and blocking until the response is received. This allows +straightforward code where you have the response as soon as the function returns. + +The asynchronous API works on the concept of futures. When you invoke the async version of a command, it will quickly +return an instance of a type that promises to provide the result of the RPC at some future time. In the background, the +RPC call is issued and the result is stored in the returned instance. + +Invoking the Receive method on the returned instance will either return the result immediately if it has already +arrived, or block until it has. This is useful since it provides the caller with greater control over concurrency. + +Notifications + +The first important part of notifications is to realize that they will only work when connected via websockets. This +should intuitively make sense because HTTP POST mode does not keep a connection open! + +All notifications provided by pod require registration to opt-in. + +For example, if you want to be notified when funds are received by a set of addresses, you register the addresses via +the NotifyReceived (or NotifyReceivedAsync) function. + +Notification Handlers + +Notifications are exposed by the client through the use of callback handlers which are setup via a NotificationHandlers +instance that is specified by the caller when creating the client. + +It is important that these notification handlers complete quickly since they are intentionally in the main read loop and +will block further reads until they complete. + +This provides the caller with the flexibility to decide what to do when notifications are coming in faster than they are +being handled. + +In particular this means issuing a blocking RPC call from a callback handler will cause a deadlock as more server +responses won't be read until the callback returns, but the callback would be waiting for a response. + +Thus, any additional RPCs must be issued an a completely decoupled manner. + +Automatic Reconnection + +By default, when running in websockets mode, this client will automatically keep trying to reconnect to the RPC server +should the connection be lost. + +There is a back-off in between each connection attempt until it reaches one try per minute. Once a connection is +re-established, all previously registered notifications are automatically re-registered and any in-flight commands are +re-issued. This means from the caller's perspective, the request simply takes longer to complete. + +The caller may invoke the Shutdown method on the client to force the client to cease reconnect attempts and return +ErrClientShutdown for all outstanding commands. + +The automatic reconnection can be disabled by setting the DisableAutoReconnect flag to true in the connection config +when creating the client. + +Minor RPC Server Differences and Chain/Wallet Separation + +Some of the commands are extensions specific to a particular RPC server. + +For example, the DebugLevel call is an extension only provided by pod (and sac wallet passthrough). Therefore if you +call one of these commands against an RPC server that doesn't provide them, you will get an unimplemented error from the +server. An effort has been made to call out which commmands are extensions in their documentation. + +Also, it is important to realize that pod intentionally separates the wallet functionality into a separate process named +btcwallet. This means if you are connected to the pod RPC server directly, only the RPCs which are related to chain +services will be available. Depending on your application, you might only need chain-related RPCs. In contrast, +btcwallet provides pass through treatment for chain-related RPCs, so it supports them in addition to wallet-related +RPCs. + +Errors + +There are 3 categories of errors that will be returned throughout this package: + + - Errors related to the client connection such as authentication, endpoint, disconnect, and shutdown + + - Errors that occur before communicating with the remote RPC server such as command creation and marshaling errors or + issues talking to the remote server + + - Errors returned from the remote RPC server like unimplemented commands, nonexistent requested blocks and + transactions, malformed data, and incorrect networks + +The first category of errors are typically one of ErrInvalidAuth, ErrInvalidEndpoint, ErrClientDisconnect, or +ErrClientShutdown. + +NOTE: The ErrClientDisconnect will not be returned unless the DisableAutoReconnect flag is set since the client +automatically handles reconnect by default as previously described. + +The second category of errors typically indicates a programmer error and as such the type can vary, but usually will be +best handled by simply showing/logging it. + +The third category of errors, that is errors returned by the server, can be detected by type asserting the error in a +*btcjson.RPCError. + +For example, to detect if a command is unimplemented by the remote RPC server: + + amount, e := client.GetBalance("") + if e != nil { + + if jerr, ok := err.(*btcjson.RPCError); ok { + + switch jerr.Code { + + case btcjson.ErrRPCUnimplemented: + // Handle not implemented error + // Handle other specific errors you care about + } + } + // Log or otherwise handle the error knowing it was not one returned + // from the remote RPC server. + } + +Example Usage + +The following full-blown client examples are in the examples directory: + + - bitcoincorehttp + + Connects to a bitcoin core RPC server using HTTP POST mode with TLS disabled and gets the current block count + + - podwebsockets + + Connects to a pod RPC server using TLS-secured websockets, registers for block connected and block disconnected + notifications, and gets the current block count + + - btcwalletwebsockets + + Connects to a btcwallet RPC server using TLS-secured websockets, registers for notifications about changes to account + balances, and gets a list of unspent transaction outputs (utxos) the wallet can sign +*/ +package rpcclient diff --git a/pkg/rpcclient/examples/bitcoincorehttp/README.md b/pkg/rpcclient/examples/bitcoincorehttp/README.md new file mode 100755 index 0000000..5626e63 --- /dev/null +++ b/pkg/rpcclient/examples/bitcoincorehttp/README.md @@ -0,0 +1,30 @@ +# Bitcoin Core HTTP POST Example + +This example shows how to use the rpcclient package to connect to a Bitcoin Core RPC server using HTTP POST mode with +TLS disabled and gets the current block count. + +## Running the Example + +The first step is to use `go get` to download and install the rpcclient package: + +```bash +$ go get github.com/p9c/p9/rpcclient +``` + +Next, modify the `main.go` source to specify the correct RPC username and password for the RPC server: + +```Go + User: "yourrpcuser", +Pass: "yourrpcpass", +``` + +Finally, navigate to the example's directory and run it with: + +```bash +$ cd $GOPATH/src/github.com/p9c/p9/rpcclient/examples/bitcoincorehttp +$ go run *.go +``` + +## License + +This example is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/pkg/rpcclient/examples/bitcoincorehttp/logmain.go b/pkg/rpcclient/examples/bitcoincorehttp/logmain.go new file mode 100644 index 0000000..ad256c8 --- /dev/null +++ b/pkg/rpcclient/examples/bitcoincorehttp/logmain.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/rpcclient/examples/bitcoincorehttp/main.go b/pkg/rpcclient/examples/bitcoincorehttp/main.go new file mode 100644 index 0000000..81ce5a5 --- /dev/null +++ b/pkg/rpcclient/examples/bitcoincorehttp/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/rpcclient" +) + +func main() { + // Connect to local bitcoin core RPC server using HTTP POST mode. + connCfg := &rpcclient.ConnConfig{ + Host: "localhost:11046", + User: "yourrpcuser", + Pass: "yourrpcpass", + HTTPPostMode: true, // Bitcoin core only supports HTTP POST mode + TLS: false, // Bitcoin core does not provide TLS by default + } + // Notice the notification parameter is nil since notifications are not supported in HTTP POST mode. + client, e := rpcclient.New(connCfg, nil, qu.T()) + if e != nil { + F.Ln(e) + } + defer client.Shutdown() + // Get the current block count. + blockCount, e := client.GetBlockCount() + if e != nil { + F.Ln(e) + } + log.Printf("Block count: %d", blockCount) +} diff --git a/pkg/rpcclient/examples/btcdwebsockets/README.md b/pkg/rpcclient/examples/btcdwebsockets/README.md new file mode 100755 index 0000000..fe05366 --- /dev/null +++ b/pkg/rpcclient/examples/btcdwebsockets/README.md @@ -0,0 +1,35 @@ +# pod Websockets Example + +This example shows how to use the rpcclient package to connect to a pod RPC +server using TLS-secured websockets, register for block connected and block +disconnected notifications, and get the current block count. + +This example also sets a timer to shutdown the client after 10 seconds to +demonstrate clean shutdown. + +## Running the Example + +The first step is to use `go get` to download and install the rpcclient package: + +```bash +$ go get github.com/p9c/p9/rpcclient +``` + +Next, modify the `main.go` source to specify the correct RPC username and +password for the RPC server: + +```Go + User: "yourrpcuser", +Pass: "yourrpcpass", +``` + +Finally, navigate to the example's directory and run it with: + +```bash +$ cd $GOPATH/src/github.com/p9c/p9/rpcclient/examples/podwebsockets +$ go run *.go +``` + +## License + +This example is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/pkg/rpcclient/examples/btcdwebsockets/logmain.go b/pkg/rpcclient/examples/btcdwebsockets/logmain.go new file mode 100644 index 0000000..ad256c8 --- /dev/null +++ b/pkg/rpcclient/examples/btcdwebsockets/logmain.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/rpcclient/examples/btcdwebsockets/main.go b/pkg/rpcclient/examples/btcdwebsockets/main.go new file mode 100644 index 0000000..c6d59c9 --- /dev/null +++ b/pkg/rpcclient/examples/btcdwebsockets/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "path/filepath" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/appdata" + "github.com/p9c/p9/pkg/rpcclient" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +func main() { + // Only override the handlers for notifications you care about. Also note most of these handlers will only be called + // if you register for notifications. See the documentation of the rpcclient NotificationHandlers type for more + // details about each handler. + ntfnHandlers := rpcclient.NotificationHandlers{ + OnFilteredBlockConnected: func(height int32, header *wire.BlockHeader, txns []*util.Tx) { + log.Printf( + "Block connected: %v (%d) %v", + header.BlockHash(), height, header.Timestamp, + ) + }, + OnFilteredBlockDisconnected: func(height int32, header *wire.BlockHeader) { + log.Printf( + "Block disconnected: %v (%d) %v", + header.BlockHash(), height, header.Timestamp, + ) + }, + } + // Connect to local pod RPC server using websockets. + podHomeDir := appdata.Dir("pod", false) + var certs []byte + var e error + certs, e = ioutil.ReadFile(filepath.Join(podHomeDir, "rpc.cert")) + if e != nil { + F.Ln(e) + } + connCfg := &rpcclient.ConnConfig{ + Host: "localhost:11048", + Endpoint: "ws", + User: "yourrpcuser", + Pass: "yourrpcpass", + Certificates: certs, + } + var client *rpcclient.Client + client, e = rpcclient.New(connCfg, &ntfnHandlers, qu.T()) + if e != nil { + F.Ln(e) + } + // Register for block connect and disconnect notifications. + if e = client.NotifyBlocks(); E.Chk(e) { + F.Ln(e) + } + fmt.Println("NotifyBlocks: Registration Complete") + // Get the current block count. + blockCount, e := client.GetBlockCount() + if e != nil { + F.Ln(e) + } + log.Printf("Block count: %d", blockCount) + // For this example gracefully shutdown the client after 10 seconds. Ordinarily when to shutdown the client is + // highly application specific. + fmt.Println("Client shutdown in 10 seconds...") + time.AfterFunc( + time.Second*10, func() { + fmt.Println("Client shutting down...") + client.Shutdown() + fmt.Println("Client shutdown complete.") + }, + ) + // Wait until the client either shuts down gracefully (or the user terminates the process with Ctrl+C). + client.WaitForShutdown() +} diff --git a/pkg/rpcclient/examples/btcwalletwebsockets/README.md b/pkg/rpcclient/examples/btcwalletwebsockets/README.md new file mode 100755 index 0000000..7889593 --- /dev/null +++ b/pkg/rpcclient/examples/btcwalletwebsockets/README.md @@ -0,0 +1,34 @@ +# btcwallet Websockets Example + +This example shows how to use the rpcclient package to connect to a btcwallet +RPC server using TLS-secured websockets, register for notifications about +changes to account balances, and get a list of unspent transaction outputs +(utxos) the wallet can sign. This example also sets a timer to shutdown the +client after 10 seconds to demonstrate clean shutdown. + +## Running the Example + +The first step is to use `go get` to download and install the rpcclient package: + +```bash +$ go get github.com/p9c/p9/rpcclient +``` + +Next, modify the `main.go` source to specify the correct RPC username and +password for the RPC server: + +```Go + User: "yourrpcuser", +Pass: "yourrpcpass", +``` + +Finally, navigate to the example's directory and run it with: + +```bash +$ cd $GOPATH/src/github.com/p9c/p9/rpcclient/examples/btcwalletwebsockets +$ go run *.go +``` + +## License + +This example is licensed under the [copyfree](http://copyfree.org) ISC License. diff --git a/pkg/rpcclient/examples/btcwalletwebsockets/log.go b/pkg/rpcclient/examples/btcwalletwebsockets/log.go new file mode 100644 index 0000000..ad256c8 --- /dev/null +++ b/pkg/rpcclient/examples/btcwalletwebsockets/log.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/rpcclient/examples/btcwalletwebsockets/main.go b/pkg/rpcclient/examples/btcwalletwebsockets/main.go new file mode 100644 index 0000000..1923891 --- /dev/null +++ b/pkg/rpcclient/examples/btcwalletwebsockets/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "github.com/p9c/p9/pkg/amt" + "io/ioutil" + "log" + "path/filepath" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/davecgh/go-spew/spew" + + "github.com/p9c/p9/pkg/appdata" + "github.com/p9c/p9/pkg/rpcclient" +) + +func main() { + // Only override the handlers for notifications you care about. Also note most of the handlers will only be called + // if you register for notifications. See the documentation of the rpcclient NotificationHandlers type for more + // details about each handler. + ntfnHandlers := rpcclient.NotificationHandlers{ + OnAccountBalance: func(account string, balance amt.Amount, confirmed bool) { + log.Printf( + "New balance for account %s: %v", account, + balance, + ) + }, + } + // Connect to local btcwallet RPC server using websockets. + certHomeDir := appdata.Dir("mod", false) + certs, e := ioutil.ReadFile(filepath.Join(certHomeDir, "rpc.cert")) + if e != nil { + F.Ln(e) + } + connCfg := &rpcclient.ConnConfig{ + Host: "localhost:11046", + Endpoint: "ws", + User: "yourrpcuser", + Pass: "yourrpcpass", + Certificates: certs, + } + client, e := rpcclient.New(connCfg, &ntfnHandlers, qu.T()) + if e != nil { + F.Ln(e) + } + // Get the list of unspent transaction outputs (utxos) that the connected wallet has at least one private key for. + unspent, e := client.ListUnspent() + if e != nil { + F.Ln(e) + } + log.Printf("Num unspent outputs (utxos): %d", len(unspent)) + if len(unspent) > 0 { + log.Printf("First utxo:\n%v", spew.Sdump(unspent[0])) + } + // For this example gracefully shutdown the client after 10 seconds. Ordinarily when to shutdown the client is + // highly application specific. + fmt.Println("Client shutdown in 10 seconds...") + time.AfterFunc( + time.Second*10, func() { + fmt.Println("Client shutting down...") + client.Shutdown() + fmt.Println("Client shutdown complete.") + }, + ) + // Wait until the client either shuts down gracefully (or the user terminates the process with Ctrl+C). + client.WaitForShutdown() +} diff --git a/pkg/rpcclient/exts.go b/pkg/rpcclient/exts.go new file mode 100644 index 0000000..df07a1b --- /dev/null +++ b/pkg/rpcclient/exts.go @@ -0,0 +1,405 @@ +package rpcclient + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + js "encoding/json" + "fmt" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// FutureDebugLevelResult is a future promise to deliver the result of a DebugLevelAsync RPC invocation (or an +// applicable error). +type FutureDebugLevelResult chan *response + +// Receive waits for the response promised by the future and returns the result of setting the debug logging level to +// the passed level specification or the list of of the available subsystems for the special keyword 'show'. +func (r FutureDebugLevelResult) Receive() (string, error) { + res, e := receiveFuture(r) + if e != nil { + return "", e + } + // Unmashal the result as a string. + var result string + e = js.Unmarshal(res, &result) + if e != nil { + return "", e + } + return result, nil +} + +// DebugLevelAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See DebugLevel for the blocking version and more details. +// NOTE: This is a pod extension. +func (c *Client) DebugLevelAsync(levelSpec string) FutureDebugLevelResult { + cmd := btcjson.NewDebugLevelCmd(levelSpec) + return c.sendCmd(cmd) +} + +// DebugLevel dynamically sets the debug logging level to the passed level specification. The levelspec can be either a +// debug level or of the form: +// +// =,=,... +// +// Additionally, the special keyword 'show' can be used to get a list of the available subsystems. +// +// NOTE: This is a pod extension. +func (c *Client) DebugLevel(levelSpec string) (string, error) { + return c.DebugLevelAsync(levelSpec).Receive() +} + +// FutureCreateEncryptedWalletResult is a future promise to deliver the error result of a CreateEncryptedWalletAsync RPC +// invocation. +type FutureCreateEncryptedWalletResult chan *response + +// Receive waits for and returns the error response promised by the future. +func (r FutureCreateEncryptedWalletResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// CreateEncryptedWalletAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See CreateEncryptedWallet for the blocking version +// and more details. NOTE: This is a btcwallet extension. +func (c *Client) CreateEncryptedWalletAsync(passphrase string) FutureCreateEncryptedWalletResult { + cmd := btcjson.NewCreateEncryptedWalletCmd(passphrase) + return c.sendCmd(cmd) +} + +// CreateEncryptedWallet requests the creation of an encrypted wallet. Wallets managed by btcwallet are only written to +// disk with encrypted private keys, and generating wallets on the fly is impossible as it requires user input for the +// encryption passphrase. +// +// This RPC specifies the passphrase and instructs the wallet creation. This may error if a +// wallet is already opened, or the new wallet cannot be written to disk. NOTE: This is a btcwallet extension. +func (c *Client) CreateEncryptedWallet(passphrase string) (e error) { + return c.CreateEncryptedWalletAsync(passphrase).Receive() +} + +// FutureListAddressTransactionsResult is a future promise to deliver the result of a ListAddressTransactionsAsync RPC +// invocation (or an applicable error). +type FutureListAddressTransactionsResult chan *response + +// Receive waits for the response promised by the future and returns information about all transactions associated with +// the provided addresses. +func (r FutureListAddressTransactionsResult) Receive() ([]btcjson.ListTransactionsResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal the result as an array of listtransactions objects. + var transactions []btcjson.ListTransactionsResult + e = js.Unmarshal(res, &transactions) + if e != nil { + return nil, e + } + return transactions, nil +} + +// ListAddressTransactionsAsync returns an instance of a type that can be used get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See ListAddressTransactions for the blocking version +// and more details. NOTE: This is a pod extension. +func (c *Client) ListAddressTransactionsAsync( + addresses []btcaddr.Address, + account string, +) FutureListAddressTransactionsResult { + // Convert addresses to strings. + addrs := make([]string, 0, len(addresses)) + for _, addr := range addresses { + addrs = append(addrs, addr.EncodeAddress()) + } + cmd := btcjson.NewListAddressTransactionsCmd(addrs, &account) + return c.sendCmd(cmd) +} + +// ListAddressTransactions returns information about all transactions associated with the provided addresses. NOTE: This +// is a btcwallet extension. +func (c *Client) ListAddressTransactions(addresses []btcaddr.Address, account string) ( + []btcjson.ListTransactionsResult, + error, +) { + return c.ListAddressTransactionsAsync(addresses, account).Receive() +} + +// FutureGetBestBlockResult is a future promise to deliver the result of a GetBestBlockAsync RPC invocation (or an +// applicable error). +type FutureGetBestBlockResult chan *response + +// Receive waits for the response promised by the future and returns the hash and height of the block in the longest +// (best) chain. +func (r FutureGetBestBlockResult) Receive() (*chainhash.Hash, int32, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, 0, e + } + // Unmarshal result as a getbestblock result object. + var bestBlock btcjson.GetBestBlockResult + e = js.Unmarshal(res, &bestBlock) + if e != nil { + return nil, 0, e + } + // Convert to hash from string. + hash, e := chainhash.NewHashFromStr(bestBlock.Hash) + if e != nil { + return nil, 0, e + } + return hash, bestBlock.Height, nil +} + +// GetBestBlockAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetBestBlock for the blocking version and more details. +// +// NOTE: This is a pod extension. +func (c *Client) GetBestBlockAsync() FutureGetBestBlockResult { + cmd := btcjson.NewGetBestBlockCmd() + return c.sendCmd(cmd) +} + +// GetBestBlock returns the hash and height of the block in the longest (best) chain. +// +// NOTE: This is a pod extension. +func (c *Client) GetBestBlock() (*chainhash.Hash, int32, error) { + return c.GetBestBlockAsync().Receive() +} + +// FutureGetCurrentNetResult is a future promise to deliver the result of a GetCurrentNetAsync RPC invocation (or an +// applicable error). +type FutureGetCurrentNetResult chan *response + +// Receive waits for the response promised by the future and returns the network the server is running on. +func (r FutureGetCurrentNetResult) Receive() (wire.BitcoinNet, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as an int64. + var net int64 + e = js.Unmarshal(res, &net) + if e != nil { + return 0, e + } + return wire.BitcoinNet(net), nil +} + +// GetCurrentNetAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetCurrentNet for the blocking version and more details. +// +// NOTE: This is a pod extension. +func (c *Client) GetCurrentNetAsync() FutureGetCurrentNetResult { + cmd := btcjson.NewGetCurrentNetCmd() + return c.sendCmd(cmd) +} + +// GetCurrentNet returns the network the server is running on. +// +// NOTE: This is a pod extension. +func (c *Client) GetCurrentNet() (wire.BitcoinNet, error) { + return c.GetCurrentNetAsync().Receive() +} + +// FutureGetHeadersResult is a future promise to deliver the result of a getheaders RPC invocation (or an applicable +// error). +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +type FutureGetHeadersResult chan *response + +// Receive waits for the response promised by the future and returns the getheaders result. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (r FutureGetHeadersResult) Receive() ([]wire.BlockHeader, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a slice of strings. + var result []string + e = js.Unmarshal(res, &result) + if e != nil { + return nil, e + } + // Deserialize the []string into []wire.BlockHeader. + headers := make([]wire.BlockHeader, len(result)) + for i, headerHex := range result { + serialized, e := hex.DecodeString(headerHex) + if e != nil { + return nil, e + } + e = headers[i].Deserialize(bytes.NewReader(serialized)) + if e != nil { + return nil, e + } + } + return headers, nil +} + +// GetHeadersAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetHeaders for the blocking version and more details. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (c *Client) GetHeadersAsync(blockLocators []chainhash.Hash, hashStop *chainhash.Hash) FutureGetHeadersResult { + locators := make([]string, len(blockLocators)) + for i := range blockLocators { + locators[i] = blockLocators[i].String() + } + hash := "" + if hashStop != nil { + hash = hashStop.String() + } + cmd := btcjson.NewGetHeadersCmd(locators, hash) + return c.sendCmd(cmd) +} + +// GetHeaders mimics the wire protocol getheaders and headers messages by returning all headers on the main chain after +// the first known block in the locators, up until a block hash matches hashStop. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (c *Client) GetHeaders(blockLocators []chainhash.Hash, hashStop *chainhash.Hash) ([]wire.BlockHeader, error) { + return c.GetHeadersAsync(blockLocators, hashStop).Receive() +} + +// FutureExportWatchingWalletResult is a future promise to deliver the result of an ExportWatchingWalletAsync RPC +// invocation (or an applicable error). +type FutureExportWatchingWalletResult chan *response + +// Receive waits for the response promised by the future and returns the exported wallet. +func (r FutureExportWatchingWalletResult) Receive() ([]byte, []byte, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, nil, e + } + // Unmarshal result as a JSON object. + var obj map[string]interface{} + e = js.Unmarshal(res, &obj) + if e != nil { + return nil, nil, e + } + // Chk for the wallet and tx string fields in the object. + base64Wallet, ok := obj["wallet"].(string) + if !ok { + return nil, nil, fmt.Errorf( + "unexpected response type for exportwatchingwallet 'wallet' field: %T\n", + obj["wallet"], + ) + } + base64TxStore, ok := obj["tx"].(string) + if !ok { + return nil, nil, fmt.Errorf( + "unexpected response type for exportwatchingwallet 'tx' field: %T\n", + obj["tx"], + ) + } + walletBytes, e := base64.StdEncoding.DecodeString(base64Wallet) + if e != nil { + return nil, nil, e + } + txStoreBytes, e := base64.StdEncoding.DecodeString(base64TxStore) + if e != nil { + return nil, nil, e + } + return walletBytes, txStoreBytes, nil +} + +// ExportWatchingWalletAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See ExportWatchingWallet for the blocking version and +// more details. +// +// NOTE: This is a btcwallet extension. +func (c *Client) ExportWatchingWalletAsync(account string) FutureExportWatchingWalletResult { + cmd := btcjson.NewExportWatchingWalletCmd(&account, btcjson.Bool(true)) + return c.sendCmd(cmd) +} + +// ExportWatchingWallet returns the raw bytes for a watching-only version of wallet.bin and tx.bin, respectively, for +// the specified account that can be used by btcwallet to enable a wallet which does not have the private keys necessary +// to spend funds. +// +// NOTE: This is a btcwallet extension. +func (c *Client) ExportWatchingWallet(account string) ([]byte, []byte, error) { + return c.ExportWatchingWalletAsync(account).Receive() +} + +// FutureSessionResult is a future promise to deliver the result of a SessionAsync RPC invocation (or an applicable +// error). +type FutureSessionResult chan *response + +// Receive waits for the response promised by the future and returns the session result. +func (r FutureSessionResult) Receive() (*btcjson.SessionResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a session result object. + var session btcjson.SessionResult + e = js.Unmarshal(res, &session) + if e != nil { + return nil, e + } + return &session, nil +} + +// SessionAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See Session for the blocking version and more details. +// +// NOTE: This is a btcsuite extension. +func (c *Client) SessionAsync() FutureSessionResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + cmd := btcjson.NewSessionCmd() + return c.sendCmd(cmd) +} + +// Session returns details regarding a websocket client's current connection. This RPC requires the client to be running +// in websocket mode. +// +// NOTE: This is a btcsuite extension. +func (c *Client) Session() (*btcjson.SessionResult, error) { + return c.SessionAsync().Receive() +} + +// FutureVersionResult is a future promise to deliver the result of a version RPC invocation (or an applicable error). +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +type FutureVersionResult chan *response + +// Receive waits for the response promised by the future and returns the version result. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (r FutureVersionResult) Receive() ( + map[string]btcjson.VersionResult, + error, +) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a version result object. + var vr map[string]btcjson.VersionResult + e = js.Unmarshal(res, &vr) + if e != nil { + return nil, e + } + return vr, nil +} + +// VersionAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See Version for the blocking version and more details. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (c *Client) VersionAsync() FutureVersionResult { + cmd := btcjson.NewVersionCmd() + return c.sendCmd(cmd) +} + +// Version returns information about the server's JSON-RPC API versions. +// +// NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. +func (c *Client) Version() (map[string]btcjson.VersionResult, error) { + return c.VersionAsync().Receive() +} diff --git a/pkg/rpcclient/infra.go b/pkg/rpcclient/infra.go new file mode 100644 index 0000000..9fa6ffd --- /dev/null +++ b/pkg/rpcclient/infra.go @@ -0,0 +1,1246 @@ +package rpcclient + +import ( + "bytes" + "container/list" + "crypto/tls" + "crypto/x509" + "encoding/base64" + js "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "net" + "net/http" + "net/url" + "sync" + "sync/atomic" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/btcsuite/go-socks/socks" + "github.com/btcsuite/websocket" + + "github.com/p9c/p9/pkg/btcjson" +) + +var ( + // ErrInvalidAuth is an error to describe the condition where the client is + // either unable to authenticate or the specified endpoint is incorrect. + ErrInvalidAuth = errors.New("authentication failure") + // ErrInvalidEndpoint is an error to describe the condition where the websocket + // handshake failed with the specified endpoint. + ErrInvalidEndpoint = errors.New("the endpoint either does not support websockets or does not exist") + // ErrClientNotConnected is an error to describe the condition where a websocket + // client has been created, but the connection was never established. + // + // This condition differs from ErrClientDisconnect, which represents an + // established connection that was lost. + ErrClientNotConnected = errors.New("the client was never connected") + // ErrClientDisconnect is an error to describe the condition where the client + // has been disconnected from the RPC server. + // + // When the DisableAutoReconnect opt is not set, any outstanding futures when + // a client disconnect occurs will return this error as will any new requests. + ErrClientDisconnect = errors.New("the client has been disconnected") + // ErrClientShutdown is an error to describe the condition where the client is + // either already shutdown, or in the process of shutting down. + // + // Any outstanding futures when a client shutdown occurs will return this error + // as will any new requests. + ErrClientShutdown = errors.New("the client has been shutdown") + // ErrNotWebsocketClient is an error to describe the condition of calling a + // Client method intended for a websocket client when the client has been + // configured to run in HTTP POST mode instead. + ErrNotWebsocketClient = errors.New("client is not configured for websockets") + // ErrClientAlreadyConnected is an error to describe the condition where a new + // client connection cannot be established due to a websocket client having + // already connected to the RPC server. + ErrClientAlreadyConnected = errors.New("websocket client has already connected") +) + +const ( + // sendBufferSize is the number of elements the websocket send channel can queue + // before blocking. + sendBufferSize = 50 + // sendPostBufferSize is the number of elements the HTTP POST send channel can + // queue before blocking. + sendPostBufferSize = 100 + // connectionRetryInterval is the amount of time to wait in between retries when + // automatically reconnecting to an RPC server. + connectionRetryInterval = time.Second * 5 +) + +// sendPostDetails houses an HTTP POST request to send to an RPC server as well +// as the original JSON-RPC command and a channel to reply on when the server +// responds with the result. +type sendPostDetails struct { + httpRequest *http.Request + jsonRequest *jsonRequest +} + +// jsonRequest holds information about a json request that is used to properly +// detect, interpret, and deliver a reply to it. +type jsonRequest struct { + id uint64 + method string + cmd interface{} + marshalledJSON []byte + responseChan chan *response +} + +// Client represents a Bitcoin RPC client which allows easy access to the +// various RPC methods available on a Bitcoin RPC server. +// +// Each of the wrapper functions handle the details of converting the passed and +// return types to and from the underlying JSON types which are required for the +// JSON-RPC invocations +// +// The client provides each RPC in both synchronous (blocking) and asynchronous +// (non-blocking) forms. +// +// The asynchronous forms are based on the concept of futures where they return +// an instance of a type that promises to deliver the result of the invocation +// at some future time. +// +// Invoking the Receive method on the returned future will block until the +// result is available if it's not already. +type Client struct { + id uint64 // atomic, so must stay 64-bit aligned + // config holds the connection configuration assoiated with this client. + config *ConnConfig + // wsConn is the underlying websocket connection when not in HTTP POST mode. + wsConn *websocket.Conn + // httpClient is the underlying HTTP client to use when running in HTTP + // POST mode. + httpClient *http.Client + // mtx is a mutex to protect access to connection related fields. + mtx sync.Mutex + // disconnected indicated whether or not the server is disconnected. + disconnected bool + // retryCount holds the number of times the client has tried to reconnect to the + // RPC server. + retryCount int64 + // Track command and their response channels by ID. + requestLock sync.Mutex + requestMap map[uint64]*list.Element + requestList *list.List + // Notifications. + ntfnHandlers *NotificationHandlers + ntfnStateLock sync.Mutex + ntfnState *notificationState + // Networking infrastructure. + sendChan chan []byte + sendPostChan chan *sendPostDetails + connEstablished qu.C + disconnect qu.C + shutdown qu.C + wg sync.WaitGroup +} + +// NextID returns the next id to be used when sending a JSON-RPC message. This +// ID allows responses to be associated with particular requests per the +// JSON-RPC specification. +// +// Typically the consumer of the client does not need to call this function, +// however, if a custom request is being created and used this function should +// be used to ensure the ID is unique amongst all requests being made. +func (c *Client) NextID() uint64 { + return atomic.AddUint64(&c.id, 1) +} + +// addRequest associates the passed jsonRequest with its id. +// +// This allows the response from the remote server to be unmarshalled to the +// appropriate type and sent to the specified channel when it is received. +// +// If the client has already begun shutting down, ErrClientShutdown is returned +// and the request is not added. This function is safe for concurrent access. +func (c *Client) addRequest(jReq *jsonRequest) (e error) { + c.requestLock.Lock() + defer c.requestLock.Unlock() + // A non-blocking read of the shutdown channel with the request lock held avoids + // adding the request to the client's internal data structures if the client is + // in the process of shutting down (and has not yet grabbed the request lock), + // or has finished shutdown already (responding to each outstanding request with + // ErrClientShutdown). + select { + case <-c.shutdown.Wait(): + return ErrClientShutdown + default: + } + element := c.requestList.PushBack(jReq) + c.requestMap[jReq.id] = element + return nil +} + +// removeRequest returns and removes the jsonRequest which contains the response +// channel and original method associated with the passed id or nil if there is +// no association. This function is safe for concurrent access. +func (c *Client) removeRequest(id uint64) *jsonRequest { + c.requestLock.Lock() + defer c.requestLock.Unlock() + element := c.requestMap[id] + if element != nil { + delete(c.requestMap, id) + request := c.requestList.Remove(element).(*jsonRequest) + return request + } + return nil +} + +// removeAllRequests removes all the jsonRequests which contain the response +// channels for outstanding requests. +// +// This function MUST be called with the request lock held. +func (c *Client) removeAllRequests() { + c.requestMap = make(map[uint64]*list.Element) + c.requestList.Init() +} + +// trackRegisteredNtfns examines the passed command to see if it is one of the +// notification commands and updates the notification state that is used to +// automatically re-establish registered notifications on reconnects. +func (c *Client) trackRegisteredNtfns(cmd interface{}) { + // Nothing to do if the caller is not interested in notifications. + if c.ntfnHandlers == nil { + return + } + c.ntfnStateLock.Lock() + defer c.ntfnStateLock.Unlock() + switch bcmd := cmd.(type) { + case *btcjson.NotifyBlocksCmd: + c.ntfnState.notifyBlocks = true + case *btcjson.NotifyNewTransactionsCmd: + if bcmd.Verbose != nil && *bcmd.Verbose { + c.ntfnState.notifyNewTxVerbose = true + } else { + c.ntfnState.notifyNewTx = true + } + case *btcjson.NotifySpentCmd: + for _, op := range bcmd.OutPoints { + c.ntfnState.notifySpent[op] = struct{}{} + } + case *btcjson.NotifyReceivedCmd: + for _, addr := range bcmd.Addresses { + c.ntfnState.notifyReceived[addr] = struct{}{} + } + } +} + +type ( + // inMessage is the first type that an incoming message is unmarshalled into. It + // supports both requests (for notification support) and responses. + // + // The partially-unmarshaled message is a notification if the embedded ID (from + // the response) is nil. Otherwise, it is a response. + inMessage struct { + ID *float64 `json:"id"` + *rawNotification + *rawResponse + } + // rawNotification is a partially-unmarshalled JSON-RPC notification. + rawNotification struct { + Method string `json:"method"` + Params []js.RawMessage `json:"netparams"` + } + // rawResponse is a partially-unmarshalled JSON-RPC response. For this to be + // valid (according to JSON-RPC 1.0 spec), ID may not be nil. + rawResponse struct { + Result js.RawMessage `json:"result"` + Error *btcjson.RPCError `json:"error"` + } +) + +// response is the raw bytes of a JSON-RPC result, or the error if the response +// error object was non-null. +type response struct { + result []byte + err error +} + +// result checks whether the unmarshalled response contains a non-nil error, +// returning an unmarshalled json.RPCError (or an unmarshaling error) if so. +// +// If the response is not an error, the raw bytes of the request are returned +// for further unmashaling into specific result types. +func (r rawResponse) result() (result []byte, e error) { + if r.Error != nil { + return nil, r.Error + } + return r.Result, nil +} + +// handleMessage is the main handler for incoming notifications and responses. +func (c *Client) handleMessage(msg []byte) { + // Attempt to unmarshal the message as either a notification or response. + var in inMessage + in.rawResponse = new(rawResponse) + in.rawNotification = new(rawNotification) + e := js.Unmarshal(msg, &in) + if e != nil { + W.Ln("remote server sent invalid message:", e) + return + } + // JSON-RPC 1.0 notifications are requests with a null id. + if in.ID == nil { + ntfn := in.rawNotification + if ntfn == nil { + W.Ln("malformed notification: missing method and parameters") + return + } + if ntfn.Method == "" { + W.Ln("malformed notification: missing method") + return + } + // netparams are not optional: nil isn't valid (but len == 0 is) + if ntfn.Params == nil { + W.Ln("malformed notification: missing netparams") + return + } + // Deliver the notification. + T.Ln("received notification:", in.Method) + c.handleNotification(in.rawNotification) + return + } + + // ensure that in.ID can be converted to an integer without loss of precision + if *in.ID < 0 || *in.ID != math.Trunc(*in.ID) { + W.Ln("malformed response: invalid identifier") + return + } + if in.rawResponse == nil { + W.Ln("malformed response: missing result and error") + return + } + id := uint64(*in.ID) + // Tracef("received response for id %d (result %s)", id, in.Result) + request := c.removeRequest(id) + // Nothing more to do if there is no request associated with this reply. + if request == nil || request.responseChan == nil { + W.F("received unexpected reply: %s (id %d)", in.Result, id) + return + } + // Since the command was successful, examine it to see if it's a notification, + // and if is, add it to the notification state so it automatically be + // re-established on reconnect. + c.trackRegisteredNtfns(request.cmd) + // Deliver the response. + result, e := in.rawResponse.result() + request.responseChan <- &response{result: result, err: e} +} + +// shouldLogReadError returns whether or not the passed error, which is expected +// to have come from reading from the websocket connection in wsInHandler, +// should be logged. +func (c *Client) shouldLogReadError(e error) bool { + // No logging when the connection is being forcibly disconnected. + select { + case <-c.shutdown.Wait(): + return false + default: + } + // No logging when the connection has been disconnected. + if e == io.EOF { + return false + } + if opErr, ok := e.(*net.OpError); ok && !opErr.Temporary() { + return false + } + return true +} + +// wsInHandler handles all incoming messages for the websocket connection +// associated with the client. It must be run as a goroutine. +func (c *Client) wsInHandler() { +out: + for { + // Break out of the loop once the shutdown channel has been closed. Use a + // non-blocking select here so we fall through otherwise. + select { + case <-c.shutdown.Wait(): + break out + default: + } + _, msg, e := c.wsConn.ReadMessage() + if e != nil && e != io.ErrUnexpectedEOF && e != io.EOF { + // Log the error if it's not due to disconnecting. + if c.shouldLogReadError(e) { + T.F("websocket receive error from %s: %v %s", c.config.Host, e) + } + break out + } + c.handleMessage(msg) + } + + // Ensure the connection is closed. + c.Disconnect() + c.wg.Done() + T.Ln("RPC client input handler done for", c.config.Host) +} + +// disconnectChan returns a copy of the current disconnect channel. +// +// The channel is read protected by the client mutex, and is safe to call while +// the channel is being reassigned during a reconnect. +func (c *Client) disconnectChan() <-chan struct{} { + c.mtx.Lock() + ch := c.disconnect + c.mtx.Unlock() + return ch +} + +// wsOutHandler handles all outgoing messages for the websocket connection. +// +// It uses a buffered channel to serialize output messages while allowing the +// sender to continue running asynchronously. It must be run as a goroutine. +func (c *Client) wsOutHandler() { +out: + for { + // Send any messages ready for send until the client is disconnected closed. + select { + case msg := <-c.sendChan: + // T.Ln("### sendChan received message to send") + var e error + if e = c.wsConn.WriteMessage(websocket.TextMessage, msg); E.Chk(e) { + // T.Ln("### sendChan disconnecting client") + c.Disconnect() + break out + } + // T.Ln("### message sent") + case <-c.disconnectChan(): + break out + } + } + // Drain any channels before exiting so nothing is left waiting around send. +cleanup: + for { + select { + case <-c.sendChan: + default: + break cleanup + } + } + c.wg.Done() + T.Ln("RPC client output handler done for", c.config.Host) +} + +// sendMessage sends the passed JSON to the connected server using the websocket +// connection. It is backed by a buffered channel, so it will not block until +// the send channel is full. +func (c *Client) sendMessage(marshalledJSON []byte) { + // T.Ln("### sendMessage") + // Don't send the message if disconnected. + select { + case c.sendChan <- marshalledJSON: + // T.Ln("### message sent on channel") + case <-c.disconnectChan(): + // T.Ln("### client is disconnected") + return + } +} + +// reregisterNtfns creates and sends commands needed to re-establish the current +// notification state associated with the client. +// +// It should only be called on on reconnect by the resendRequests function. +func (c *Client) reregisterNtfns() (e error) { + // Nothing to do if the caller is not interested in notifications. + if c.ntfnHandlers == nil { + return nil + } + // In order to avoid holding the lock on the notification state for the entire + // time of the potentially long running RPCs issued below, make a copy of it and + // work from that. + // + // Also, other commands will be running concurrently which could modify the + // notification state (while not under the lock of course) which also register + // it with the remote RPC server, so this prevents double registrations. + c.ntfnStateLock.Lock() + stateCopy := c.ntfnState.Copy() + c.ntfnStateLock.Unlock() + // Reregister notifyblocks if needed. + if stateCopy.notifyBlocks { + D.Ln("reregistering [notifyblocks]") + if e := c.NotifyBlocks(); E.Chk(e) { + return e + } + } + // Reregister notifynewtransactions if needed. + if stateCopy.notifyNewTx || stateCopy.notifyNewTxVerbose { + D.F( + "reregistering [notifynewtransactions] (verbose=%v)", + stateCopy.notifyNewTxVerbose, + ) + e := c.NotifyNewTransactions(stateCopy.notifyNewTxVerbose) + if e != nil { + return e + } + } + // Reregister the combination of all previously registered notifyspent outpoints + // in one command if needed. + nsLen := len(stateCopy.notifySpent) + if nsLen > 0 { + outpoints := make([]btcjson.OutPoint, 0, nsLen) + for op := range stateCopy.notifySpent { + outpoints = append(outpoints, op) + } + D.F("reregistering [notifyspent] outpoints: %v", outpoints) + if e := c.notifySpentInternal(outpoints).Receive(); E.Chk(e) { + return e + } + } + // Reregister the combination of all previously registered notifyreceived + // addresses in one command if needed. + nrLen := len(stateCopy.notifyReceived) + if nrLen > 0 { + addresses := make([]string, 0, nrLen) + for addr := range stateCopy.notifyReceived { + addresses = append(addresses, addr) + } + D.Ln("reregistering [notifyreceived] addresses:", addresses) + if e := c.notifyReceivedInternal(addresses).Receive(); E.Chk(e) { + return e + } + } + return nil +} + +// ignoreResends is a set of all methods for requests that are "long running" +// are not be reissued by the client on reconnect. +var ignoreResends = map[string]struct{}{ + "rescan": {}, +} + +// resendRequests resends any requests that had not completed when the client +// disconnected. It is intended to be called once the client has reconnected as +// a separate goroutine. +func (c *Client) resendRequests() { + // Set the notification state back up. If anything goes wrong, disconnect the client. + if e := c.reregisterNtfns(); E.Chk(e) { + W.Ln("unable to re-establish notification state:", e) + c.Disconnect() + return + } + // Since it's possible to block on send and more requests might be added by the + // caller while resending, make a copy of all of the requests that need to be + // resent now and work from the copy. This allows the lock to be released + // quickly. + c.requestLock.Lock() + resendReqs := make([]*jsonRequest, 0, c.requestList.Len()) + var nextElem *list.Element + for e := c.requestList.Front(); e != nil; e = nextElem { + nextElem = e.Next() + jReq := e.Value.(*jsonRequest) + if _, ok := ignoreResends[jReq.method]; ok { + // If a request is not sent on reconnect, remove it from the request structures, + // since no reply is expected. + delete(c.requestMap, jReq.id) + c.requestList.Remove(e) + } else { + resendReqs = append(resendReqs, jReq) + } + } + c.requestLock.Unlock() + for _, jReq := range resendReqs { + // Stop resending commands if the client disconnected again since the next + // reconnect will handle them. + if c.Disconnected() { + return + } + T.F("sending command [%s] with id %d %s", jReq.method, jReq.id) + c.sendMessage(jReq.marshalledJSON) + } +} + +// wsReconnectHandler listens for client disconnects and automatically tries to +// reconnect with retry interval that scales based on the number of retries. +// +// It also resends any commands that had not completed when the client +// disconnected so the disconnect/reconnect process is largely transparent to +// the caller. +// +// This function is not run when the DisableAutoReconnect config options is set. +// +// This function must be run as a goroutine. +func (c *Client) wsReconnectHandler() { +out: + for { + select { + case <-c.disconnect.Wait(): + // On disconnect, fallthrough to reestablish the connection. + case <-c.shutdown.Wait(): + break out + } + reconnect: + for { + select { + case <-c.shutdown.Wait(): + break out + default: + } + wsConn, e := dial(c.config) + if e != nil { + c.retryCount++ + T.F("failed to connect to %s: %v %s", c.config.Host, e) + // IconScale the retry interval by the number of retries so there is a backoff + // up to a max of 1 minute. + scaledInterval := connectionRetryInterval.Nanoseconds() * c. + retryCount + scaledDuration := time.Duration(scaledInterval) + if scaledDuration > time.Minute { + scaledDuration = time.Minute + } + T.F( + "retrying connection to %s in %s", + c.config.Host, scaledDuration, + ) + time.Sleep(scaledDuration) + continue reconnect + } + I.Ln("reestablished connection to RPC server", c.config.Host) + // Reset the connection state and signal the reconnect happened. + c.wsConn = wsConn + c.retryCount = 0 + c.mtx.Lock() + c.disconnect = qu.T() + c.disconnected = false + c.mtx.Unlock() + // Start processing input and output for the new connection. + c.start() + // Reissue pending requests in another goroutine since the send can block. + go c.resendRequests() + // Break out of the reconnect loop back to wait for disconnect again. + break reconnect + } + } + c.wg.Done() + T.Ln("RPC client reconnect handler done for", c.config.Host) +} + +// handleSendPostMessage handles performing the passed HTTP request, reading the +// result unmarshalling it and delivering the unmarshalled result to the +// provided response channel. +func (c *Client) handleSendPostMessage(details *sendPostDetails) { + jReq := details.jsonRequest + // Tracef("sending command [%s] with id %d", jReq.method, jReq.id) + httpResponse, e := c.httpClient.Do(details.httpRequest) + if e != nil { + jReq.responseChan <- &response{err: e} + return + } + // Read the raw bytes and close the response. + var respBytes []byte + if respBytes, e = ioutil.ReadAll(httpResponse.Body); E.Chk(e) { + } + if e = httpResponse.Body.Close(); E.Chk(e) { + e = fmt.Errorf("error reading json reply: %v", e) + jReq.responseChan <- &response{err: e} + return + } + // Try to unmarshal the response as a regular JSON-RPC response. + var resp rawResponse + if e = js.Unmarshal(respBytes, &resp); E.Chk(e) { + // When the response itself isn't a valid JSON-RPC response return an error + // which includes the HTTP status code and raw response bytes. + e = fmt.Errorf("status code: %d, response: %q", httpResponse.StatusCode, string(respBytes)) + jReq.responseChan <- &response{err: e} + return + } + var res []byte + if res, e = resp.result(); E.Chk(e) { + } + jReq.responseChan <- &response{result: res, err: e} +} + +// sendPostHandler handles all outgoing messages when the client is running in +// HTTP POST mode. +// +// It uses a buffered channel to serialize output messages while allowing the +// sender to continue running asynchronously. It must be run as a goroutine. +func (c *Client) sendPostHandler() { +out: + for { + // Send any messages ready for send until the shutdown channel is closed. + select { + case details := <-c.sendPostChan: + c.handleSendPostMessage(details) + case <-c.shutdown.Wait(): + break out + } + } + // Drain any wait channels before exiting so nothing is left waiting around to + // send. +cleanup: + for { + select { + case details := <-c.sendPostChan: + details.jsonRequest.responseChan <- &response{ + result: nil, + err: ErrClientShutdown, + } + default: + break cleanup + } + } + c.wg.Done() + T.Ln("RPC client send handler done for", c.config.Host) +} + +// sendPostRequest sends the passed HTTP request to the RPC server using the +// HTTP client associated with the client. +// +// It is backed by a buffered channel so it will not block until the send +// channel is full. +func (c *Client) sendPostRequest(httpReq *http.Request, jReq *jsonRequest) { + // Don't send the message if shutting down. + select { + case <-c.shutdown.Wait(): + jReq.responseChan <- &response{result: nil, err: ErrClientShutdown} + default: + } + c.sendPostChan <- &sendPostDetails{ + jsonRequest: jReq, + httpRequest: httpReq, + } +} + +// newFutureError returns a new future result channel that already has the +// passed error waiting on the channel with the reply set to nil. This is useful +// to easily return errors from the various Async functions. +func newFutureError(e error) chan *response { + responseChan := make(chan *response, 1) + responseChan <- &response{err: e} + return responseChan +} + +// receiveFuture receives from the passed futureResult channel to extract a +// reply or any errors. +// +// The examined errors include an error in the futureResult and the error in the +// reply from the server. This will block until the result is available on the +// passed channel. +func receiveFuture(f chan *response) ([]byte, error) { + // Wait for a response on the returned channel. + r := <-f + return r.result, r.err +} + +// sendPost sends the passed request to the server by issuing an HTTP POST +// request using the provided response channel for the reply. +// +// Typically a new connection is opened and closed for each command when using +// this method, however, the underlying HTTP client might coalesce multiple +// commands depending on several factors including the remote server +// configuration. +func (c *Client) sendPost(jReq *jsonRequest) { + // Generate a request to the configured RPC server. + protocol := "http" + if c.config.TLS { + protocol = "https" + } + address := protocol + "://" + c.config.Host + bodyReader := bytes.NewReader(jReq.marshalledJSON) + httpReq, e := http.NewRequest("POST", address, bodyReader) + if e != nil { + jReq.responseChan <- &response{result: nil, err: e} + return + } + httpReq.Close = true + httpReq.Header.Set("Content-Type", "application/json") + // Configure basic access authorization. + httpReq.SetBasicAuth(c.config.User, c.config.Pass) + // Tracef("sending command [%s] with id %d", jReq.method, jReq.id) + c.sendPostRequest(httpReq, jReq) +} + +// sendRequest sends the passed json request to the associated server using the +// provided response channel for the reply. +// +// It handles both websocket and HTTP POST mode depending on the configuration +// of the client. +func (c *Client) sendRequest(jReq *jsonRequest) { + // Choose which marshal and send function to use depending on whether the client + // running in HTTP POST mode or not. + // + // When running in HTTP POST mode, the command is issued via an HTTP client. + // Otherwise, the command is issued via the asynchronous websocket channels. + if c.config.HTTPPostMode { + // T.Ln("### sending via http post mode") + c.sendPost(jReq) + return + } + // Chk whether the websocket connection has never been established, in which + // case the handler goroutines are not running. + // T.Ln("### waiting for connection established") + select { + case <-c.connEstablished.Wait(): + // T.Ln("### connEstablished") + default: + // T.Ln("### sending back error client not connected") + jReq.responseChan <- &response{err: ErrClientNotConnected} + return + } + // Add the request to the internal tracking map so the response from the remote + // server can be properly detected and routed to the response channel. Then send + // the marshalled request via the websocket connection. + if e := c.addRequest(jReq); E.Chk(e) { + // T.Ln("### error", e) + jReq.responseChan <- &response{err: e} + return + } + // Tracef("### sending command [%s] with id %d", jReq.method, jReq.id) + c.sendMessage(jReq.marshalledJSON) +} + +// sendCmd sends the passed command to the associated server and returns a +// response channel on which the reply will be delivered at some point in the +// future. +// +// It handles both websocket and HTTP POST mode depending on the configuration +// of the client. +func (c *Client) sendCmd(cmd interface{}) chan *response { + // T.Ln("### sendCmd") + // Traces(cmd) + // Get the method associated with the command. + var e error + var method string + if method, e = btcjson.CmdMethod(cmd); E.Chk(e) { + // T.Ln("### error", e) + return newFutureError(e) + } + // Marshal the command. + id := c.NextID() + var marshalledJSON []byte + if marshalledJSON, e = btcjson.MarshalCmd(id, cmd); E.Chk(e) { + return newFutureError(e) + } + // Generate the request and send it along with a channel to respond on. + responseChan := make(chan *response, 1) + jReq := &jsonRequest{ + id: id, + method: method, + cmd: cmd, + marshalledJSON: marshalledJSON, + responseChan: responseChan, + } + // T.Ln("### sending request") + c.sendRequest(jReq) + return responseChan +} + +// sendCmdAndWait sends the passed command to the associated server, waits for +// the reply, and returns the result from it. +// +// It will return the error field in the reply if there is one. +func (c *Client) sendCmdAndWait(cmd interface{}) (interface{}, error) { + // Marshal the command to JSON-RPC, send it to the connected server, and wait + // for a response on the returned channel. + return receiveFuture(c.sendCmd(cmd)) +} + +// Disconnected returns whether or not the server is disconnected. If a +// websocket client was created but never connected, this also returns false. +func (c *Client) Disconnected() bool { + if c == nil { + return true + } + c.mtx.Lock() + defer c.mtx.Unlock() + select { + case <-c.connEstablished.Wait(): + return c.disconnected + default: + return false + } +} + +// doDisconnect disconnects the websocket associated with the client if it +// hasn't already been disconnected. +// +// It will return false if the disconnect is not needed or the client is running +// in HTTP POST mode. This function is safe for concurrent access. +func (c *Client) doDisconnect() bool { + if c.config.HTTPPostMode { + return false + } + c.mtx.Lock() + defer c.mtx.Unlock() + // Nothing to do if already disconnected. + if c.disconnected { + return false + } + T.Ln("disconnecting RPC client", c.config.Host) + c.disconnect.Q() + if c.wsConn != nil { + if e := c.wsConn.Close(); E.Chk(e) { + } + } + c.disconnected = true + return true +} + +// doShutdown closes the shutdown channel and logs the shutdown unless shutdown +// is already in progress. +// +// It will return false if the shutdown is not needed. +// +// This function is safe for concurrent access. +func (c *Client) doShutdown() bool { + // Ignore the shutdown request if the client is already in the process of + // shutting down or already shutdown. + select { + case <-c.shutdown.Wait(): + return false + default: + } + T.Ln("shutting down RPC client", c.config.Host) + c.shutdown.Q() + return true +} + +// Disconnect disconnects the current websocket associated with the client. +// +// The connection will automatically be re-established unless the client was +// created with the DisableAutoReconnect flag. This function has no effect when +// the client is running in HTTP POST mode. +func (c *Client) Disconnect() { + // Nothing to do if already disconnected or running in HTTP POST mode. + if !c.doDisconnect() { + return + } + c.requestLock.Lock() + defer c.requestLock.Unlock() + // When operating without auto reconnect, send errors to any pending requests + // and shutdown the client. + if c.config.DisableAutoReconnect { + for e := c.requestList.Front(); e != nil; e = e.Next() { + req := e.Value.(*jsonRequest) + req.responseChan <- &response{ + result: nil, + err: ErrClientDisconnect, + } + } + c.removeAllRequests() + c.doShutdown() + } +} + +// Shutdown shuts down the client by disconnecting any connections associated +// with the client and, when automatic reconnect is enabled, preventing future +// attempts to reconnect. It also stops all goroutines. +func (c *Client) Shutdown() { + // Do the shutdown under the request lock to prevent clients from adding new + // requests while the client shutdown process is initiated. + c.requestLock.Lock() + defer c.requestLock.Unlock() + // Ignore the shutdown request if the client is already in the process of + // shutting down or already shutdown. + if !c.doShutdown() { + return + } + // Send the ErrClientShutdown error to any pending requests. + for e := c.requestList.Front(); e != nil; e = e.Next() { + req := e.Value.(*jsonRequest) + req.responseChan <- &response{ + result: nil, + err: ErrClientShutdown, + } + } + c.removeAllRequests() + // Disconnect the client if needed. + c.doDisconnect() +} + +// start begins processing input and output messages. +func (c *Client) start() { + T.Ln("starting RPC client", c.config.Host) + // Start the I/O processing handlers depending on whether the client is in HTTP + // POST mode or the default websocket mode. + if c.config.HTTPPostMode { + c.wg.Add(1) + go c.sendPostHandler() + } else { + c.wg.Add(3) + go func() { + if c.ntfnHandlers != nil { + if c.ntfnHandlers.OnClientConnected != nil { + c.ntfnHandlers.OnClientConnected() + } + } + c.wg.Done() + }() + go c.wsInHandler() + go c.wsOutHandler() + } +} + +// WaitForShutdown blocks until the client goroutines are stopped and the +// connection is closed. +func (c *Client) WaitForShutdown() { + c.wg.Wait() +} + +// ConnConfig describes the connection configuration parameters for the client. +type ConnConfig struct { + // Host is the IP address and port of the RPC server you want to connect to. + Host string + // Endpoint is the websocket endpoint on the RPC server. This is typically "ws". + Endpoint string + // User is the username to use to authenticate to the RPC server. + User string + // Pass is the passphrase to use to authenticate to the RPC server. + Pass string + // TLS enables transport layer security encryption. It is recommended to always + // use TLS if the RPC server supports it as otherwise your username and password + // is sent across the wire in cleartext. + TLS bool + // Certificates are the bytes for a PEM-encoded certificate chain used for the + // TLS connection. It has no effect if the DisableTLS parameter is true. + Certificates []byte + // Proxy specifies to connect through a SOCKS 5 proxy server. It may be an empty + // string if a proxy is not required. + Proxy string + // ProxyUser is an optional username to use for the proxy server if it requires + // authentication. It has no effect if the Proxy parameter is not set. + ProxyUser string + // ProxyPass is an optional password to use for the proxy server if it requires + // authentication. It has no effect if the Proxy parameter is not set. + ProxyPass string + // DisableAutoReconnect specifies the client should not automatically try to + // reconnect to the server when it has been disconnected. + DisableAutoReconnect bool + // DisableConnectOnNew specifies that a websocket client connection should not + // be tried when creating the client with New. Instead, the client is created + // and returned unconnected, and Connect must be called manually. + DisableConnectOnNew bool + // HTTPPostMode instructs the client to run using multiple independent + // connections issuing HTTP POST requests instead of using the default of + // websockets. + // + // Websockets are generally preferred as some of the features of the client such + // notifications only work with websockets, however, not all servers support the + // websocket extensions, so this flag can be set to true to use basic HTTP POST + // requests instead. + HTTPPostMode bool + // EnableBCInfoHacks is an opt provided to enable compatibility hacks when + // connecting to blockchain.info RPC server + EnableBCInfoHacks bool +} + +// newHTTPClient returns a new http client that is configured according to the +// proxy and TLS settings in the associated connection configuration. +func newHTTPClient(config *ConnConfig) (*http.Client, error) { + // Set proxy function if there is a proxy configured. + var proxyFunc func(*http.Request) (*url.URL, error) + if config.Proxy != "" { + proxyURL, e := url.Parse(config.Proxy) + if e != nil { + return nil, e + } + proxyFunc = http.ProxyURL(proxyURL) + } + // Configure TLS if needed. + var tlsConfig *tls.Config + if !config.TLS { + if len(config.Certificates) > 0 { + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(config.Certificates) + tlsConfig = &tls.Config{ + RootCAs: pool, + } + } + } + client := http.Client{ + Transport: &http.Transport{ + Proxy: proxyFunc, + TLSClientConfig: tlsConfig, + }, + } + return &client, nil +} + +// dial opens a websocket connection using the passed connection configuration +// details. +func dial(config *ConnConfig) (*websocket.Conn, error) { + // Setup TLS if not disabled. + var tlsConfig *tls.Config + var scheme = "ws" + if config.TLS { + tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + if len(config.Certificates) > 0 { + // D.Ln("no certificates for verification") + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(config.Certificates) + tlsConfig.RootCAs = pool + } + scheme = "wss" + } + // Create a websocket dialer that will be used to make the connection. It is + // modified by the proxy setting below as needed. + dialer := websocket.Dialer{TLSClientConfig: tlsConfig} + // Setup the proxy if one is configured. + if config.Proxy != "" { + proxy := &socks.Proxy{ + Addr: config.Proxy, + Username: config.ProxyUser, + Password: config.ProxyPass, + } + dialer.NetDial = proxy.Dial + } + // The RPC server requires basic authorization, so create a custom request + // header with the Authorization header set. + login := config.User + ":" + config.Pass + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) + requestHeader := make(http.Header) + requestHeader.Add("Authorization", auth) + // Dial the connection. + address := fmt.Sprintf("%s://%s/%s", scheme, config.Host, config.Endpoint) + wsConn, resp, e := dialer.Dial(address, requestHeader) + if e != nil { + // debug.SetTraceback("all") + // debug.PrintStack() + if e != websocket.ErrBadHandshake || resp == nil { + return nil, e + } + // Detect HTTP authentication error status codes. + if resp.StatusCode == http.StatusUnauthorized || + resp.StatusCode == http.StatusForbidden { + return nil, ErrInvalidAuth + } + // The connection was authenticated and the status response was ok, but the + // websocket handshake still failed, so the endpoint is invalid in some way. + if resp.StatusCode == http.StatusOK { + return nil, ErrInvalidEndpoint + } + // Return the status text from the server if none of the special cases above + // apply. + return nil, errors.New(resp.Status) + } + return wsConn, nil +} + +// New creates a new RPC client based on the provided connection configuration +// details. +// +// The notification handlers parameter may be nil if you are not interested in +// receiving notifications and will be ignored if the configuration is set to +// run in HTTP POST mode. +func New(config *ConnConfig, ntfnHandlers *NotificationHandlers, quit qu.C) (*Client, error) { + // Either open a websocket connection or create an HTTP client depending on the + // HTTP POST mode. Also, set the notification handlers to nil when running in + // HTTP POST mode. + var wsConn *websocket.Conn + var httpClient *http.Client + connEstablished := qu.T() + var start bool + if config.HTTPPostMode { + ntfnHandlers = nil + start = true + var e error + httpClient, e = newHTTPClient(config) + if e != nil { + return nil, e + } + } else { + if !config.DisableConnectOnNew { + var e error + wsConn, e = dial(config) + if e != nil { + return nil, e + } + start = true + } + } + client := &Client{ + config: config, + wsConn: wsConn, + httpClient: httpClient, + requestMap: make(map[uint64]*list.Element), + requestList: list.New(), + ntfnHandlers: ntfnHandlers, + ntfnState: newNotificationState(), + sendChan: make(chan []byte, sendBufferSize), + sendPostChan: make(chan *sendPostDetails, sendPostBufferSize), + connEstablished: connEstablished, + disconnect: qu.T(), + shutdown: qu.T(), + } + go func() { + out: + for { + select { + case <-quit.Wait(): + client.disconnect.Q() + client.shutdown.Q() + break out + } + } + }() + if start { + T.Ln("established connection to RPC server", config.Host) + connEstablished.Q() + client.start() + if !client.config.HTTPPostMode && !client.config.DisableAutoReconnect { + client.wg.Add(1) + go client.wsReconnectHandler() + } + } + return client, nil +} + +// Connect establishes the initial websocket connection. This is necessary when +// a client was created after setting the DisableConnectOnNew field of the +// Config struct. +// +// Up to tries number of connections (each after an increasing backoff) will be +// tried if the connection can not be established. The special value of 0 +// indicates an unlimited number of connection attempts. +// +// This method will error if the client is not configured for websockets, if the +// connection has already been established, or if none of the connection +// attempts were successful. +func (c *Client) Connect(tries int) (e error) { + c.mtx.Lock() + defer c.mtx.Unlock() + if c.config.HTTPPostMode { + return ErrNotWebsocketClient + } + if c.wsConn != nil { + return ErrClientAlreadyConnected + } + // Begin connection attempts. Increase the backoff after each failed attempt, up + // to a maximum of one minute. + var backoff time.Duration + for i := 0; tries == 0 || i < tries; i++ { + var wsConn *websocket.Conn + wsConn, e = dial(c.config) + if e != nil { + backoff = connectionRetryInterval * time.Duration(i+1) + if backoff > time.Minute { + backoff = time.Minute + } + time.Sleep(backoff) + continue + } + // Connection was established. Set the websocket connection member of the client + // and start the goroutines necessary to run the client. + D.Ln("established connection to RPC server", c.config.Host) + c.wsConn = wsConn + c.connEstablished.Q() + c.start() + if !c.config.DisableAutoReconnect { + c.wg.Add(1) + go c.wsReconnectHandler() + } + return nil + } + + // All connection attempts failed, so return the last error. + return e +} diff --git a/pkg/rpcclient/log.go b/pkg/rpcclient/log.go new file mode 100644 index 0000000..5752258 --- /dev/null +++ b/pkg/rpcclient/log.go @@ -0,0 +1,43 @@ +package rpcclient + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/rpcclient/mining.go b/pkg/rpcclient/mining.go new file mode 100644 index 0000000..2639081 --- /dev/null +++ b/pkg/rpcclient/mining.go @@ -0,0 +1,349 @@ +package rpcclient + +import ( + "encoding/hex" + js "encoding/json" + "errors" + "github.com/p9c/p9/pkg/block" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" +) + +// FutureGenerateResult is a future promise to deliver the result of a GenerateAsync RPC invocation (or an applicable +// error). +type FutureGenerateResult chan *response + +// Receive waits for the response promised by the future and returns a list of block hashes generated by the call. +func (r FutureGenerateResult) Receive() ([]*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a list of strings. + var result []string + e = js.Unmarshal(res, &result) + if e != nil { + return nil, e + } + // Convert each block hash to a chainhash.Hash and store a pointer to each. + convertedResult := make([]*chainhash.Hash, len(result)) + for i, hashString := range result { + convertedResult[i], e = chainhash.NewHashFromStr(hashString) + if e != nil { + return nil, e + } + } + return convertedResult, nil +} + +// GenerateAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See Generate for the blocking version and more details. +func (c *Client) GenerateAsync(numBlocks uint32) FutureGenerateResult { + cmd := btcjson.NewGenerateCmd(numBlocks) + return c.sendCmd(cmd) +} + +// Generate generates numBlocks blocks and returns their hashes. +func (c *Client) Generate(numBlocks uint32) ([]*chainhash.Hash, error) { + return c.GenerateAsync(numBlocks).Receive() +} + +// FutureGetGenerateResult is a future promise to deliver the result of a GetGenerateAsync RPC invocation (or an +// applicable error). +type FutureGetGenerateResult chan *response + +// Receive waits for the response promised by the future and returns true if the server is set to mine, otherwise false. +func (r FutureGetGenerateResult) Receive() (bool, error) { + res, e := receiveFuture(r) + if e != nil { + return false, e + } + // Unmarshal result as a boolean. + var result bool + e = js.Unmarshal(res, &result) + if e != nil { + return false, e + } + return result, nil +} + +// GetGenerateAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetGenerate for the blocking version and more details. +func (c *Client) GetGenerateAsync() FutureGetGenerateResult { + cmd := btcjson.NewGetGenerateCmd() + return c.sendCmd(cmd) +} + +// GetGenerate returns true if the server is set to mine, otherwise false. +func (c *Client) GetGenerate() (bool, error) { + return c.GetGenerateAsync().Receive() +} + +// FutureSetGenerateResult is a future promise to deliver the result of a SetGenerateAsync RPC invocation (or an +// applicable error). +type FutureSetGenerateResult chan *response + +// Receive waits for the response promised by the future and returns an error if any occurred when setting the server to +// generate coins (mine) or not. +func (r FutureSetGenerateResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// SetGenerateAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See SetGenerate for the blocking version and more details. +func (c *Client) SetGenerateAsync(enable bool, numCPUs int) FutureSetGenerateResult { + cmd := btcjson.NewSetGenerateCmd(enable, &numCPUs) + return c.sendCmd(cmd) +} + +// SetGenerate sets the server to generate coins (mine) or not. +func (c *Client) SetGenerate(enable bool, numCPUs int) (e error) { + return c.SetGenerateAsync(enable, numCPUs).Receive() +} + +// FutureGetHashesPerSecResult is a future promise to deliver the result of a GetHashesPerSecAsync RPC invocation (or an +// applicable error). +type FutureGetHashesPerSecResult chan *response + +// Receive waits for the response promised by the future and returns a recent hashes per second performance measurement +// while generating coins (mining). Zero is returned if the server is not mining. +func (r FutureGetHashesPerSecResult) Receive() (int64, error) { + res, e := receiveFuture(r) + if e != nil { + return -1, e + } + // Unmarshal result as an int64. + var result int64 + e = js.Unmarshal(res, &result) + if e != nil { + return 0, e + } + return result, nil +} + +// GetHashesPerSecAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetHashesPerSec for the blocking version and more +// details. +func (c *Client) GetHashesPerSecAsync() FutureGetHashesPerSecResult { + cmd := btcjson.NewGetHashesPerSecCmd() + return c.sendCmd(cmd) +} + +// GetHashesPerSec returns a recent hashes per second performance measurement while generating coins (mining). Zero is +// returned if the server is not mining. +func (c *Client) GetHashesPerSec() (int64, error) { + return c.GetHashesPerSecAsync().Receive() +} + +// FutureGetMiningInfoResult is a future promise to deliver the result of a GetMiningInfoAsync RPC invocation (or an +// applicable error). +type FutureGetMiningInfoResult chan *response + +// Receive waits for the response promised by the future and returns the mining information. +func (r FutureGetMiningInfoResult) Receive() (*btcjson.GetMiningInfoResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a getmininginfo result object. + var infoResult btcjson.GetMiningInfoResult + e = js.Unmarshal(res, &infoResult) + if e != nil { + return nil, e + } + return &infoResult, nil +} + +// GetMiningInfoAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetMiningInfo for the blocking version and more details. +func (c *Client) GetMiningInfoAsync() FutureGetMiningInfoResult { + cmd := btcjson.NewGetMiningInfoCmd() + return c.sendCmd(cmd) +} + +// GetMiningInfo returns mining information. +func (c *Client) GetMiningInfo() (*btcjson.GetMiningInfoResult, error) { + return c.GetMiningInfoAsync().Receive() +} + +// FutureGetNetworkHashPS is a future promise to deliver the result of a GetNetworkHashPSAsync RPC invocation (or an +// applicable error). +type FutureGetNetworkHashPS chan *response + +// Receive waits for the response promised by the future and returns the estimated network hashes per second for the +// block heights provided by the parameters. +func (r FutureGetNetworkHashPS) Receive() (int64, error) { + res, e := receiveFuture(r) + if e != nil { + return -1, e + } + // Unmarshal result as an int64. + var result int64 + e = js.Unmarshal(res, &result) + if e != nil { + return 0, e + } + return result, nil +} + +// GetNetworkHashPSAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetNetworkHashPS for the blocking version and more +// details. +func (c *Client) GetNetworkHashPSAsync() FutureGetNetworkHashPS { + cmd := btcjson.NewGetNetworkHashPSCmd(nil, nil) + return c.sendCmd(cmd) +} + +// GetNetworkHashPS returns the estimated network hashes per second using the default number of blocks and the most +// recent block height. GetNetworkHashPS2 to override the number of blocks to use and GetNetworkHashPS3 to override the +// height at which to calculate the estimate. +func (c *Client) GetNetworkHashPS() (int64, error) { + return c.GetNetworkHashPSAsync().Receive() +} + +// GetNetworkHashPS2Async returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetNetworkHashPS2 for the blocking version and +// more details. +func (c *Client) GetNetworkHashPS2Async(blocks int) FutureGetNetworkHashPS { + cmd := btcjson.NewGetNetworkHashPSCmd(&blocks, nil) + return c.sendCmd(cmd) +} + +// GetNetworkHashPS2 returns the estimated network hashes per second for the specified previous number of blocks working +// backwards from the most recent block height. +// +// The blocks parameter can also be -1 in which case the number of blocks since the last difficulty change will be used. +// +// See GetNetworkHashPS to use defaults and GetNetworkHashPS3 to override the height at which to calculate the estimate. +func (c *Client) GetNetworkHashPS2(blocks int) (int64, error) { + return c.GetNetworkHashPS2Async(blocks).Receive() +} + +// GetNetworkHashPS3Async returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetNetworkHashPS3 for the blocking version and more details. +func (c *Client) GetNetworkHashPS3Async(blocks, height int) FutureGetNetworkHashPS { + cmd := btcjson.NewGetNetworkHashPSCmd(&blocks, &height) + return c.sendCmd(cmd) +} + +// GetNetworkHashPS3 returns the estimated network hashes per second for the specified previous number of blocks working +// backwards from the specified block height. +// +// The blocks parameter can also be -1 in which case the number of blocks since the last difficulty change will be used. +// +// See GetNetworkHashPS and GetNetworkHashPS2 to use defaults. +func (c *Client) GetNetworkHashPS3(blocks, height int) (int64, error) { + return c.GetNetworkHashPS3Async(blocks, height).Receive() +} + +// FutureGetWork is a future promise to deliver the result of a GetWorkAsync RPC invocation (or an applicable error). +type FutureGetWork chan *response + +// Receive waits for the response promised by the future and returns the hash data to work on. +func (r FutureGetWork) Receive() (*btcjson.GetWorkResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a getwork result object. + var result btcjson.GetWorkResult + e = js.Unmarshal(res, &result) + if e != nil { + return nil, e + } + return &result, nil +} + +// GetWorkAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetWork for the blocking version and more details. +func (c *Client) GetWorkAsync() FutureGetWork { + cmd := btcjson.NewGetWorkCmd(nil) + return c.sendCmd(cmd) +} + +// GetWork returns hash data to work on. See GetWorkSubmit to submit the found solution. +func (c *Client) GetWork() (*btcjson.GetWorkResult, error) { + return c.GetWorkAsync().Receive() +} + +// FutureGetWorkSubmit is a future promise to deliver the result of a GetWorkSubmitAsync RPC invocation (or an +// applicable error). +type FutureGetWorkSubmit chan *response + +// Receive waits for the response promised by the future and returns whether or not the submitted block header was +// accepted. +func (r FutureGetWorkSubmit) Receive() (bool, error) { + res, e := receiveFuture(r) + if e != nil { + return false, e + } + // Unmarshal result as a boolean. + var accepted bool + e = js.Unmarshal(res, &accepted) + if e != nil { + return false, e + } + return accepted, nil +} + +// GetWorkSubmitAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See GetWorkSubmit for the blocking version and more details. +func (c *Client) GetWorkSubmitAsync(data string) FutureGetWorkSubmit { + cmd := btcjson.NewGetWorkCmd(&data) + return c.sendCmd(cmd) +} + +// GetWorkSubmit submits a block header which is a solution to previously requested data and returns whether or not the +// solution was accepted. See GetWork to request data to work on. +func (c *Client) GetWorkSubmit(data string) (bool, error) { + return c.GetWorkSubmitAsync(data).Receive() +} + +// FutureSubmitBlockResult is a future promise to deliver the result of a SubmitBlockAsync RPC invocation (or an +// applicable error). +type FutureSubmitBlockResult chan *response + +// Receive waits for the response promised by the future and returns an error if any occurred when submitting the block. +func (r FutureSubmitBlockResult) Receive() (e error) { + res, e := receiveFuture(r) + if e != nil { + return e + } + if string(res) != "null" { + var result string + e = js.Unmarshal(res, &result) + if e != nil { + return e + } + return errors.New(result) + } + return nil +} + +// SubmitBlockAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See SubmitBlock for the blocking version and more details. +func (c *Client) SubmitBlockAsync(block *block.Block, options *btcjson.SubmitBlockOptions) FutureSubmitBlockResult { + blockHex := "" + if block != nil { + blockBytes, e := block.Bytes() + if e != nil { + return newFutureError(e) + } + blockHex = hex.EncodeToString(blockBytes) + } + cmd := btcjson.NewSubmitBlockCmd(blockHex, options) + return c.sendCmd(cmd) +} + +// SubmitBlock attempts to submit a new block into the bitcoin network. +func (c *Client) SubmitBlock(block *block.Block, options *btcjson.SubmitBlockOptions) (e error) { + return c.SubmitBlockAsync(block, options).Receive() +} + +// TODO(davec): Implement GetBlockTemplate diff --git a/pkg/rpcclient/net.go b/pkg/rpcclient/net.go new file mode 100644 index 0000000..3683bb0 --- /dev/null +++ b/pkg/rpcclient/net.go @@ -0,0 +1,267 @@ +package rpcclient + +import ( + js "encoding/json" + + "github.com/p9c/p9/pkg/btcjson" +) + +// AddNodeCommand enumerates the available commands that the AddNode function accepts. +type AddNodeCommand string + +// Constants used to indicate the command for the AddNode function. +const ( + // ANAdd indicates the specified host should be added as a persistent peer. + ANAdd AddNodeCommand = "add" + // ANRemove indicates the specified peer should be removed. + ANRemove AddNodeCommand = "remove" + // ANOneTry indicates the specified host should try to connect once, but it should not be made persistent. + ANOneTry AddNodeCommand = "onetry" +) + +// String returns the AddNodeCommand in human-readable form. +func (cmd AddNodeCommand) String() string { + return string(cmd) +} + +// FutureAddNodeResult is a future promise to deliver the result of an AddNodeAsync RPC invocation (or an applicable +// error). +type FutureAddNodeResult chan *response + +// Receive waits for the response promised by the future and returns an error if any occurred when performing the +// specified command. +func (r FutureAddNodeResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// AddNodeAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See AddNode for the blocking version and more details. +func (c *Client) AddNodeAsync(host string, command AddNodeCommand) FutureAddNodeResult { + cmd := btcjson.NewAddNodeCmd(host, btcjson.AddNodeSubCmd(command)) + return c.sendCmd(cmd) +} + +// AddNode attempts to perform the passed command on the passed persistent peer. For example, it can be used to add or a +// remove a persistent peer, or to do a one time connection to a peer. It may not be used to remove non-persistent +// peers. +func (c *Client) AddNode(host string, command AddNodeCommand) (e error) { + return c.AddNodeAsync(host, command).Receive() +} + +// FutureNodeResult is a future promise to deliver the result of a NodeAsync RPC invocation (or an applicable error). +type FutureNodeResult chan *response + +// Receive waits for the response promised by the future and returns an error if any occurred when performing the +// specified command. +func (r FutureNodeResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// NodeAsync returns an instance of a type that can be used to get the result of the RPC at some future time by invoking +// the Receive function on the returned instance. See Node for the blocking version and more details. +func (c *Client) NodeAsync(command btcjson.NodeSubCmd, host string, + connectSubCmd *string, +) FutureNodeResult { + cmd := btcjson.NewNodeCmd(command, host, connectSubCmd) + return c.sendCmd(cmd) +} + +// Node attempts to perform the passed node command on the host. For example, it can be used to add or a remove a +// persistent peer, or to do connect or diconnect a non-persistent one. The connectSubCmd should be set either "perm" or +// "temp", depending on whether we are targetting a persistent or non-persistent peer. Passing nil will cause the +// default value to be used, which currently is "temp". +func (c *Client) Node(command btcjson.NodeSubCmd, host string, + connectSubCmd *string, +) (e error) { + return c.NodeAsync(command, host, connectSubCmd).Receive() +} + +// FutureGetAddedNodeInfoResult is a future promise to deliver the result of a GetAddedNodeInfoAsync RPC invocation (or +// an applicable error). +type FutureGetAddedNodeInfoResult chan *response + +// Receive waits for the response promised by the future and returns information about manually added (persistent) +// peers. +func (r FutureGetAddedNodeInfoResult) Receive() ([]btcjson.GetAddedNodeInfoResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal as an array of getaddednodeinfo result objects. + var nodeInfo []btcjson.GetAddedNodeInfoResult + e = js.Unmarshal(res, &nodeInfo) + if e != nil { + return nil, e + } + return nodeInfo, nil +} + +// GetAddedNodeInfoAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. See GetAddedNodeInfo for the blocking version and more +// details. +func (c *Client) GetAddedNodeInfoAsync(peer string) FutureGetAddedNodeInfoResult { + cmd := btcjson.NewGetAddedNodeInfoCmd(true, &peer) + return c.sendCmd(cmd) +} + +// GetAddedNodeInfo returns information about manually added (persistent) peers. See GetAddedNodeInfoNoDNS to retrieve +// only a list of the added (persistent) peers. +func (c *Client) GetAddedNodeInfo(peer string) ([]btcjson.GetAddedNodeInfoResult, error) { + return c.GetAddedNodeInfoAsync(peer).Receive() +} + +// FutureGetAddedNodeInfoNoDNSResult is a future promise to deliver the result of a GetAddedNodeInfoNoDNSAsync RPC +// invocation (or an applicable error). +type FutureGetAddedNodeInfoNoDNSResult chan *response + +// Receive waits for the response promised by the future and returns a list of manually added (persistent) peers. +func (r FutureGetAddedNodeInfoNoDNSResult) Receive() ([]string, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as an array of strings. + var nodes []string + e = js.Unmarshal(res, &nodes) + if e != nil { + return nil, e + } + return nodes, nil +} + +// GetAddedNodeInfoNoDNSAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetAddedNodeInfoNoDNS for the blocking version +// and more details. +func (c *Client) GetAddedNodeInfoNoDNSAsync(peer string) FutureGetAddedNodeInfoNoDNSResult { + cmd := btcjson.NewGetAddedNodeInfoCmd(false, &peer) + return c.sendCmd(cmd) +} + +// GetAddedNodeInfoNoDNS returns a list of manually added (persistent) peers. This works by setting the dns flag to +// false in the underlying RPC. See GetAddedNodeInfo to obtain more information about each added (persistent) peer. +func (c *Client) GetAddedNodeInfoNoDNS(peer string) ([]string, error) { + return c.GetAddedNodeInfoNoDNSAsync(peer).Receive() +} + +// FutureGetConnectionCountResult is a future promise to deliver the result of a GetConnectionCountAsync RPC invocation +// (or an applicable error). +type FutureGetConnectionCountResult chan *response + +// Receive waits for the response promised by the future and returns the number of active connections to other peers. +func (r FutureGetConnectionCountResult) Receive() (int64, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as an int64. + var count int64 + e = js.Unmarshal(res, &count) + if e != nil { + return 0, e + } + return count, nil +} + +// GetConnectionCountAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetConnectionCount for the blocking version and +// more details. +func (c *Client) GetConnectionCountAsync() FutureGetConnectionCountResult { + cmd := btcjson.NewGetConnectionCountCmd() + return c.sendCmd(cmd) +} + +// GetConnectionCount returns the number of active connections to other peers. +func (c *Client) GetConnectionCount() (int64, error) { + return c.GetConnectionCountAsync().Receive() +} + +// FuturePingResult is a future promise to deliver the result of a PingAsync RPC invocation (or an applicable error). +type FuturePingResult chan *response + +// Receive waits for the response promised by the future and returns the result of queueing a ping to be sent to each +// connected peer. +func (r FuturePingResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// PingAsync returns an instance of a type that can be used to get the result of the RPC at some future time by invoking +// the Receive function on the returned instance. See Ping for the blocking version and more details. +func (c *Client) PingAsync() FuturePingResult { + cmd := btcjson.NewPingCmd() + return c.sendCmd(cmd) +} + +// Ping queues a ping to be sent to each connected peer. Use the GetPeerInfo function and examine the PingTime and +// PingWait fields to access the ping times. +func (c *Client) Ping() (e error) { + return c.PingAsync().Receive() +} + +// FutureGetPeerInfoResult is a future promise to deliver the result of a GetPeerInfoAsync RPC invocation (or an +// applicable error). +type FutureGetPeerInfoResult chan *response + +// Receive waits for the response promised by the future and returns data about each connected network peer. +func (r FutureGetPeerInfoResult) Receive() ([]btcjson.GetPeerInfoResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as an array of getpeerinfo result objects. + var peerInfo []btcjson.GetPeerInfoResult + e = js.Unmarshal(res, &peerInfo) + if e != nil { + return nil, e + } + return peerInfo, nil +} + +// GetPeerInfoAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetPeerInfo for the blocking version and more details. +func (c *Client) GetPeerInfoAsync() FutureGetPeerInfoResult { + cmd := btcjson.NewGetPeerInfoCmd() + return c.sendCmd(cmd) +} + +// GetPeerInfo returns data about each connected network peer. +func (c *Client) GetPeerInfo() ([]btcjson.GetPeerInfoResult, error) { + return c.GetPeerInfoAsync().Receive() +} + +// FutureGetNetTotalsResult is a future promise to deliver the result of a GetNetTotalsAsync RPC invocation (or an +// applicable error). +type FutureGetNetTotalsResult chan *response + +// Receive waits for the response promised by the future and returns network statistics. +func (r FutureGetNetTotalsResult) Receive() (*btcjson.GetNetTotalsResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a getnettotals result object. + var totals btcjson.GetNetTotalsResult + e = js.Unmarshal(res, &totals) + if e != nil { + return nil, e + } + return &totals, nil +} + +// GetNetTotalsAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetNetTotals for the blocking version and more details. +func (c *Client) GetNetTotalsAsync() FutureGetNetTotalsResult { + cmd := btcjson.NewGetNetTotalsCmd() + return c.sendCmd(cmd) +} + +// GetNetTotals returns network traffic statistics. +func (c *Client) GetNetTotals() (*btcjson.GetNetTotalsResult, error) { + return c.GetNetTotalsAsync().Receive() +} diff --git a/pkg/rpcclient/notify.go b/pkg/rpcclient/notify.go new file mode 100644 index 0000000..e439394 --- /dev/null +++ b/pkg/rpcclient/notify.go @@ -0,0 +1,1158 @@ +package rpcclient + +import ( + "bytes" + "encoding/hex" + js "encoding/json" + "errors" + "fmt" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "time" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +var ( + // ErrWebsocketsRequired is an error to describe the condition where the caller is trying to use a websocket-only + // feature, such as requesting notifications or other websocket requests when the client is configured to run in + // HTTP POST mode. + ErrWebsocketsRequired = errors.New( + "a websocket connection is required " + + "to use this feature", + ) +) + +// notificationState is used to track the current state of successfully registered notification so the state can be +// automatically re-established on reconnect. +type notificationState struct { + notifyBlocks bool + notifyNewTx bool + notifyNewTxVerbose bool + notifyReceived map[string]struct{} + notifySpent map[btcjson.OutPoint]struct{} +} + +// Copy returns a deep copy of the receiver. +func (s *notificationState) Copy() *notificationState { + var stateCopy notificationState + stateCopy.notifyBlocks = s.notifyBlocks + stateCopy.notifyNewTx = s.notifyNewTx + stateCopy.notifyNewTxVerbose = s.notifyNewTxVerbose + stateCopy.notifyReceived = make(map[string]struct{}) + for addr := range s.notifyReceived { + stateCopy.notifyReceived[addr] = struct{}{} + } + stateCopy.notifySpent = make(map[btcjson.OutPoint]struct{}) + for op := range s.notifySpent { + stateCopy.notifySpent[op] = struct{}{} + } + return &stateCopy +} + +func // newNotificationState returns a new notification state ready to be +// populated. +newNotificationState() *notificationState { + return ¬ificationState{ + notifyReceived: make(map[string]struct{}), + notifySpent: make(map[btcjson.OutPoint]struct{}), + } +} + +func // newNilFutureResult returns a new future result channel that already +// has the result waiting on the channel with the reply set to nil. +// This is useful to ignore things such as notifications when the caller didn't +// specify any notification handlers. +newNilFutureResult() chan *response { + responseChan := make(chan *response, 1) + responseChan <- &response{result: nil, err: nil} + return responseChan +} + +// NotificationHandlers defines callback function pointers to invoke with +// notifications. Since all of the functions are nil by default, all notifications are effectively ignored until +// their handlers are set to a concrete callback. +// +// NOTE: Unless otherwise documented, these handlers must NOT directly call any blocking calls on the client +// instance since the input reader goroutine blocks until the callback has completed. Doing so will result in a +// deadlock situation. +type NotificationHandlers struct { + // OnClientConnected is invoked when the client connects or reconnects to the RPC server. This callback is run async + // with the rest of the notification handlers, and is safe for blocking client requests. + OnClientConnected func() + // OnBlockConnected is invoked when a block is connected to the longest (best) chain. It will only be invoked if a + // preceding call to NotifyBlocks has been made to register for the notification and the function is non-nil. NOTE: + // Deprecated. Use OnFilteredBlockConnected instead. + OnBlockConnected func(hash *chainhash.Hash, height int32, t time.Time) + // OnFilteredBlockConnected is invoked when a block is connected to the longest (best) chain. It will only be + // invoked if a preceding call to NotifyBlocks has been made to register for the notification and the function is + // non-nil. Its parameters differ from OnBlockConnected: it receives the block's height, header, and relevant + // transactions. + OnFilteredBlockConnected func(height int32, header *wire.BlockHeader, txs []*util.Tx) + // OnBlockDisconnected is invoked when a block is disconnected from the longest (best) chain. It will only be + // invoked if a preceding call to NotifyBlocks has been made to register for the notification and the function is + // non-nil. NOTE: Deprecated. Use OnFilteredBlockDisconnected instead. + OnBlockDisconnected func(hash *chainhash.Hash, height int32, t time.Time) + // OnFilteredBlockDisconnected is invoked when a block is disconnected from the longest (best) chain. It will only + // be invoked if a preceding NotifyBlocks has been made to register for the notification and the call to function is + // non-nil. Its parameters differ from OnBlockDisconnected: it receives the block's height and header. + OnFilteredBlockDisconnected func(height int32, header *wire.BlockHeader) + // OnRecvTx is invoked when a transaction that receives funds to a registered address is received into the memory + // pool and also connected to the longest (best) chain. It will only be invoked if a preceding call to + // NotifyReceived, Rescan, or RescanEndHeight has been made to register for the notification and the function is + // non-nil. NOTE: Deprecated. Use OnRelevantTxAccepted instead. + OnRecvTx func(transaction *util.Tx, details *btcjson.BlockDetails) + // OnRedeemingTx is invoked when a transaction that spends a registered outpoint is received into the memory pool + // and also connected to the longest (best) chain. + // + // It will only be invoked if a preceding call to NotifySpent, Rescan, or RescanEndHeight has been made to register + // for the notification and the function is non-nil. + // + // NOTE: The NotifyReceived will automatically register notifications for the outpoints that are now "owned" as a + // result of receiving funds to the registered addresses. + // + // This means it is possible for this to invoked indirectly as the result of a NotifyReceived call. NOTE: + // Deprecated. Use OnRelevantTxAccepted instead. + OnRedeemingTx func(transaction *util.Tx, details *btcjson.BlockDetails) + // OnRelevantTxAccepted is invoked when an unmined transaction passes the client's transaction filter. + // + // NOTE: This is a btcsuite extension ported from github.com/decred/dcrrpcclient. + OnRelevantTxAccepted func(transaction []byte) + // OnRescanFinished is invoked after a rescan finishes due to a previous call to Rescan or RescanEndHeight. Finished + // rescans should be signaled on this notification, rather than relying on the return result of a rescan request, + // due to how pod may send various rescan notifications after the rescan request has already returned. + // + // NOTE: Deprecated. Not used with RescanBlocks. + OnRescanFinished func(hash *chainhash.Hash, height int32, blkTime time.Time) + // OnRescanProgress is invoked periodically when a rescan is underway. It will only be invoked if a preceding call + // to Rescan or RescanEndHeight has been made and the function is non-nil. + // + // NOTE: Deprecated. Not used with RescanBlocks. + OnRescanProgress func(hash *chainhash.Hash, height int32, blkTime time.Time) + // OnTxAccepted is invoked when a transaction is accepted into the memory pool. It will only be invoked if a + // preceding call to NotifyNewTransactions with the verbose flag set to false has been made to register for the + // notification and the function is non-nil. + OnTxAccepted func(hash *chainhash.Hash, amount amt.Amount) + // OnTxAccepted is invoked when a transaction is accepted into the memory pool. It will only be invoked if a + // preceding call to NotifyNewTransactions with the verbose flag set to true has been made to register for the + // notification and the function is non-nil. + OnTxAcceptedVerbose func(txDetails *btcjson.TxRawResult) + // OnPodConnected is invoked when a wallet connects or disconnects from pod. This will only be available when client + // is connected to a wallet server such as btcwallet. + OnPodConnected func(connected bool) + // OnAccountBalance is invoked with account balance updates. This will only be available when speaking to a wallet + // server such as btcwallet. + OnAccountBalance func(account string, balance amt.Amount, confirmed bool) + // OnWalletLockState is invoked when a wallet is locked or unlocked. This will only be available when client is + // connected to a wallet server such as btcwallet. + OnWalletLockState func(locked bool) + // OnUnknownNotification is invoked when an unrecognized notification is received. This typically means the + // notification handling code for this package needs to be updated for a new notification type or the caller is + // using a custom notification this package does not know about. + OnUnknownNotification func(method string, params []js.RawMessage) +} + +// handleNotification examines the passed notification type, performs conversions to get the raw notification types into +// higher level types and delivers the notification to the appropriate On handler registered with the client. +func (c *Client) handleNotification(ntfn *rawNotification) { + D.Ln("<<>>", ntfn.Method) + // Ignore the notification if the client is not interested in any notifications. + if c.ntfnHandlers == nil { + D.Ln("<<>>") + return + } + switch ntfn.Method { + // OnBlockConnected + case btcjson.BlockConnectedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnBlockConnected == nil { + D.Ln("<<>>") + return + } + blockHash, blockHeight, blockTime, e := parseChainNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid block connected notification:", e) + return + } + c.ntfnHandlers.OnBlockConnected(blockHash, blockHeight, blockTime) + // OnFilteredBlockConnected + case btcjson.FilteredBlockConnectedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnFilteredBlockConnected == nil { + D.Ln("<<>>") + return + } + blockHeight, blockHeader, transactions, e := + parseFilteredBlockConnectedParams(ntfn.Params) + if e != nil { + W.Ln( + "received invalid filtered block connected notification:", + e, + ) + return + } + c.ntfnHandlers.OnFilteredBlockConnected( + blockHeight, + blockHeader, transactions, + ) + // OnBlockDisconnected + case btcjson.BlockDisconnectedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnBlockDisconnected == nil { + D.Ln("<<>>") + return + } + blockHash, blockHeight, blockTime, e := parseChainNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid block connected notification:", e) + return + } + c.ntfnHandlers.OnBlockDisconnected(blockHash, blockHeight, blockTime) + // OnFilteredBlockDisconnected + case btcjson.FilteredBlockDisconnectedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnFilteredBlockDisconnected == nil { + D.Ln("<<>>") + return + } + blockHeight, blockHeader, e := parseFilteredBlockDisconnectedParams(ntfn.Params) + if e != nil { + W.Ln( + "received invalid filtered block disconnected"+ + " notification"+ + ":", e, + ) + return + } + c.ntfnHandlers.OnFilteredBlockDisconnected(blockHeight, blockHeader) + // OnRecvTx + case btcjson.RecvTxNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnRecvTx == nil { + D.Ln("<<>>") + return + } + tx, block, e := parseChainTxNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid recvtx notification:", e) + return + } + c.ntfnHandlers.OnRecvTx(tx, block) + // OnRedeemingTx + case btcjson.RedeemingTxNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnRedeemingTx == nil { + D.Ln("<<>>") + return + } + tx, block, e := parseChainTxNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid redeemingtx notification:", e) + return + } + c.ntfnHandlers.OnRedeemingTx(tx, block) + // OnRelevantTxAccepted + case btcjson.RelevantTxAcceptedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnRelevantTxAccepted == nil { + D.Ln("<<>>") + return + } + transaction, e := parseRelevantTxAcceptedParams(ntfn.Params) + if e != nil { + W.Ln("received invalid relevanttxaccepted notification:", e) + return + } + c.ntfnHandlers.OnRelevantTxAccepted(transaction) + // OnRescanFinished + case btcjson.RescanFinishedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnRescanFinished == nil { + D.Ln("<<>>") + return + } + hash, height, blkTime, e := parseRescanProgressParams(ntfn.Params) + if e != nil { + W.Ln("received invalid rescanfinished notification:", e) + return + } + c.ntfnHandlers.OnRescanFinished(hash, height, blkTime) + // OnRescanProgress + case btcjson.RescanProgressNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnRescanProgress == nil { + D.Ln("<<>>") + return + } + hash, height, blkTime, e := parseRescanProgressParams(ntfn.Params) + if e != nil { + W.Ln("received invalid rescanprogress notification:", e) + return + } + c.ntfnHandlers.OnRescanProgress(hash, height, blkTime) + // OnTxAccepted + case btcjson.TxAcceptedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnTxAccepted == nil { + D.Ln("<<>>") + return + } + hash, amt, e := parseTxAcceptedNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid tx accepted notification:", e) + return + } + c.ntfnHandlers.OnTxAccepted(hash, amt) + // OnTxAcceptedVerbose + case btcjson.TxAcceptedVerboseNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnTxAcceptedVerbose == nil { + D.Ln("<<>>") + return + } + rawTx, e := parseTxAcceptedVerboseNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid tx accepted verbose notification:", e) + return + } + c.ntfnHandlers.OnTxAcceptedVerbose(rawTx) + // OnPodConnected + case btcjson.PodConnectedNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnPodConnected == nil { + D.Ln("<<>>") + return + } + connected, e := parsePodConnectedNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid pod connected notification:", e) + return + } + c.ntfnHandlers.OnPodConnected(connected) + // OnAccountBalance + case btcjson.AccountBalanceNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnAccountBalance == nil { + D.Ln("<<>>") + return + } + account, bal, conf, e := parseAccountBalanceNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid account balance notification:", e) + return + } + c.ntfnHandlers.OnAccountBalance(account, bal, conf) + // OnWalletLockState + case btcjson.WalletLockStateNtfnMethod: + // Ignore the notification if the client is not interested in it. + if c.ntfnHandlers.OnWalletLockState == nil { + D.Ln("<<>>") + return + } + // The account name is not notified, so the return value is discarded. + _, locked, e := parseWalletLockStateNtfnParams(ntfn.Params) + if e != nil { + W.Ln("received invalid wallet lock state notification:", e) + return + } + c.ntfnHandlers.OnWalletLockState(locked) + // OnUnknownNotification + default: + if c.ntfnHandlers.OnUnknownNotification == nil { + D.Ln("<<>>") + return + } + c.ntfnHandlers.OnUnknownNotification(ntfn.Method, ntfn.Params) + } +} + +// wrongNumParams is an error type describing an unparseable JSON-RPC notification due to an incorrect number of +// parameters for the expected notification type. +// +// The value is the number of parameters of the invalid notification. +type wrongNumParams int + +// BTCJSONError satisfies the builtin error interface. +func (e wrongNumParams) Error() string { + return fmt.Sprintf("wrong number of parameters (%d)", e) +} + +// parseChainNtfnParams parses out the block hash and height from the parameters of blockconnected and blockdisconnected +// notifications. +func parseChainNtfnParams(params []js.RawMessage) ( + *chainhash.Hash, + int32, time.Time, error, +) { + if len(params) != 3 { + return nil, 0, time.Time{}, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a string. + var blockHashStr string + e := js.Unmarshal(params[0], &blockHashStr) + if e != nil { + return nil, 0, time.Time{}, e + } + // Unmarshal second parameter as an integer. + var blockHeight int32 + e = js.Unmarshal(params[1], &blockHeight) + if e != nil { + return nil, 0, time.Time{}, e + } + // Unmarshal third parameter as unix time. + var blockTimeUnix int64 + e = js.Unmarshal(params[2], &blockTimeUnix) + if e != nil { + return nil, 0, time.Time{}, e + } + // Create hash from block hash string. + blockHash, e := chainhash.NewHashFromStr(blockHashStr) + if e != nil { + return nil, 0, time.Time{}, e + } + // Create time.Time from unix time. + blockTime := time.Unix(blockTimeUnix, 0) + return blockHash, blockHeight, blockTime, nil +} + +// parseFilteredBlockConnectedParams parses out the parameters included in a filteredblockconnected notification. +// +// NOTE: This is a pod extension ported from github. com/decred/dcrrpcclient and requires a websocket connection. +func parseFilteredBlockConnectedParams(params []js.RawMessage) ( + int32, + *wire.BlockHeader, []*util.Tx, error, +) { + if len(params) < 3 { + return 0, nil, nil, wrongNumParams(len(params)) + } + // Unmarshal first parameter as an integer. + var blockHeight int32 + e := js.Unmarshal(params[0], &blockHeight) + if e != nil { + return 0, nil, nil, e + } + // Unmarshal second parameter as a slice of bytes. + blockHeaderBytes, e := parseHexParam(params[1]) + if e != nil { + return 0, nil, nil, e + } + // Deserialize block header from slice of bytes. + var blockHeader wire.BlockHeader + e = blockHeader.Deserialize(bytes.NewReader(blockHeaderBytes)) + if e != nil { + return 0, nil, nil, e + } + // Unmarshal third parameter as a slice of hex-encoded strings. + var hexTransactions []string + e = js.Unmarshal(params[2], &hexTransactions) + if e != nil { + return 0, nil, nil, e + } + // Create slice of transactions from slice of strings by hex-decoding. + transactions := make([]*util.Tx, len(hexTransactions)) + for i, hexTx := range hexTransactions { + transaction, e := hex.DecodeString(hexTx) + if e != nil { + return 0, nil, nil, e + } + transactions[i], e = util.NewTxFromBytes(transaction) + if e != nil { + return 0, nil, nil, e + } + } + return blockHeight, &blockHeader, transactions, nil +} + +// parseFilteredBlockDisconnectedParams parses out the parameters included in a filteredblockdisconnected notification. +// +// NOTE: This is a pod extension ported from github. com/decred/dcrrpcclient and requires a websocket connection. +func parseFilteredBlockDisconnectedParams(params []js.RawMessage) ( + int32, + *wire.BlockHeader, error, +) { + if len(params) < 2 { + return 0, nil, wrongNumParams(len(params)) + } + // Unmarshal first parameter as an integer. + var blockHeight int32 + e := js.Unmarshal(params[0], &blockHeight) + if e != nil { + return 0, nil, e + } + // Unmarshal second parameter as a slice of bytes. + blockHeaderBytes, e := parseHexParam(params[1]) + if e != nil { + return 0, nil, e + } + // Deserialize block header from slice of bytes. + var blockHeader wire.BlockHeader + e = blockHeader.Deserialize(bytes.NewReader(blockHeaderBytes)) + if e != nil { + return 0, nil, e + } + return blockHeight, &blockHeader, nil +} + +func parseHexParam(param js.RawMessage) ([]byte, error) { + var s string + e := js.Unmarshal(param, &s) + if e != nil { + return nil, e + } + return hex.DecodeString(s) +} + +// parseRelevantTxAcceptedParams parses out the parameter included in a relevanttxaccepted notification. +func parseRelevantTxAcceptedParams(params []js.RawMessage) (transaction []byte, e error) { + if len(params) < 1 { + return nil, wrongNumParams(len(params)) + } + return parseHexParam(params[0]) +} + +// parseChainTxNtfnParams parses out the transaction and optional details about the block it's mined in from the +// parameters of recvtx and redeemingtx notifications. +func parseChainTxNtfnParams(params []js.RawMessage) ( + *util.Tx, + *btcjson.BlockDetails, error, +) { + if len(params) == 0 || len(params) > 2 { + return nil, nil, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a string. + var txHex string + e := js.Unmarshal(params[0], &txHex) + if e != nil { + return nil, nil, e + } + // If present, unmarshal second optional parameter as the block details JSON object. + var block *btcjson.BlockDetails + if len(params) > 1 { + e = js.Unmarshal(params[1], &block) + if e != nil { + return nil, nil, e + } + } + // Hex decode and deserialize the transaction. + serializedTx, e := hex.DecodeString(txHex) + if e != nil { + return nil, nil, e + } + var msgTx wire.MsgTx + e = msgTx.Deserialize(bytes.NewReader(serializedTx)) + if e != nil { + return nil, nil, e + } + // TODO: Change recvtx and redeemingtx callback signatures to use nicer + // types for details about the block (block hash as a chainhash.Hash, + // block time as a time.Time, etc.). + return util.NewTx(&msgTx), block, nil +} + +// parseRescanProgressParams parses out the height of the last rescanned block from the parameters of rescanfinished and +// rescanprogress notifications. +func parseRescanProgressParams(params []js.RawMessage) (*chainhash.Hash, int32, time.Time, error) { + if len(params) != 3 { + return nil, 0, time.Time{}, wrongNumParams(len(params)) + } + // Unmarshal first parameter as an string. + var hashStr string + e := js.Unmarshal(params[0], &hashStr) + if e != nil { + return nil, 0, time.Time{}, e + } + // Unmarshal second parameter as an integer. + var height int32 + e = js.Unmarshal(params[1], &height) + if e != nil { + return nil, 0, time.Time{}, e + } + // Unmarshal third parameter as an integer. + var blkTime int64 + e = js.Unmarshal(params[2], &blkTime) + if e != nil { + return nil, 0, time.Time{}, e + } + // Decode string encoding of block hash. + hash, e := chainhash.NewHashFromStr(hashStr) + if e != nil { + return nil, 0, time.Time{}, e + } + return hash, height, time.Unix(blkTime, 0), nil +} + +// parseTxAcceptedNtfnParams parses out the transaction hash and total amount from the parameters of a txaccepted +// notification. +func parseTxAcceptedNtfnParams(params []js.RawMessage) ( + *chainhash.Hash, + amt.Amount, error, +) { + if len(params) != 2 { + return nil, 0, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a string. + var txHashStr string + e := js.Unmarshal(params[0], &txHashStr) + if e != nil { + return nil, 0, e + } + // Unmarshal second parameter as a floating point number. + var fAmt float64 + e = js.Unmarshal(params[1], &fAmt) + if e != nil { + return nil, 0, e + } + // Bounds check amount. + amt, e := amt.NewAmount(fAmt) + if e != nil { + return nil, 0, e + } + // Decode string encoding of transaction sha. + txHash, e := chainhash.NewHashFromStr(txHashStr) + if e != nil { + return nil, 0, e + } + return txHash, amt, nil +} + +// parseTxAcceptedVerboseNtfnParams parses out details about a raw transaction from the parameters of a +// txacceptedverbose notification. +func parseTxAcceptedVerboseNtfnParams(params []js.RawMessage) ( + *btcjson.TxRawResult, + error, +) { + if len(params) != 1 { + return nil, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a raw transaction result object. + var rawTx btcjson.TxRawResult + e := js.Unmarshal(params[0], &rawTx) + if e != nil { + return nil, e + } + // TODO: change txacceptedverbose notification callbacks to use nicer + // types for all details about the transaction (i.e. + // decoding hashes from their string encoding). + return &rawTx, nil +} + +// parsePodConnectedNtfnParams parses out the connection status of pod and btcwallet from the parameters of a +// podconnected notification. +func parsePodConnectedNtfnParams(params []js.RawMessage) (bool, error) { + if len(params) != 1 { + return false, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a boolean. + var connected bool + e := js.Unmarshal(params[0], &connected) + if e != nil { + return false, e + } + return connected, nil +} + +// parseAccountBalanceNtfnParams parses out the account name, total balance, and whether or not the balance is confirmed +// or unconfirmed from the parameters of an accountbalance notification. +func parseAccountBalanceNtfnParams(params []js.RawMessage) ( + account string, + balance amt.Amount, confirmed bool, e error, +) { + if len(params) != 3 { + return "", 0, false, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a string. + e = js.Unmarshal(params[0], &account) + if e != nil { + return "", 0, false, e + } + // Unmarshal second parameter as a floating point number. + var fBal float64 + e = js.Unmarshal(params[1], &fBal) + if e != nil { + return "", 0, false, e + } + // Unmarshal third parameter as a boolean. + e = js.Unmarshal(params[2], &confirmed) + if e != nil { + return "", 0, false, e + } + // Bounds check amount. + bal, e := amt.NewAmount(fBal) + if e != nil { + return "", 0, false, e + } + return account, bal, confirmed, nil +} + +// parseWalletLockStateNtfnParams parses out the account name and locked state of an account from the parameters of a +// walletlockstate notification. +func parseWalletLockStateNtfnParams(params []js.RawMessage) ( + account string, + locked bool, e error, +) { + if len(params) != 2 { + return "", false, wrongNumParams(len(params)) + } + // Unmarshal first parameter as a string. + e = js.Unmarshal(params[0], &account) + if e != nil { + return "", false, e + } + // Unmarshal second parameter as a boolean. + e = js.Unmarshal(params[1], &locked) + if e != nil { + return "", false, e + } + return account, locked, nil +} + +// FutureNotifyBlocksResult is a future promise to deliver the result of a NotifyBlocksAsync RPC invocation (or an +// applicable error). +type FutureNotifyBlocksResult chan *response + +// Receive waits for the response promised by the future and returns an +// error if the registration was not successful. +func (r FutureNotifyBlocksResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// NotifyBlocksAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See NotifyBlocks for the blocking version and more details. +// +// NOTE: This is a pod extension and requires a websocket connection. +func (c *Client) NotifyBlocksAsync() FutureNotifyBlocksResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + cmd := btcjson.NewNotifyBlocksCmd() + return c.sendCmd(cmd) +} + +// NotifyBlocks registers the client to receive notifications when blocks are connected and disconnected from the main +// chain. +// +// The notifications are delivered to the notification handlers associated with the client. Calling this function has no +// effect if there are no notification handlers and will result in an error if the client is configured to run in HTTP +// POST mode. +// +// The notifications delivered as a result of this call will be via one of or OnBlockDisconnected. NOTE: This is a pod +// extension and requires a websocket connection. +func (c *Client) NotifyBlocks() (e error) { + return c.NotifyBlocksAsync().Receive() +} + +// FutureNotifySpentResult is a future promise to deliver the result of a NotifySpentAsync RPC invocation (or an +// applicable error). +// +// NOTE: Deprecated. Use FutureLoadTxFilterResult instead. +type FutureNotifySpentResult chan *response + +// Receive waits for the response promised by the future and returns an +// error if the registration was not successful. +func (r FutureNotifySpentResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// notifySpentInternal is the same as notifySpentAsync except it accepts the converted outpoints as a parameter so the +// client can more efficiently recreate the previous notification state on reconnect. +func (c *Client) notifySpentInternal(outpoints []btcjson.OutPoint) FutureNotifySpentResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + cmd := btcjson.NewNotifySpentCmd(outpoints) + return c.sendCmd(cmd) +} + +// newOutPointFromWire constructs the json representation of a transaction outpoint from the wire type. +func newOutPointFromWire(op *wire.OutPoint) btcjson.OutPoint { + return btcjson.OutPoint{ + Hash: op.Hash.String(), + Index: op.Index, + } +} + +// NotifySpentAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See NotifySpent for the blocking version and more details. +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use LoadTxFilterAsync instead. +func (c *Client) NotifySpentAsync(outpoints []*wire.OutPoint) FutureNotifySpentResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + ops := make([]btcjson.OutPoint, 0, len(outpoints)) + for _, outpoint := range outpoints { + ops = append(ops, newOutPointFromWire(outpoint)) + } + cmd := btcjson.NewNotifySpentCmd(ops) + return c.sendCmd(cmd) +} + +// NotifySpent registers the client to receive notifications when the passed transaction outputs are spent. +// +// The notifications are delivered to the notification handlers associated with the client. Calling this function has no +// effect if there are no notification handlers and will result in an error if the client is configured to run in HTTP +// POST mode. +// +// The notifications delivered as a result of this call will be via OnRedeemingTx. +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use LoadTxFilter instead. +func (c *Client) NotifySpent(outpoints []*wire.OutPoint) (e error) { + return c.NotifySpentAsync(outpoints).Receive() +} + +// FutureNotifyNewTransactionsResult is a future promise to deliver the result of a NotifyNewTransactionsAsync RPC +// invocation (or an applicable error). +type FutureNotifyNewTransactionsResult chan *response + +// Receive waits for the response promised by the future and returns an error if the registration was not successful. +func (r FutureNotifyNewTransactionsResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// NotifyNewTransactionsAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See NotifyNewTransactionsAsync for the blocking version and more details. +// +// NOTE: This is a pod extension and requires a websocket connection. +func (c *Client) NotifyNewTransactionsAsync(verbose bool) FutureNotifyNewTransactionsResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + cmd := btcjson.NewNotifyNewTransactionsCmd(&verbose) + return c.sendCmd(cmd) +} + +// NotifyNewTransactions registers the client to receive notifications every time a new transaction is accepted to the +// memory pool. +// +// The notifications are delivered to the notification handlers associated with the client. Calling this function has no +// effect if there are no notification handlers and will result in an error if the client is configured to run in HTTP +// POST mode. +// +// The notifications delivered as a result of this call will be via one of OnTxAccepted (when verbose is false) or +// OnTxAcceptedVerbose ( when verbose is true). NOTE: This is a pod extension and requires a websocket connection. +func (c *Client) NotifyNewTransactions(verbose bool) (e error) { + return c.NotifyNewTransactionsAsync(verbose).Receive() +} + +// FutureNotifyReceivedResult is a future promise to deliver the result of a NotifyReceivedAsync RPC invocation (or an +// applicable error). +// +// NOTE: Deprecated. Use FutureLoadTxFilterResult instead. +type FutureNotifyReceivedResult chan *response + +// Receive waits for the response promised by the future and returns an error if the registration was not successful. +func (r FutureNotifyReceivedResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// notifyReceivedInternal is the same as notifyReceivedAsync except it accepts the converted addresses as a parameter so +// the client can more efficiently recreate the previous notification state on reconnect. +func (c *Client) notifyReceivedInternal(addresses []string) FutureNotifyReceivedResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + // Convert addresses to strings. + cmd := btcjson.NewNotifyReceivedCmd(addresses) + return c.sendCmd(cmd) +} + +// NotifyReceivedAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See NotifyReceived for the blocking version and more details. +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use LoadTxFilterAsync instead. +func (c *Client) NotifyReceivedAsync(addresses []btcaddr.Address) FutureNotifyReceivedResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + // Convert addresses to strings. + addrs := make([]string, 0, len(addresses)) + for _, addr := range addresses { + addrs = append(addrs, addr.String()) + } + cmd := btcjson.NewNotifyReceivedCmd(addrs) + return c.sendCmd(cmd) +} + +// NotifyReceived registers the client to receive notifications every time a new transaction which pays to one of the +// passed addresses is accepted to memory pool or in a block connected to the block chain. +// +// In addition, when one of these transactions is detected, the client is also automatically registered for +// notifications when the new transaction outpoints the address now has available are spent ( +// +// See NotifySpent). The notifications are delivered to the notification handlers associated with the client. +// +// Calling this function has no effect if there are no notification handlers and will result in an error if the client +// is configured to run in HTTP POST mode. +// +// The notifications delivered as a result of this call will be via one of *OnRecvTx (for transactions that receive +// funds to one of the passed addresses) or OnRedeemingTx ( for transactions which spend from one of the outpoints which +// are automatically registered upon receipt of funds to the address). +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use LoadTxFilter instead. +func (c *Client) NotifyReceived(addresses []btcaddr.Address) (e error) { + return c.NotifyReceivedAsync(addresses).Receive() +} + +// FutureRescanResult is a future promise to deliver the result of a RescanAsync or RescanEndHeightAsync RPC invocation +// ( or an applicable error). +// +// NOTE: Deprecated. Use FutureRescanBlocksResult instead. +type FutureRescanResult chan *response + +// Receive waits for the response promised by the future and returns an error if the rescan was not successful. +func (r FutureRescanResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// RescanAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See Rescan for the blocking version and more details. +// +// NOTE: Rescan requests are not issued on client reconnect and must be performed manually (ideally with a new start +// height based on the last rescan progress notification). +// +// See the OnClientConnected notification callback for a good call site to reissue rescan requests on connect and +// reconnect. +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use RescanBlocksAsync instead. +func (c *Client) RescanAsync( + startBlock *chainhash.Hash, + addresses []btcaddr.Address, + outpoints []*wire.OutPoint, +) FutureRescanResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + // Convert block hashes to strings. + var startBlockHashStr string + if startBlock != nil { + startBlockHashStr = startBlock.String() + } + // Convert addresses to strings. + addrs := make([]string, 0, len(addresses)) + for _, addr := range addresses { + addrs = append(addrs, addr.String()) + } + // Convert outpoints. + ops := make([]btcjson.OutPoint, 0, len(outpoints)) + for _, op := range outpoints { + ops = append(ops, newOutPointFromWire(op)) + } + cmd := btcjson.NewRescanCmd(startBlockHashStr, addrs, ops, nil) + return c.sendCmd(cmd) +} + +// Rescan rescans the block chain starting from the provided starting block to the end of the longest chain for +// transactions that pay to the passed addresses and transactions which spend the passed outpoints. +// +// The notifications of found transactions are delivered to the notification handlers associated with client and this +// call will not return until the rescan has completed. Calling this function has no effect if there are no notification +// handlers and will result in an error if the client is configured to run in HTTP POST mode. +// +// The notifications delivered as a result of this call will be via one of OnRedeemingTx (for transactions which spend +// from the one of the passed outpoints), OnRecvTx (for transactions that receive funds to one of the passed addresses), +// and OnRescanProgress (for rescan progress updates). +// +// See RescanEndBlock to also specify an ending block to finish the rescan without continuing through the best block on +// the main chain. +// +// NOTE: Rescan requests are not issued on client reconnect and must be performed manually (ideally with a new start +// height based on the last rescan progress notification). +// +// See the OnClientConnected notification callback for a good call site to reissue rescan requests on connect and +// reconnect. +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use RescanBlocks instead. +func (c *Client) Rescan( + startBlock *chainhash.Hash, + addresses []btcaddr.Address, + outpoints []*wire.OutPoint, +) (e error) { + return c.RescanAsync(startBlock, addresses, outpoints).Receive() +} + +// RescanEndBlockAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See RescanEndBlock for the blocking version and more details. +// +// NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use RescanBlocksAsync instead. +func (c *Client) RescanEndBlockAsync( + startBlock *chainhash.Hash, + addresses []btcaddr.Address, outpoints []*wire.OutPoint, + endBlock *chainhash.Hash, +) FutureRescanResult { + // Not supported in HTTP POST mode. + if c.config.HTTPPostMode { + return newFutureError(ErrWebsocketsRequired) + } + // Ignore the notification if the client is not interested in notifications. + if c.ntfnHandlers == nil { + return newNilFutureResult() + } + // Convert block hashes to strings. + var startBlockHashStr, endBlockHashStr string + if startBlock != nil { + startBlockHashStr = startBlock.String() + } + if endBlock != nil { + endBlockHashStr = endBlock.String() + } + // Convert addresses to strings. + addrs := make([]string, 0, len(addresses)) + for _, addr := range addresses { + addrs = append(addrs, addr.String()) + } + // Convert outpoints. + ops := make([]btcjson.OutPoint, 0, len(outpoints)) + for _, op := range outpoints { + ops = append(ops, newOutPointFromWire(op)) + } + cmd := btcjson.NewRescanCmd( + startBlockHashStr, addrs, ops, + &endBlockHashStr, + ) + return c.sendCmd(cmd) +} + +// RescanEndHeight rescans the block chain starting from the provided starting block up to the provided ending block for +// transactions that pay to the passed addresses and transactions which spend the passed outpoints. +// +// The notifications of found transactions are delivered to the notification handlers associated with client and this +// call will not return until the rescan has completed. +// +// Calling this function has no effect if there are no notification handlers and will result in an error if the client +// is configured to run in HTTP POST mode. +// +// The notifications delivered as a result of this call will be via one of OnRedeemingTx (for transactions which spend +// from the one of the passed outpoints), OnRecvTx (for transactions that receive funds to one of the passed addresses), +// and OnRescanProgress (for rescan progress updates). +// +// See Rescan to also perform a rescan through current end of the longest +// chain. NOTE: This is a pod extension and requires a websocket connection. +// +// NOTE: Deprecated. Use RescanBlocks instead. +func (c *Client) RescanEndHeight( + startBlock *chainhash.Hash, + addresses []btcaddr.Address, outpoints []*wire.OutPoint, + endBlock *chainhash.Hash, +) (e error) { + return c.RescanEndBlockAsync( + startBlock, addresses, outpoints, + endBlock, + ).Receive() +} + +// FutureLoadTxFilterResult is a future promise to deliver the result of a LoadTxFilterAsync RPC invocation (or an +// applicable error). +// +// NOTE: This is a pod extension ported from github.com/decred/dcrrpcclient and requires a websocket connection. +type FutureLoadTxFilterResult chan *response + +// Receive waits for the response promised by the future and returns an error if the registration was not successful. +// +// NOTE: This is a pod extension ported from github.com/decred/dcrrpcclient and requires a websocket connection. +func (r FutureLoadTxFilterResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// LoadTxFilterAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See LoadTxFilter for the blocking version and more details. +// +// NOTE: This is a pod extension ported from github. com/decred/dcrrpcclient and requires a websocket connection. +func (c *Client) LoadTxFilterAsync( + reload bool, addresses []btcaddr.Address, + outPoints []wire.OutPoint, +) FutureLoadTxFilterResult { + addrStrs := make([]string, len(addresses)) + for i, a := range addresses { + addrStrs[i] = a.EncodeAddress() + } + outPointObjects := make([]btcjson.OutPoint, len(outPoints)) + for i := range outPoints { + outPointObjects[i] = btcjson.OutPoint{ + Hash: outPoints[i].Hash.String(), + Index: outPoints[i].Index, + } + } + cmd := btcjson.NewLoadTxFilterCmd(reload, addrStrs, outPointObjects) + return c.sendCmd(cmd) +} + +// LoadTxFilter loads reloads or adds data to a websocket client's transaction filter. +// +// The filter is consistently updated based on inspected transactions during mempool acceptance, block acceptance, and +// for all rescanned blocks. +// +// NOTE: This is a pod extension ported from github. com/decred/dcrrpcclient and requires a websocket connection. +func (c *Client) LoadTxFilter(reload bool, addresses []btcaddr.Address, outPoints []wire.OutPoint) (e error) { + return c.LoadTxFilterAsync(reload, addresses, outPoints).Receive() +} diff --git a/pkg/rpcclient/rawreq.go b/pkg/rpcclient/rawreq.go new file mode 100644 index 0000000..f613fef --- /dev/null +++ b/pkg/rpcclient/rawreq.go @@ -0,0 +1,64 @@ +package rpcclient + +import ( + js "encoding/json" + "errors" + + "github.com/p9c/p9/pkg/btcjson" +) + +// FutureRawResult is a future promise to deliver the result of a RawRequest RPC invocation (or an applicable error). +type FutureRawResult chan *response + +// Receive waits for the response promised by the future and returns the raw response, or an error if the request was unsuccessful. +func (r FutureRawResult) Receive() (js.RawMessage, error) { + return receiveFuture(r) +} + +// RawRequestAsync returns an instance of a type that can be used to get the result of a custom RPC request at some +// future time by invoking the Receive function on the returned instance. +// +// See RawRequest for the blocking version and more details. +func (c *Client) RawRequestAsync(method string, params []js.RawMessage) FutureRawResult { + // Method may not be empty. + if method == "" { + return newFutureError(errors.New("no method")) + } + // Marshal parameters as "[]" instead of "null" when no parameters are passed. + if params == nil { + params = []js.RawMessage{} + } + // Create a raw JSON-RPC request using the provided method and netparams and marshal it. This is done rather than + // using the sendCmd function since that relies on marshalling registered json commands rather than custom commands. + id := c.NextID() + rawRequest := &btcjson.Request{ + Jsonrpc: "1.0", + ID: id, + Method: method, + Params: params, + } + marshalledJSON, e := js.Marshal(rawRequest) + if e != nil { + return newFutureError(e) + } + // Generate the request and send it along with a channel to respond on. + responseChan := make(chan *response, 1) + jReq := &jsonRequest{ + id: id, + method: method, + cmd: nil, + marshalledJSON: marshalledJSON, + responseChan: responseChan, + } + c.sendRequest(jReq) + return responseChan +} + +// RawRequest allows the caller to send a raw or custom request to the server. +// +// This method may be used to send and receive requests and responses for requests that are not handled by this client +// package, or to proxy partially unmarshalled requests to another JSON-RPC server if a request cannot be handled +// directly. +func (c *Client) RawRequest(method string, params []js.RawMessage) (js.RawMessage, error) { + return c.RawRequestAsync(method, params).Receive() +} diff --git a/pkg/rpcclient/rawtxs.go b/pkg/rpcclient/rawtxs.go new file mode 100644 index 0000000..31ac9aa --- /dev/null +++ b/pkg/rpcclient/rawtxs.go @@ -0,0 +1,598 @@ +package rpcclient + +import ( + "bytes" + "encoding/hex" + js "encoding/json" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// SigHashType enumerates the available signature hashing types that the function accepts. +type SigHashType string + +// Constants used to indicate the signature hash type for SignRawTransaction. +const ( + // SigHashAll indicates ALL of the outputs should be signed. + SigHashAll SigHashType = "ALL" + // SigHashNone indicates NONE of the outputs should be signed. This can be thought of as specifying the signer does + // not care where the bitcoins go. + SigHashNone SigHashType = "NONE" + // SigHashSingle indicates that a SINGLE output should be signed. This can be thought of specifying the signer only + // cares about where ONE of the outputs goes, but not any of the others. + SigHashSingle SigHashType = "SINGLE" + // SigHashAllAnyoneCanPay indicates that signer does not care where the other inputs to the transaction come from, + // so it allows other people to add inputs. In addition, it uses the SigHashAll signing method for outputs. + SigHashAllAnyoneCanPay SigHashType = "ALL|ANYONECANPAY" + // SigHashNoneAnyoneCanPay indicates that signer does not care where the other inputs to the transaction come from, + // so it allows other people to add inputs. In addition, it uses the SigHashNone signing method for outputs. + SigHashNoneAnyoneCanPay SigHashType = "NONE|ANYONECANPAY" + // SigHashSingleAnyoneCanPay indicates that signer does not care where the other inputs to the transaction come + // from, so it allows other people to add inputs. In addition, it uses the SigHashSingle signing method for outputs. + SigHashSingleAnyoneCanPay SigHashType = "SINGLE|ANYONECANPAY" +) + +// String returns the SighHashType in human-readable form. +func (s SigHashType) String() string { + return string(s) +} + +// FutureGetRawTransactionResult is a future promise to deliver the result of a GetRawTransactionAsync RPC invocation +// (or an applicable error). +type FutureGetRawTransactionResult chan *response + +// Receive waits for the response promised by the future and returns a transaction given its hash. +func (r FutureGetRawTransactionResult) Receive() (*util.Tx, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var txHex string + e = js.Unmarshal(res, &txHex) + if e != nil { + return nil, e + } + // Decode the serialized transaction hex to raw bytes. + serializedTx, e := hex.DecodeString(txHex) + if e != nil { + return nil, e + } + // Deserialize the transaction and return it. + var msgTx wire.MsgTx + if e := msgTx.Deserialize(bytes.NewReader(serializedTx)); E.Chk(e) { + return nil, e + } + return util.NewTx(&msgTx), nil +} + +// GetRawTransactionAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See GetRawTransaction for the blocking version and +// more details. +func (c *Client) GetRawTransactionAsync(txHash *chainhash.Hash) FutureGetRawTransactionResult { + hash := "" + if txHash != nil { + hash = txHash.String() + } + cmd := btcjson.NewGetRawTransactionCmd(hash, btcjson.Int(0)) + return c.sendCmd(cmd) +} + +// GetRawTransaction returns a transaction given its hash. +// +// See GetRawTransactionVerbose to obtain additional information about the transaction. +func (c *Client) GetRawTransaction(txHash *chainhash.Hash) (*util.Tx, error) { + return c.GetRawTransactionAsync(txHash).Receive() +} + +// FutureGetRawTransactionVerboseResult is a future promise to deliver the result of a GetRawTransactionVerboseAsync RPC +// invocation (or an applicable error). +type FutureGetRawTransactionVerboseResult chan *response + +// Receive waits for the response promised by the future and returns information about a transaction given its hash. +func (r FutureGetRawTransactionVerboseResult) Receive() (*btcjson.TxRawResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a gettrawtransaction result object. + var rawTxResult btcjson.TxRawResult + e = js.Unmarshal(res, &rawTxResult) + if e != nil { + return nil, e + } + return &rawTxResult, nil +} + +// GetRawTransactionVerboseAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See GetRawTransactionVerbose for the blocking version and more details. +func (c *Client) GetRawTransactionVerboseAsync(txHash *chainhash.Hash) FutureGetRawTransactionVerboseResult { + hash := "" + if txHash != nil { + hash = txHash.String() + } + cmd := btcjson.NewGetRawTransactionCmd(hash, btcjson.Int(1)) + return c.sendCmd(cmd) +} + +// GetRawTransactionVerbose returns information about a transaction given its hash. See GetRawTransaction to obtain only +// the transaction already deserialized. +func (c *Client) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) { + return c.GetRawTransactionVerboseAsync(txHash).Receive() +} + +// FutureDecodeRawTransactionResult is a future promise to deliver the result of a DecodeRawTransactionAsync RPC +// invocation (or an applicable error). +type FutureDecodeRawTransactionResult chan *response + +// Receive waits for the response promised by the future and returns information about a transaction given its +// serialized bytes. +func (r FutureDecodeRawTransactionResult) Receive() (*btcjson.TxRawResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a decoderawtransaction result object. + var rawTxResult btcjson.TxRawResult + e = js.Unmarshal(res, &rawTxResult) + if e != nil { + return nil, e + } + return &rawTxResult, nil +} + +// DecodeRawTransactionAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See DecodeRawTransaction for the blocking version and +// more details. +func (c *Client) DecodeRawTransactionAsync(serializedTx []byte) FutureDecodeRawTransactionResult { + txHex := hex.EncodeToString(serializedTx) + cmd := btcjson.NewDecodeRawTransactionCmd(txHex) + return c.sendCmd(cmd) +} + +// DecodeRawTransaction returns information about a transaction given its serialized bytes. +func (c *Client) DecodeRawTransaction(serializedTx []byte) (*btcjson.TxRawResult, error) { + return c.DecodeRawTransactionAsync(serializedTx).Receive() +} + +// FutureCreateRawTransactionResult is a future promise to deliver the result of a CreateRawTransactionAsync RPC +// invocation (or an applicable error). +type FutureCreateRawTransactionResult chan *response + +// Receive waits for the response promised by the future and returns a new transaction spending the provided inputs and +// sending to the provided addresses. +func (r FutureCreateRawTransactionResult) Receive() (*wire.MsgTx, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var txHex string + e = js.Unmarshal(res, &txHex) + if e != nil { + return nil, e + } + // Decode the serialized transaction hex to raw bytes. + serializedTx, e := hex.DecodeString(txHex) + if e != nil { + return nil, e + } + // Deserialize the transaction and return it. + var msgTx wire.MsgTx + if e := msgTx.Deserialize(bytes.NewReader(serializedTx)); E.Chk(e) { + return nil, e + } + return &msgTx, nil +} + +// CreateRawTransactionAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See CreateRawTransaction for the blocking version and +// more details. +func (c *Client) CreateRawTransactionAsync( + inputs []btcjson.TransactionInput, + amounts map[btcaddr.Address]amt.Amount, lockTime *int64, +) FutureCreateRawTransactionResult { + convertedAmts := make(map[string]float64, len(amounts)) + for addr, amount := range amounts { + convertedAmts[addr.String()] = amount.ToDUO() + } + cmd := btcjson.NewCreateRawTransactionCmd(inputs, convertedAmts, lockTime) + return c.sendCmd(cmd) +} + +// CreateRawTransaction returns a new transaction spending the provided inputs and sending to the provided addresses. +func (c *Client) CreateRawTransaction( + inputs []btcjson.TransactionInput, + amounts map[btcaddr.Address]amt.Amount, lockTime *int64, +) (*wire.MsgTx, error) { + return c.CreateRawTransactionAsync(inputs, amounts, lockTime).Receive() +} + +// FutureSendRawTransactionResult is a future promise to deliver the result of a SendRawTransactionAsync RPC invocation +// (or an applicable error). +type FutureSendRawTransactionResult chan *response + +// Receive waits for the response promised by the future and returns the result of submitting the encoded transaction to +// the server which then relays it to the network. +func (r FutureSendRawTransactionResult) Receive() (*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var txHashStr string + e = js.Unmarshal(res, &txHashStr) + if e != nil { + return nil, e + } + return chainhash.NewHashFromStr(txHashStr) +} + +// SendRawTransactionAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See SendRawTransaction for the blocking version and more details. +func (c *Client) SendRawTransactionAsync(tx *wire.MsgTx, allowHighFees bool) FutureSendRawTransactionResult { + txHex := "" + if tx != nil { + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if e := tx.Serialize(buf); E.Chk(e) { + return newFutureError(e) + } + txHex = hex.EncodeToString(buf.Bytes()) + } + cmd := btcjson.NewSendRawTransactionCmd(txHex, &allowHighFees) + return c.sendCmd(cmd) +} + +// SendRawTransaction submits the encoded transaction to the server which will then relay it to the network. +func (c *Client) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + return c.SendRawTransactionAsync(tx, allowHighFees).Receive() +} + +// FutureSignRawTransactionResult is a future promise to deliver the result of one of the SignRawTransactionAsync family +// of RPC invocations (or an applicable error). +type FutureSignRawTransactionResult chan *response + +// Receive waits for the response promised by the future and returns the signed transaction as well as whether or not all inputs are now signed. +func (r FutureSignRawTransactionResult) Receive() (*wire.MsgTx, bool, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, false, e + } + // Unmarshal as a signrawtransaction result. + var signRawTxResult btcjson.SignRawTransactionResult + e = js.Unmarshal(res, &signRawTxResult) + if e != nil { + return nil, false, e + } + // Decode the serialized transaction hex to raw bytes. + serializedTx, e := hex.DecodeString(signRawTxResult.Hex) + if e != nil { + return nil, false, e + } + // Deserialize the transaction and return it. + var msgTx wire.MsgTx + if e := msgTx.Deserialize(bytes.NewReader(serializedTx)); E.Chk(e) { + return nil, false, e + } + return &msgTx, signRawTxResult.Complete, nil +} + +// SignRawTransactionAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on returned instance. See SignRawTransaction for the blocking version and more +// details. +func (c *Client) SignRawTransactionAsync(tx *wire.MsgTx) FutureSignRawTransactionResult { + txHex := "" + if tx != nil { + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if e := tx.Serialize(buf); E.Chk(e) { + return newFutureError(e) + } + txHex = hex.EncodeToString(buf.Bytes()) + } + cmd := btcjson.NewSignRawTransactionCmd(txHex, nil, nil, nil) + return c.sendCmd(cmd) +} + +// SignRawTransaction signs inputs for the passed transaction and returns the signed transaction as well as whether or +// not all inputs are now signed. +// +// This function assumes the RPC server already knows the input transactions and private keys for the passed transaction +// which needs to be signed and uses the default signature hash type. +// +// Use one of the SignRawTransaction# variants to specify that information if needed. +func (c *Client) SignRawTransaction(tx *wire.MsgTx) (*wire.MsgTx, bool, error) { + return c.SignRawTransactionAsync(tx).Receive() +} + +// SignRawTransaction2Async returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive on the returned instance. +// +// See SignRawTransaction2 for the blocking version and more details. +func (c *Client) SignRawTransaction2Async(tx *wire.MsgTx, inputs []btcjson.RawTxInput) FutureSignRawTransactionResult { + txHex := "" + if tx != nil { + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if e := tx.Serialize(buf); E.Chk(e) { + return newFutureError(e) + } + txHex = hex.EncodeToString(buf.Bytes()) + } + cmd := btcjson.NewSignRawTransactionCmd(txHex, &inputs, nil, nil) + return c.sendCmd(cmd) +} + +// SignRawTransaction2 signs inputs for the passed transaction given the list information about the input transactions +// needed to perform the signing process. +// +// This only input transactions that need to be specified are ones the RPC server does not already know. Already known +// input transactions will be merged with the specified transactions. +// +// See SignRawTransaction if the RPC server already knows the input transactions. +func (c *Client) SignRawTransaction2(tx *wire.MsgTx, inputs []btcjson.RawTxInput) (*wire.MsgTx, bool, error) { + return c.SignRawTransaction2Async(tx, inputs).Receive() +} + +// SignRawTransaction3Async returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See SignRawTransaction3 for the blocking version and more details. +func (c *Client) SignRawTransaction3Async( + tx *wire.MsgTx, + inputs []btcjson.RawTxInput, + privKeysWIF []string, +) FutureSignRawTransactionResult { + txHex := "" + if tx != nil { + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if e := tx.Serialize(buf); E.Chk(e) { + return newFutureError(e) + } + txHex = hex.EncodeToString(buf.Bytes()) + } + cmd := btcjson.NewSignRawTransactionCmd( + txHex, &inputs, &privKeysWIF, + nil, + ) + return c.sendCmd(cmd) +} + +// SignRawTransaction3 signs inputs for the passed transaction given the list of information about extra input +// transactions and a list of private keys needed to perform the signing process. The private keys must be in wallet +// import format (WIF). +// +// This only input transactions that need to be specified are ones the RPC server does not already know. Already known +// input transactions will be merged with the specified transactions. This means the list of transaction inputs can be +// nil if the RPC server already knows them all. +// +// NOTE: Unlike the merging functionality of the input transactions, ONLY the specified private keys will be used, so +// even if the server already knows some of the private keys, they will NOT be used. +// +// See SignRawTransaction if the RPC server already knows the input transactions and private keys or SignRawTransaction2 +// if it already knows the private keys. +func (c *Client) SignRawTransaction3( + tx *wire.MsgTx, + inputs []btcjson.RawTxInput, + privKeysWIF []string, +) (*wire.MsgTx, bool, error) { + return c.SignRawTransaction3Async(tx, inputs, privKeysWIF).Receive() +} + +// SignRawTransaction4Async returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See SignRawTransaction4 for the blocking version and more details. +func (c *Client) SignRawTransaction4Async( + tx *wire.MsgTx, + inputs []btcjson.RawTxInput, privKeysWIF []string, + hashType SigHashType, +) FutureSignRawTransactionResult { + txHex := "" + if tx != nil { + // Serialize the transaction and convert to hex string. + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if e := tx.Serialize(buf); E.Chk(e) { + return newFutureError(e) + } + txHex = hex.EncodeToString(buf.Bytes()) + } + cmd := btcjson.NewSignRawTransactionCmd( + txHex, &inputs, &privKeysWIF, + btcjson.String(string(hashType)), + ) + return c.sendCmd(cmd) +} + +// SignRawTransaction4 signs inputs for the passed transaction using the the specified signature hash type given the +// list of information about extra input transactions and a potential list of private keys needed to perform the signing +// process. +// +// The private keys, if specified, must be in wallet import format (WIF). The only input transactions that need to be +// specified are ones the RPC server does not already know. This means the list of transaction inputs can be nil if the +// RPC server already knows them all. +// +// NOTE: Unlike the merging functionality of the input transactions, ONLY the specified private keys will be used, so +// even if the server already knows some of the private keys, they will NOT be used. The list of private keys can be nil +// in which case any private keys the RPC server knows will be used. +// +// This function should only used if a non-default signature hash type is desired. +// +// Otherwise, see SignRawTransaction if the RPC server already knows the input transactions and private keys, +// SignRawTransaction2 if it already knows the private keys, or SignRawTransaction3 if it does not know both. +func (c *Client) SignRawTransaction4( + tx *wire.MsgTx, + inputs []btcjson.RawTxInput, privKeysWIF []string, + hashType SigHashType, +) (*wire.MsgTx, bool, error) { + return c.SignRawTransaction4Async( + tx, inputs, privKeysWIF, + hashType, + ).Receive() +} + +// FutureSearchRawTransactionsResult is a future promise to deliver the result of the SearchRawTransactionsAsync RPC +// invocation (or an applicable error). +type FutureSearchRawTransactionsResult chan *response + +// Receive waits for the response promised by the future and returns the found raw transactions. +func (r FutureSearchRawTransactionsResult) Receive() ([]*wire.MsgTx, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal as an array of strings. + var searchRawTxnsResult []string + e = js.Unmarshal(res, &searchRawTxnsResult) + if e != nil { + return nil, e + } + // Decode and deserialize each transaction. + msgTxns := make([]*wire.MsgTx, 0, len(searchRawTxnsResult)) + for _, hexTx := range searchRawTxnsResult { + // Decode the serialized transaction hex to raw bytes. + serializedTx, e := hex.DecodeString(hexTx) + if e != nil { + return nil, e + } + // Deserialize the transaction and add it to the result slice. + var msgTx wire.MsgTx + e = msgTx.Deserialize(bytes.NewReader(serializedTx)) + if e != nil { + return nil, e + } + msgTxns = append(msgTxns, &msgTx) + } + return msgTxns, nil +} + +// SearchRawTransactionsAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. See SearchRawTransactions for the blocking version +// and more details. +func (c *Client) SearchRawTransactionsAsync( + address btcaddr.Address, skip, count int, reverse bool, + filterAddrs []string, +) FutureSearchRawTransactionsResult { + addr := address.EncodeAddress() + verbose := btcjson.Int(0) + cmd := btcjson.NewSearchRawTransactionsCmd( + addr, verbose, &skip, &count, + nil, &reverse, &filterAddrs, + ) + return c.sendCmd(cmd) +} + +// SearchRawTransactions returns transactions that involve the passed address. +// +// NOTE: Chain servers do not typically provide this capability unless it has specifically been enabled. +// +// See SearchRawTransactionsVerbose to retrieve a list of data structures with information about the transactions +// instead of the transactions themselves. +func (c *Client) SearchRawTransactions( + address btcaddr.Address, + skip, count int, + reverse bool, + filterAddrs []string, +) ([]*wire.MsgTx, error) { + return c.SearchRawTransactionsAsync(address, skip, count, reverse, filterAddrs).Receive() +} + +// FutureSearchRawTransactionsVerboseResult is a future promise to deliver the result of the +// SearchRawTransactionsVerboseAsync RPC invocation (or an applicable error). +type FutureSearchRawTransactionsVerboseResult chan *response + +// Receive waits for the response promised by the future and returns the found raw transactions. +func (r FutureSearchRawTransactionsVerboseResult) Receive() ([]*btcjson.SearchRawTransactionsResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal as an array of raw transaction results. + var result []*btcjson.SearchRawTransactionsResult + e = js.Unmarshal(res, &result) + if e != nil { + return nil, e + } + return result, nil +} + +// SearchRawTransactionsVerboseAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See SearchRawTransactionsVerbose for the blocking version and more details. +func (c *Client) SearchRawTransactionsVerboseAsync( + address btcaddr.Address, skip, + count int, includePrevOut, reverse bool, filterAddrs *[]string, +) FutureSearchRawTransactionsVerboseResult { + addr := address.EncodeAddress() + verbose := btcjson.Int(1) + var prevOut *int + if includePrevOut { + prevOut = btcjson.Int(1) + } + cmd := btcjson.NewSearchRawTransactionsCmd( + addr, verbose, &skip, &count, + prevOut, &reverse, filterAddrs, + ) + return c.sendCmd(cmd) +} + +// SearchRawTransactionsVerbose returns a list of data structures that describe transactions which involve the passed +// address. NOTE: Chain servers do not typically provide this capability unless it has specifically been enabled. +// +// See SearchRawTransactions to retrieve a list of raw transactions instead. +func (c *Client) SearchRawTransactionsVerbose( + address btcaddr.Address, skip, + count int, includePrevOut, reverse bool, filterAddrs []string, +) ([]*btcjson.SearchRawTransactionsResult, error) { + return c.SearchRawTransactionsVerboseAsync( + address, skip, count, + includePrevOut, reverse, &filterAddrs, + ).Receive() +} + +// FutureDecodeScriptResult is a future promise to deliver the result of a DecodeScriptAsync RPC invocation (or an +// applicable error). +type FutureDecodeScriptResult chan *response + +// Receive waits for the response promised by the future and returns information about a script given its serialized +// bytes. +func (r FutureDecodeScriptResult) Receive() (*btcjson.DecodeScriptResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a decodescript result object. + var decodeScriptResult btcjson.DecodeScriptResult + e = js.Unmarshal(res, &decodeScriptResult) + if e != nil { + return nil, e + } + return &decodeScriptResult, nil +} + +// DecodeScriptAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See DecodeScript for the blocking version and more details. +func (c *Client) DecodeScriptAsync(serializedScript []byte) FutureDecodeScriptResult { + scriptHex := hex.EncodeToString(serializedScript) + cmd := btcjson.NewDecodeScriptCmd(scriptHex) + return c.sendCmd(cmd) +} + +// DecodeScript returns information about a script given its serialized bytes. +func (c *Client) DecodeScript(serializedScript []byte) (*btcjson.DecodeScriptResult, error) { + return c.DecodeScriptAsync(serializedScript).Receive() +} diff --git a/pkg/rpcclient/wallet.go b/pkg/rpcclient/wallet.go new file mode 100644 index 0000000..d2d5845 --- /dev/null +++ b/pkg/rpcclient/wallet.go @@ -0,0 +1,2120 @@ +package rpcclient + +import ( + js "encoding/json" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "strconv" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// ***************************** +// Transaction Listing Functions +// ***************************** + +// FutureGetTransactionResult is a future promise to deliver the result of a GetTransactionAsync RPC invocation (or an +// applicable error). +type FutureGetTransactionResult chan *response + +// Receive waits for the response promised by the future and returns detailed information about a wallet transaction. +func (r FutureGetTransactionResult) Receive() (*btcjson.GetTransactionResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a gettransaction result object + var getTx btcjson.GetTransactionResult + e = js.Unmarshal(res, &getTx) + if e != nil { + return nil, e + } + return &getTx, nil +} + +// GetTransactionAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See GetTransaction for the blocking version and more details. +func (c *Client) GetTransactionAsync(txHash *chainhash.Hash) FutureGetTransactionResult { + hash := "" + if txHash != nil { + hash = txHash.String() + } + cmd := btcjson.NewGetTransactionCmd(hash, nil) + return c.sendCmd(cmd) +} + +// GetTransaction returns detailed information about a wallet transaction. +// +// See GetRawTransaction to return the raw transaction instead. +func (c *Client) GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) { + return c.GetTransactionAsync(txHash).Receive() +} + +// FutureListTransactionsResult is a future promise to deliver the result of a ListTransactionsAsync, +// ListTransactionsCountAsync, or ListTransactionsCountFromAsync RPC invocation (or an applicable error). +type FutureListTransactionsResult chan *response + +// Receive waits for the response promised by the future and returns a list of the most recent transactions. +func (r FutureListTransactionsResult) Receive() ([]btcjson.ListTransactionsResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as an array of listtransaction result objects. + var transactions []btcjson.ListTransactionsResult + e = js.Unmarshal(res, &transactions) + if e != nil { + return nil, e + } + return transactions, nil +} + +// ListTransactionsAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See ListTransactions for the blocking version and more details. +func (c *Client) ListTransactionsAsync(account string) FutureListTransactionsResult { + cmd := btcjson.NewListTransactionsCmd(&account, nil, nil, nil) + D.S(cmd) + return c.sendCmd(cmd) +} + +// ListTransactions returns a list of the most recent transactions. +// +// See the ListTransactionsCount and ListTransactionsCountFrom to control the number of transactions returned and +// starting point, respectively. +func (c *Client) ListTransactions(account string) ([]btcjson.ListTransactionsResult, error) { + return c.ListTransactionsAsync(account).Receive() +} + +// ListTransactionsCountAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ListTransactionsCount for the blocking version and more details. +func (c *Client) ListTransactionsCountAsync(account string, count int) FutureListTransactionsResult { + cmd := btcjson.NewListTransactionsCmd(&account, &count, nil, nil) + return c.sendCmd(cmd) +} + +// ListTransactionsCount returns a list of the most recent transactions up to the passed count. +// +// See the ListTransactions and ListTransactionsCountFrom functions for different options. +func (c *Client) ListTransactionsCount(account string, count int) ([]btcjson.ListTransactionsResult, error) { + return c.ListTransactionsCountAsync(account, count).Receive() +} + +// ListTransactionsCountFromAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See ListTransactionsCountFrom for the blocking version and more details. +func (c *Client) ListTransactionsCountFromAsync(account string, count, from int) FutureListTransactionsResult { + cmd := btcjson.NewListTransactionsCmd(&account, &count, &from, nil) + return c.sendCmd(cmd) +} + +// ListTransactionsCountFrom returns a list of the most recent transactions up to the passed count while skipping the +// first 'from' transactions. +// +// See the ListTransactions and ListTransactionsCount functions to use defaults. +func (c *Client) ListTransactionsCountFrom(account string, count, from int) ([]btcjson.ListTransactionsResult, error) { + return c.ListTransactionsCountFromAsync(account, count, from).Receive() +} + +// FutureListUnspentResult is a future promise to deliver the result of a ListUnspentAsync, ListUnspentMinAsync, +// ListUnspentMinMaxAsync, or ListUnspentMinMaxAddressesAsync RPC invocation (or an applicable error). +type FutureListUnspentResult chan *response + +// Receive waits for the response promised by the future and returns all unspent wallet transaction outputs returned by +// the RPC call. +// +// If the future wac returned by a call to ListUnspentMinAsync, ListUnspentMinMaxAsync, or +// ListUnspentMinMaxAddressesAsync, the range may be limited by the parameters of the RPC invocation. +func (r FutureListUnspentResult) Receive() ([]btcjson.ListUnspentResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as an array of listunspent results. + var unspent []btcjson.ListUnspentResult + e = js.Unmarshal(res, &unspent) + if e != nil { + return nil, e + } + return unspent, nil +} + +// ListUnspentAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See ListUnspent for the blocking version and more details. +func (c *Client) ListUnspentAsync() FutureListUnspentResult { + cmd := btcjson.NewListUnspentCmd(nil, nil, nil) + return c.sendCmd(cmd) +} + +// ListUnspentMinAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See ListUnspentMin for the blocking version and more details. +func (c *Client) ListUnspentMinAsync(minConf int) FutureListUnspentResult { + cmd := btcjson.NewListUnspentCmd(&minConf, nil, nil) + return c.sendCmd(cmd) +} + +// ListUnspentMinMaxAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ListUnspentMinMax for the blocking version and more details. +func (c *Client) ListUnspentMinMaxAsync(minConf, maxConf int) FutureListUnspentResult { + cmd := btcjson.NewListUnspentCmd(&minConf, &maxConf, nil) + return c.sendCmd(cmd) +} + +// ListUnspentMinMaxAddressesAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See ListUnspentMinMaxAddresses for the blocking version and more details. +func (c *Client) ListUnspentMinMaxAddressesAsync(minConf, maxConf int, addrs []btcaddr.Address, +) FutureListUnspentResult { + addrStrs := make([]string, 0, len(addrs)) + for _, a := range addrs { + addrStrs = append(addrStrs, a.EncodeAddress()) + } + cmd := btcjson.NewListUnspentCmd(&minConf, &maxConf, &addrStrs) + return c.sendCmd(cmd) +} + +// ListUnspent returns all unspent transaction outputs known to a wallet, using the default number of minimum and +// maximum number of confirmations as a filter (1 and 999999, respectively). +func (c *Client) ListUnspent() ([]btcjson.ListUnspentResult, error) { + return c.ListUnspentAsync().Receive() +} + +// ListUnspentMin returns all unspent transaction outputs known to a wallet, using the specified number of minimum +// conformations and default number of maximum confiramtions (999999) as a filter. +func (c *Client) ListUnspentMin(minConf int) ([]btcjson.ListUnspentResult, error) { + return c.ListUnspentMinAsync(minConf).Receive() +} + +// ListUnspentMinMax returns all unspent transaction outputs known to a wallet, using the specified number of minimum +// and maximum number of confirmations as a filter. +func (c *Client) ListUnspentMinMax(minConf, maxConf int) ([]btcjson.ListUnspentResult, error) { + return c.ListUnspentMinMaxAsync(minConf, maxConf).Receive() +} + +// ListUnspentMinMaxAddresses returns all unspent transaction outputs that pay to any of specified addresses in a wallet +// using the specified number of minimum and maximum number of confirmations as a filter. +func (c *Client) ListUnspentMinMaxAddresses(minConf, maxConf int, addrs []btcaddr.Address) ( + []btcjson.ListUnspentResult, + error, +) { + return c.ListUnspentMinMaxAddressesAsync(minConf, maxConf, addrs).Receive() +} + +// FutureListSinceBlockResult is a future promise to deliver the result of a ListSinceBlockAsync or +// ListSinceBlockMinConfAsync RPC invocation (or an applicable error). +type FutureListSinceBlockResult chan *response + +// Receive waits for the response promised by the future and returns all transactions added in blocks since the +// specified block hash, or all transactions if it is nil. +func (r FutureListSinceBlockResult) Receive() (*btcjson.ListSinceBlockResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a listsinceblock result object. + var listResult btcjson.ListSinceBlockResult + e = js.Unmarshal(res, &listResult) + if e != nil { + return nil, e + } + return &listResult, nil +} + +// ListSinceBlockAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See ListSinceBlock for the blocking version and more details. +func (c *Client) ListSinceBlockAsync(blockHash *chainhash.Hash) FutureListSinceBlockResult { + var hash *string + if blockHash != nil { + hash = btcjson.String(blockHash.String()) + } + cmd := btcjson.NewListSinceBlockCmd(hash, nil, nil) + return c.sendCmd(cmd) +} + +// ListSinceBlock returns all transactions added in blocks since the specified block hash, or all transactions if it is +// nil, using the default number of minimum confirmations as a filter. +// +// See ListSinceBlockMinConf to override the minimum number of confirmations. +func (c *Client) ListSinceBlock(blockHash *chainhash.Hash) (*btcjson.ListSinceBlockResult, error) { + return c.ListSinceBlockAsync(blockHash).Receive() +} + +// ListSinceBlockMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ListSinceBlockMinConf for the blocking version and more details. +func (c *Client) ListSinceBlockMinConfAsync(blockHash *chainhash.Hash, minConfirms int) FutureListSinceBlockResult { + var hash *string + if blockHash != nil { + hash = btcjson.String(blockHash.String()) + } + cmd := btcjson.NewListSinceBlockCmd(hash, &minConfirms, nil) + return c.sendCmd(cmd) +} + +// ListSinceBlockMinConf returns all transactions added in blocks since the specified block hash, or all transactions if +// it is nil, using the specified number of minimum confirmations as a filter. +// +// See ListSinceBlock to use the default minimum number of confirmations. +func (c *Client) ListSinceBlockMinConf(blockHash *chainhash.Hash, minConfirms int) ( + *btcjson.ListSinceBlockResult, + error, +) { + return c.ListSinceBlockMinConfAsync(blockHash, minConfirms).Receive() +} + +// ************************** +// Transaction Send Functions +// ************************** + +// FutureLockUnspentResult is a future promise to deliver the error result of a LockUnspentAsync RPC invocation. +type FutureLockUnspentResult chan *response + +// Receive waits for the response promised by the future and returns the result of locking or unlocking the unspent +// output(s). +func (r FutureLockUnspentResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// LockUnspentAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See LockUnspent for the blocking version and more details. +func (c *Client) LockUnspentAsync(unlock bool, ops []*wire.OutPoint) FutureLockUnspentResult { + outputs := make([]btcjson.TransactionInput, len(ops)) + for i, op := range ops { + outputs[i] = btcjson.TransactionInput{ + Txid: op.Hash.String(), + Vout: op.Index, + } + } + cmd := btcjson.NewLockUnspentCmd(unlock, outputs) + return c.sendCmd(cmd) +} + +// LockUnspent marks outputs as locked or unlocked, depending on the value of the unlock bool. When locked, the unspent +// output will not be selected as input for newly created, non-raw transactions, and will not be returned in future +// ListUnspent results, until the output is marked unlocked again. +// +// If unlock is false, each outpoint in ops will be marked locked. If unlocked is true and specific outputs are +// specified in ops (len != 0), exactly those outputs will be marked unlocked. If unlocked is true and no outpoints are +// specified, all previous locked outputs are marked unlocked. +// +// The locked or unlocked state of outputs are not written to disk and after restarting a wallet process, this data will +// be reset (every output unlocked). +// +// NOTE: While this method would be a bit more readable if the unlock bool was reversed (that is, LockUnspent(true, ...) +// locked the outputs), it has been left as unlock to keep compatibility with the reference client API and to avoid +// confusion for those who are already familiar with the lockunspent RPC. +func (c *Client) LockUnspent(unlock bool, ops []*wire.OutPoint) (e error) { + return c.LockUnspentAsync(unlock, ops).Receive() +} + +// FutureListLockUnspentResult is a future promise to deliver the result of a ListLockUnspentAsync RPC invocation (or an +// applicable error). +type FutureListLockUnspentResult chan *response + +// Receive waits for the response promised by the future and returns the result of all currently locked unspent outputs. +func (r FutureListLockUnspentResult) Receive() ([]*wire.OutPoint, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal as an array of transaction inputs. + var inputs []btcjson.TransactionInput + e = js.Unmarshal(res, &inputs) + if e != nil { + return nil, e + } + // Create a slice of outpoints from the transaction input structs. + ops := make([]*wire.OutPoint, len(inputs)) + for i, input := range inputs { + sha, e := chainhash.NewHashFromStr(input.Txid) + if e != nil { + return nil, e + } + ops[i] = wire.NewOutPoint(sha, input.Vout) + } + return ops, nil +} + +// ListLockUnspentAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See ListLockUnspent for the blocking version and more details. +func (c *Client) ListLockUnspentAsync() FutureListLockUnspentResult { + cmd := btcjson.NewListLockUnspentCmd() + return c.sendCmd(cmd) +} + +// ListLockUnspent returns a slice of outpoints for all unspent outputs marked as locked by a wallet. Unspent outputs +// may be marked locked using LockOutput. +func (c *Client) ListLockUnspent() ([]*wire.OutPoint, error) { + return c.ListLockUnspentAsync().Receive() +} + +// FutureSetTxFeeResult is a future promise to deliver the result of a SetTxFeeAsync RPC invocation (or an applicable +// error). +type FutureSetTxFeeResult chan *response + +// Receive waits for the response promised by the future and returns the result of setting an optional transaction fee +// per KB that helps ensure transactions are processed quickly. Most transaction are 1KB. +func (r FutureSetTxFeeResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// SetTxFeeAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See SetTxFee for the blocking version and more details. +func (c *Client) SetTxFeeAsync(fee amt.Amount) FutureSetTxFeeResult { + cmd := btcjson.NewSetTxFeeCmd(fee.ToDUO()) + return c.sendCmd(cmd) +} + +// SetTxFee sets an optional transaction fee per KB that helps ensure transactions are processed quickly. Most +// transaction are 1KB. +func (c *Client) SetTxFee(fee amt.Amount) (e error) { + return c.SetTxFeeAsync(fee).Receive() +} + +// FutureSendToAddressResult is a future promise to deliver the result of a SendToAddressAsync RPC invocation (or an +// applicable error). +type FutureSendToAddressResult chan *response + +// Receive waits for the response promised by the future and returns the hash of the transaction sending the passed +// amount to the given address. +func (r FutureSendToAddressResult) Receive() (*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var txHash string + e = js.Unmarshal(res, &txHash) + if e != nil { + return nil, e + } + return chainhash.NewHashFromStr(txHash) +} + +// SendToAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See SendToAddress for the blocking version and more details. +func (c *Client) SendToAddressAsync(address btcaddr.Address, amount amt.Amount) FutureSendToAddressResult { + addr := address.EncodeAddress() + cmd := btcjson.NewSendToAddressCmd(addr, amount.ToDUO(), nil, nil) + return c.sendCmd(cmd) +} + +// SendToAddress sends the passed amount to the given address. +// +// See SendToAddressComment to associate comments with the transaction in the wallet. The comments are not part of the +// transaction and are only internal to the wallet. +// +// NOTE: This function requires to the wallet to be unlocked. +// +// See the WalletPassphrase function for more details. +func (c *Client) SendToAddress(address btcaddr.Address, amount amt.Amount) (*chainhash.Hash, error) { + return c.SendToAddressAsync(address, amount).Receive() +} + +// SendToAddressCommentAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See SendToAddressComment for the blocking version and more details. +func (c *Client) SendToAddressCommentAsync( + address btcaddr.Address, + amount amt.Amount, comment, + commentTo string, +) FutureSendToAddressResult { + addr := address.EncodeAddress() + cmd := btcjson.NewSendToAddressCmd( + addr, amount.ToDUO(), &comment, + &commentTo, + ) + return c.sendCmd(cmd) +} + +// SendToAddressComment sends the passed amount to the given address and stores the provided comment and comment to in +// the wallet. The comment parameter is intended to be used for the purpose of the transaction while the commentTo +// parameter is indended to be used for who the transaction is being sent to. +// +// The comments are not part of the transaction and are only internal to the wallet. +// +// See SendToAddress to avoid using comments. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendToAddressComment( + address btcaddr.Address, + amount amt.Amount, + comment, commentTo string, +) (*chainhash.Hash, error) { + return c.SendToAddressCommentAsync( + address, amount, comment, + commentTo, + ).Receive() +} + +// FutureSendFromResult is a future promise to deliver the result of a SendFromAsync, SendFromMinConfAsync, or +// SendFromCommentAsync RPC invocation (or an applicable error). +type FutureSendFromResult chan *response + +// Receive waits for the response promised by the future and returns the hash of the transaction sending amount to the +// given address using the provided account as a source of funds. +func (r FutureSendFromResult) Receive() (*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var txHash string + e = js.Unmarshal(res, &txHash) + if e != nil { + return nil, e + } + return chainhash.NewHashFromStr(txHash) +} + +// SendFromAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See SendFrom for the blocking version and more details. +func (c *Client) SendFromAsync(fromAccount string, toAddress btcaddr.Address, amount amt.Amount) FutureSendFromResult { + addr := toAddress.EncodeAddress() + cmd := btcjson.NewSendFromCmd( + fromAccount, addr, amount.ToDUO(), nil, + nil, nil, + ) + return c.sendCmd(cmd) +} + +// SendFrom sends the passed amount to the given address using the provided account as a source of funds. Only funds +// with the default number of minimum confirmations will be used. +// +// See SendFromMinConf and SendFromComment for different options. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendFrom(fromAccount string, toAddress btcaddr.Address, amount amt.Amount) (*chainhash.Hash, error) { + return c.SendFromAsync(fromAccount, toAddress, amount).Receive() +} + +// SendFromMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See SendFromMinConf for the blocking version and more details. +func (c *Client) SendFromMinConfAsync( + fromAccount string, + toAddress btcaddr.Address, + amount amt.Amount, + minConfirms int, +) FutureSendFromResult { + addr := toAddress.EncodeAddress() + cmd := btcjson.NewSendFromCmd( + fromAccount, addr, amount.ToDUO(), + &minConfirms, nil, nil, + ) + return c.sendCmd(cmd) +} + +// SendFromMinConf sends the passed amount to the given address using the provided account as a source of funds. Only +// funds with the passed number of minimum confirmations will be used. +// +// See SendFrom to use the default number of minimum confirmations and SendFromComment for additional options. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendFromMinConf( + fromAccount string, + toAddress btcaddr.Address, + amount amt.Amount, + minConfirms int, +) (*chainhash.Hash, error) { + return c.SendFromMinConfAsync( + fromAccount, toAddress, amount, + minConfirms, + ).Receive() +} + +// SendFromCommentAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See SendFromComment for the blocking version and more details. +func (c *Client) SendFromCommentAsync( + fromAccount string, + toAddress btcaddr.Address, amount amt.Amount, minConfirms int, + comment, commentTo string, +) FutureSendFromResult { + addr := toAddress.EncodeAddress() + cmd := btcjson.NewSendFromCmd( + fromAccount, addr, amount.ToDUO(), + &minConfirms, &comment, &commentTo, + ) + return c.sendCmd(cmd) +} + +// SendFromComment sends the passed amount to the given address using the provided account as a source of funds and +// stores the provided comment and comment to in the wallet. The comment parameter is intended to be used for the +// purpose of the transaction while the commentTo parameter is intended to be used for who the transaction is being sent +// to. Only funds with the passed number of minimum confirmations will be used. +// +// See SendFrom and SendFromMinConf to use defaults. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendFromComment( + fromAccount string, toAddress btcaddr.Address, + amount amt.Amount, minConfirms int, + comment, commentTo string, +) (*chainhash.Hash, error) { + return c.SendFromCommentAsync( + fromAccount, toAddress, amount, + minConfirms, comment, commentTo, + ).Receive() +} + +// FutureSendManyResult is a future promise to deliver the result of a SendManyAsync, SendManyMinConfAsync, or +// SendManyCommentAsync RPC invocation (or an applicable error). +type FutureSendManyResult chan *response + +// Receive waits for the response promised by the future and returns the hash of the transaction sending multiple +// amounts to multiple addresses using the provided account as a source of funds. +func (r FutureSendManyResult) Receive() (*chainhash.Hash, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmashal result as a string. + var txHash string + e = js.Unmarshal(res, &txHash) + if e != nil { + return nil, e + } + return chainhash.NewHashFromStr(txHash) +} + +// SendManyAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See SendMany for the blocking version and more details. +func (c *Client) SendManyAsync(fromAccount string, amounts map[btcaddr.Address]amt.Amount) FutureSendManyResult { + convertedAmounts := make(map[string]float64, len(amounts)) + for addr, amount := range amounts { + convertedAmounts[addr.EncodeAddress()] = amount.ToDUO() + } + cmd := btcjson.NewSendManyCmd(fromAccount, convertedAmounts, nil, nil) + return c.sendCmd(cmd) +} + +// SendMany sends multiple amounts to multiple addresses using the provided account as a source of funds in a single +// transaction. Only funds with the default number of minimum confirmations will be used. +// +// See SendManyMinConf and SendManyComment for different options. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendMany(fromAccount string, amounts map[btcaddr.Address]amt.Amount) (*chainhash.Hash, error) { + return c.SendManyAsync(fromAccount, amounts).Receive() +} + +// SendManyMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See SendManyMinConf for the blocking version and more details. +func (c *Client) SendManyMinConfAsync( + fromAccount string, + amounts map[btcaddr.Address]amt.Amount, + minConfirms int, +) FutureSendManyResult { + convertedAmounts := make(map[string]float64, len(amounts)) + for addr, amount := range amounts { + convertedAmounts[addr.EncodeAddress()] = amount.ToDUO() + } + cmd := btcjson.NewSendManyCmd( + fromAccount, convertedAmounts, + &minConfirms, nil, + ) + return c.sendCmd(cmd) +} + +// SendManyMinConf sends multiple amounts to multiple addresses using the provided account as a source of funds in a +// single transaction. Only funds with the passed number of minimum confirmations will be used. +// +// See SendMany to use the default number of minimum confirmations and SendManyComment for additional options. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendManyMinConf( + fromAccount string, + amounts map[btcaddr.Address]amt.Amount, + minConfirms int, +) (*chainhash.Hash, error) { + return c.SendManyMinConfAsync(fromAccount, amounts, minConfirms).Receive() +} + +// SendManyCommentAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See SendManyComment for the blocking version and more details. +func (c *Client) SendManyCommentAsync( + fromAccount string, + amounts map[btcaddr.Address]amt.Amount, minConfirms int, + comment string, +) FutureSendManyResult { + convertedAmounts := make(map[string]float64, len(amounts)) + for addr, amount := range amounts { + convertedAmounts[addr.EncodeAddress()] = amount.ToDUO() + } + cmd := btcjson.NewSendManyCmd( + fromAccount, convertedAmounts, + &minConfirms, &comment, + ) + return c.sendCmd(cmd) +} + +// SendManyComment sends multiple amounts to multiple addresses using the provided account as a source of funds in a +// single transaction and stores the provided comment in the wallet. The comment parameter is intended to be used for +// the purpose of the transaction Only funds with the passed number of minimum confirmations will be used. +// +// See SendMany and SendManyMinConf to use defaults. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SendManyComment( + fromAccount string, + amounts map[btcaddr.Address]amt.Amount, minConfirms int, + comment string, +) (*chainhash.Hash, error) { + return c.SendManyCommentAsync( + fromAccount, amounts, minConfirms, + comment, + ).Receive() +} + +// ************************* +// Address/Account Functions +// ************************* + +// FutureAddMultisigAddressResult is a future promise to deliver the result of a AddMultisigAddressAsync RPC invocation +// (or an applicable error). +type FutureAddMultisigAddressResult chan *response + +// Receive waits for the response promised by the future and returns the multisignature address that requires the +// specified number of signatures for the provided addresses. +func (r FutureAddMultisigAddressResult) Receive() (btcaddr.Address, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var addr string + e = js.Unmarshal(res, &addr) + if e != nil { + return nil, e + } + return btcaddr.Decode(addr, &chaincfg.MainNetParams) +} + +// AddMultisigAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See AddMultisigAddress for the blocking version and more details. +func (c *Client) AddMultisigAddressAsync( + requiredSigs int, + addresses []btcaddr.Address, + account string, +) FutureAddMultisigAddressResult { + addrs := make([]string, 0, len(addresses)) + for _, addr := range addresses { + addrs = append(addrs, addr.String()) + } + cmd := btcjson.NewAddMultisigAddressCmd(requiredSigs, addrs, &account) + return c.sendCmd(cmd) +} + +// AddMultisigAddress adds a multisignature address that requires the specified number of signatures for the provided +// addresses to the wallet. +func (c *Client) AddMultisigAddress(requiredSigs int, addresses []btcaddr.Address, account string) (btcaddr.Address, + error, +) { + return c.AddMultisigAddressAsync( + requiredSigs, addresses, + account, + ).Receive() +} + +// FutureCreateMultisigResult is a future promise to deliver the result of a CreateMultisigAsync RPC invocation (or an +// applicable error). +type FutureCreateMultisigResult chan *response + +// Receive waits for the response promised by the future and returns the multisignature address and script needed to +// redeem it. +func (r FutureCreateMultisigResult) Receive() (*btcjson.CreateMultiSigResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a createmultisig result object. + var multisigRes btcjson.CreateMultiSigResult + e = js.Unmarshal(res, &multisigRes) + if e != nil { + return nil, e + } + return &multisigRes, nil +} + +// CreateMultisigAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See CreateMultisig for the blocking version and more details. +func (c *Client) CreateMultisigAsync(requiredSigs int, addresses []btcaddr.Address) FutureCreateMultisigResult { + addrs := make([]string, 0, len(addresses)) + for _, addr := range addresses { + addrs = append(addrs, addr.String()) + } + cmd := btcjson.NewCreateMultisigCmd(requiredSigs, addrs) + return c.sendCmd(cmd) +} + +// CreateMultisig creates a multisignature address that requires the specified number of signatures for the provided +// addresses and returns the multisignature address and script needed to redeem it. +func (c *Client) CreateMultisig(requiredSigs int, addresses []btcaddr.Address) (*btcjson.CreateMultiSigResult, error) { + return c.CreateMultisigAsync(requiredSigs, addresses).Receive() +} + +// FutureCreateNewAccountResult is a future promise to deliver the result of a CreateNewAccountAsync RPC invocation (or +// an applicable error). +type FutureCreateNewAccountResult chan *response + +// Receive waits for the response promised by the future and returns the result of creating new account. +func (r FutureCreateNewAccountResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// CreateNewAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See CreateNewAccount for the blocking version and more details. +func (c *Client) CreateNewAccountAsync(account string) FutureCreateNewAccountResult { + cmd := btcjson.NewCreateNewAccountCmd(account) + return c.sendCmd(cmd) +} + +// CreateNewAccount creates a new wallet account. +func (c *Client) CreateNewAccount(account string) (e error) { + return c.CreateNewAccountAsync(account).Receive() +} + +// FutureGetNewAddressResult is a future promise to deliver the result of a GetNewAddressAsync RPC invocation (or an +// applicable error). +type FutureGetNewAddressResult chan *response + +// Receive waits for the response promised by the future and returns a new address. +func (r FutureGetNewAddressResult) Receive() (btcaddr.Address, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var addr string + e = js.Unmarshal(res, &addr) + if e != nil { + return nil, e + } + return btcaddr.Decode(addr, &chaincfg.MainNetParams) +} + +// GetNewAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetNewAddress for the blocking version and more details. +func (c *Client) GetNewAddressAsync(account string) FutureGetNewAddressResult { + T.Ln("### GetNewAddressAsync") + cmd := btcjson.NewGetNewAddressCmd(&account) + // D.S(cmd) + return c.sendCmd(cmd) +} + +// GetNewAddress returns a new address. +func (c *Client) GetNewAddress(account string) (btcaddr.Address, error) { + T.Ln("### GetNewAddress") + return c.GetNewAddressAsync(account).Receive() +} + +// FutureGetRawChangeAddressResult is a future promise to deliver the result of a GetRawChangeAddressAsync RPC +// invocation (or an applicable error). +type FutureGetRawChangeAddressResult chan *response + +// Receive waits for the response promised by the future and returns a new address for receiving change that will be +// associated with the provided account. Note that this is only for raw transactions and NOT for normal use. +func (r FutureGetRawChangeAddressResult) Receive() (btcaddr.Address, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var addr string + e = js.Unmarshal(res, &addr) + if e != nil { + return nil, e + } + return btcaddr.Decode(addr, &chaincfg.MainNetParams) +} + +// GetRawChangeAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetRawChangeAddress for the blocking version and more details. +func (c *Client) GetRawChangeAddressAsync(account string) FutureGetRawChangeAddressResult { + cmd := btcjson.NewGetRawChangeAddressCmd(&account) + return c.sendCmd(cmd) +} + +// GetRawChangeAddress returns a new address for receiving change that will be associated with the provided account. +// +// Note that this is only for raw transactions and NOT for normal use. +func (c *Client) GetRawChangeAddress(account string) (btcaddr.Address, error) { + return c.GetRawChangeAddressAsync(account).Receive() +} + +// FutureAddWitnessAddressResult is a future promise to deliver the result of a +// AddWitnessAddressAsync RPC invocation (or an applicable error). +type FutureAddWitnessAddressResult chan *response + +// Receive waits for the response promised by the future and returns the new address. +func (r FutureAddWitnessAddressResult) Receive() (btcaddr.Address, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var addr string + e = js.Unmarshal(res, &addr) + if e != nil { + return nil, e + } + return btcaddr.Decode(addr, &chaincfg.MainNetParams) +} + +// AddWitnessAddressAsync returns an instance of a type that can be used to get +// the result of the RPC at some future time by invoking the Receive function on +// the returned instance. +// +// See AddWitnessAddress for the blocking version and more details. +func (c *Client) AddWitnessAddressAsync(address string) FutureAddWitnessAddressResult { + cmd := btcjson.NewAddWitnessAddressCmd(address) + return c.sendCmd(cmd) +} + +// AddWitnessAddress adds a witness address for a script and returns the new +// address (P2SH of the witness script). +func (c *Client) AddWitnessAddress(address string) (btcaddr.Address, error) { + return c.AddWitnessAddressAsync(address).Receive() +} + +// FutureGetAccountAddressResult is a future promise to deliver the result of a GetAccountAddressAsync RPC invocation +// (or an applicable error). +type FutureGetAccountAddressResult chan *response + +// Receive waits for the response promised by the future and returns the current Bitcoin address for receiving payments +// to the specified account. +func (r FutureGetAccountAddressResult) Receive() (btcaddr.Address, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var addr string + e = js.Unmarshal(res, &addr) + if e != nil { + return nil, e + } + return btcaddr.Decode(addr, &chaincfg.MainNetParams) +} + +// GetAccountAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetAccountAddress for the blocking version and more details. +func (c *Client) GetAccountAddressAsync(account string) FutureGetAccountAddressResult { + cmd := btcjson.NewGetAccountAddressCmd(account) + return c.sendCmd(cmd) +} + +// GetAccountAddress returns the current Bitcoin address for receiving payments to the specified account. +func (c *Client) GetAccountAddress(account string) (btcaddr.Address, error) { + return c.GetAccountAddressAsync(account).Receive() +} + +// FutureGetAccountResult is a future promise to deliver the result of a GetAccountAsync RPC invocation (or an +// applicable error). +type FutureGetAccountResult chan *response + +// Receive waits for the response promised by the future and returns the account associated with the passed address. +func (r FutureGetAccountResult) Receive() (string, error) { + res, e := receiveFuture(r) + if e != nil { + return "", e + } + // Unmarshal result as a string. + var account string + e = js.Unmarshal(res, &account) + if e != nil { + return "", e + } + return account, nil +} + +// GetAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetAccount for the blocking version and more details. +func (c *Client) GetAccountAsync(address btcaddr.Address) FutureGetAccountResult { + addr := address.EncodeAddress() + cmd := btcjson.NewGetAccountCmd(addr) + return c.sendCmd(cmd) +} + +// GetAccount returns the account associated with the passed address. +func (c *Client) GetAccount(address btcaddr.Address) (string, error) { + return c.GetAccountAsync(address).Receive() +} + +// FutureSetAccountResult is a future promise to deliver the result of a SetAccountAsync RPC invocation (or an +// applicable error). +type FutureSetAccountResult chan *response + +// Receive waits for the response promised by the future and returns the result of setting the account to be associated +// with the passed address. +func (r FutureSetAccountResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// SetAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See SetAccount for the blocking version and more details. +func (c *Client) SetAccountAsync(address btcaddr.Address, account string) FutureSetAccountResult { + addr := address.EncodeAddress() + cmd := btcjson.NewSetAccountCmd(addr, account) + return c.sendCmd(cmd) +} + +// SetAccount sets the account associated with the passed address. +func (c *Client) SetAccount(address btcaddr.Address, account string) (e error) { + return c.SetAccountAsync(address, account).Receive() +} + +// FutureGetAddressesByAccountResult is a future promise to deliver the result of a GetAddressesByAccountAsync RPC +// invocation (or an applicable error). +type FutureGetAddressesByAccountResult chan *response + +// Receive waits for the response promised by the future and returns the list of addresses associated with the passed +// account. +func (r FutureGetAddressesByAccountResult) Receive() ([]btcaddr.Address, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as an array of string. + var addrStrings []string + e = js.Unmarshal(res, &addrStrings) + if e != nil { + return nil, e + } + addrs := make([]btcaddr.Address, 0, len(addrStrings)) + for _, addrStr := range addrStrings { + addr, e := btcaddr.Decode( + addrStr, + &chaincfg.MainNetParams, + ) + if e != nil { + return nil, e + } + addrs = append(addrs, addr) + } + return addrs, nil +} + +// GetAddressesByAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetAddressesByAccount for the blocking version and more details. +func (c *Client) GetAddressesByAccountAsync(account string) FutureGetAddressesByAccountResult { + cmd := btcjson.NewGetAddressesByAccountCmd(account) + return c.sendCmd(cmd) +} + +// GetAddressesByAccount returns the list of addresses associated with the passed account. +func (c *Client) GetAddressesByAccount(account string) ([]btcaddr.Address, error) { + return c.GetAddressesByAccountAsync(account).Receive() +} + +// FutureMoveResult is a future promise to deliver the result of a MoveAsync, MoveMinConfAsync, or MoveCommentAsync RPC +// invocation (or an applicable error). +type FutureMoveResult chan *response + +// Receive waits for the response promised by the future and returns the result of the move operation. +func (r FutureMoveResult) Receive() (bool, error) { + res, e := receiveFuture(r) + if e != nil { + return false, e + } + // Unmarshal result as a boolean. + var moveResult bool + e = js.Unmarshal(res, &moveResult) + if e != nil { + return false, e + } + return moveResult, nil +} + +// MoveAsync returns an instance of a type that can be used to get the result of the RPC at some future time by invoking +// the Receive function on the returned instance. +// +// See Move for the blocking version and more details. +func (c *Client) MoveAsync(fromAccount, toAccount string, amount amt.Amount) FutureMoveResult { + cmd := btcjson.NewMoveCmd( + fromAccount, toAccount, amount.ToDUO(), nil, + nil, + ) + return c.sendCmd(cmd) +} + +// Move moves specified amount from one account in your wallet to another. Only funds with the default number of minimum +// confirmations will be used. +// +// See MoveMinConf and MoveComment for different options. +func (c *Client) Move(fromAccount, toAccount string, amount amt.Amount) (bool, error) { + return c.MoveAsync(fromAccount, toAccount, amount).Receive() +} + +// MoveMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See MoveMinConf for the blocking version and more details. +func (c *Client) MoveMinConfAsync( + fromAccount, toAccount string, + amount amt.Amount, minConfirms int, +) FutureMoveResult { + cmd := btcjson.NewMoveCmd( + fromAccount, toAccount, amount.ToDUO(), + &minConfirms, nil, + ) + return c.sendCmd(cmd) +} + +// MoveMinConf moves specified amount from one account in your wallet to another. Only funds with the passed number of +// minimum confirmations will be used. +// +// See Move to use the default number of minimum confirmations and MoveComment for additional options. +func (c *Client) MoveMinConf(fromAccount, toAccount string, amount amt.Amount, minConf int) (bool, error) { + return c.MoveMinConfAsync(fromAccount, toAccount, amount, minConf).Receive() +} + +// MoveCommentAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See MoveComment for the blocking version and more details. +func (c *Client) MoveCommentAsync( + fromAccount, toAccount string, + amount amt.Amount, minConfirms int, comment string, +) FutureMoveResult { + cmd := btcjson.NewMoveCmd( + fromAccount, toAccount, amount.ToDUO(), + &minConfirms, &comment, + ) + return c.sendCmd(cmd) +} + +// MoveComment moves specified amount from one account in your wallet to another and stores the provided comment in the +// wallet. The comment parameter is only available in the wallet. Only funds with the passed number of minimum +// confirmations will be used. +// +// See Move and MoveMinConf to use defaults. +func (c *Client) MoveComment( + fromAccount, toAccount string, amount amt.Amount, + minConf int, comment string, +) (bool, error) { + return c.MoveCommentAsync( + fromAccount, toAccount, amount, minConf, + comment, + ).Receive() +} + +// FutureRenameAccountResult is a future promise to deliver the result of a RenameAccountAsync RPC invocation (or an +// applicable error). +type FutureRenameAccountResult chan *response + +// Receive waits for the response promised by the future and returns the result of creating new account. +func (r FutureRenameAccountResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// RenameAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See RenameAccount for the blocking version and more details. +func (c *Client) RenameAccountAsync(oldAccount, newAccount string) FutureRenameAccountResult { + cmd := btcjson.NewRenameAccountCmd(oldAccount, newAccount) + return c.sendCmd(cmd) +} + +// RenameAccount creates a new wallet account. +func (c *Client) RenameAccount(oldAccount, newAccount string) (e error) { + return c.RenameAccountAsync(oldAccount, newAccount).Receive() +} + +// FutureValidateAddressResult is a future promise to deliver the result of a ValidateAddressAsync RPC invocation (or an +// applicable error). +type FutureValidateAddressResult chan *response + +// Receive waits for the response promised by the future and returns information about the given bitcoin address. +func (r FutureValidateAddressResult) Receive() (*btcjson.ValidateAddressWalletResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a validateaddress result object. + var addrResult btcjson.ValidateAddressWalletResult + e = js.Unmarshal(res, &addrResult) + if e != nil { + return nil, e + } + return &addrResult, nil +} + +// ValidateAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future time +// by invoking the Receive function on the returned instance. +// +// See ValidateAddress for the blocking version and more details. +func (c *Client) ValidateAddressAsync(address btcaddr.Address) FutureValidateAddressResult { + addr := address.EncodeAddress() + cmd := btcjson.NewValidateAddressCmd(addr) + return c.sendCmd(cmd) +} + +// ValidateAddress returns information about the given bitcoin address. +func (c *Client) ValidateAddress(address btcaddr.Address) (*btcjson.ValidateAddressWalletResult, error) { + return c.ValidateAddressAsync(address).Receive() +} + +// FutureKeyPoolRefillResult is a future promise to deliver the result of a KeyPoolRefillAsync RPC invocation (or an +// applicable error). +type FutureKeyPoolRefillResult chan *response + +// Receive waits for the response promised by the future and returns the result of refilling the key pool. +func (r FutureKeyPoolRefillResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// KeyPoolRefillAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See KeyPoolRefill for the blocking version and more details. +func (c *Client) KeyPoolRefillAsync() FutureKeyPoolRefillResult { + cmd := btcjson.NewKeyPoolRefillCmd(nil) + return c.sendCmd(cmd) +} + +// KeyPoolRefill fills the key pool as necessary to reach the default size. See KeyPoolRefillSize to override the size +// of the key pool. +func (c *Client) KeyPoolRefill() (e error) { + return c.KeyPoolRefillAsync().Receive() +} + +// KeyPoolRefillSizeAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See KeyPoolRefillSize for the blocking version and more details. +func (c *Client) KeyPoolRefillSizeAsync(newSize uint) FutureKeyPoolRefillResult { + cmd := btcjson.NewKeyPoolRefillCmd(&newSize) + return c.sendCmd(cmd) +} + +// KeyPoolRefillSize fills the key pool as necessary to reach the specified size. +func (c *Client) KeyPoolRefillSize(newSize uint) (e error) { + return c.KeyPoolRefillSizeAsync(newSize).Receive() +} + +// ************************ +// Amount/Balance Functions +// ************************ + +// FutureListAccountsResult is a future promise to deliver the result of a ListAccountsAsync or ListAccountsMinConfAsync +// RPC invocation (or an applicable error). +type FutureListAccountsResult chan *response + +// Receive waits for the response promised by the future and returns returns a map of account names and their associated +// balances. +func (r FutureListAccountsResult) Receive() (map[string]amt.Amount, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a json object. + var accounts map[string]float64 + e = js.Unmarshal(res, &accounts) + if e != nil { + return nil, e + } + accountsMap := make(map[string]amt.Amount) + for k, v := range accounts { + amount, e := amt.NewAmount(v) + if e != nil { + return nil, e + } + accountsMap[k] = amount + } + return accountsMap, nil +} + +// ListAccountsAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See ListAccounts for the blocking version and more details. +func (c *Client) ListAccountsAsync() FutureListAccountsResult { + cmd := btcjson.NewListAccountsCmd(nil) + return c.sendCmd(cmd) +} + +// ListAccounts returns a map of account names and their associated balances using the default number of minimum +// confirmations. +// +// See ListAccountsMinConf to override the minimum number of confirmations. +func (c *Client) ListAccounts() (map[string]amt.Amount, error) { + return c.ListAccountsAsync().Receive() +} + +// ListAccountsMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ListAccountsMinConf for the blocking version and more details. +func (c *Client) ListAccountsMinConfAsync(minConfirms int) FutureListAccountsResult { + cmd := btcjson.NewListAccountsCmd(&minConfirms) + return c.sendCmd(cmd) +} + +// ListAccountsMinConf returns a map of account names and their associated balances using the specified number of +// minimum confirmations. +// +// See ListAccounts to use the default minimum number of confirmations. +func (c *Client) ListAccountsMinConf(minConfirms int) (map[string]amt.Amount, error) { + return c.ListAccountsMinConfAsync(minConfirms).Receive() +} + +// FutureGetBalanceResult is a future promise to deliver the result of a GetBalanceAsync or GetBalanceMinConfAsync RPC +// invocation (or an applicable error). +type FutureGetBalanceResult chan *response + +// Receive waits for the response promised by the future and returns the available balance from the server for the +// specified account. +func (r FutureGetBalanceResult) Receive() (amt.Amount, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as a floating point number. + var balance float64 + e = js.Unmarshal(res, &balance) + if e != nil { + return 0, e + } + amount, e := amt.NewAmount(balance) + if e != nil { + return 0, e + } + return amount, nil +} + +// FutureGetBalanceParseResult is same as FutureGetBalanceResult except that the result is expected to be a string which +// is then parsed into a float64 value +// +// This is required for compatibility with servers like blockchain.info +type FutureGetBalanceParseResult chan *response + +// Receive waits for the response promised by the future and returns the available balance from the server for the +// specified account. +func (r FutureGetBalanceParseResult) Receive() (amt.Amount, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as a string + var balanceString string + e = js.Unmarshal(res, &balanceString) + if e != nil { + return 0, e + } + balance, e := strconv.ParseFloat(balanceString, 64) + if e != nil { + return 0, e + } + amount, e := amt.NewAmount(balance) + if e != nil { + return 0, e + } + return amount, nil +} + +// GetBalanceAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetBalance for the blocking version and more details. +func (c *Client) GetBalanceAsync(account string) FutureGetBalanceResult { + cmd := btcjson.NewGetBalanceCmd(&account, nil) + return c.sendCmd(cmd) +} + +// GetBalance returns the available balance from the server for the specified account using the default number of +// minimum confirmations. The account may be "*" for all accounts. +// +// See GetBalanceMinConf to override the minimum number of confirmations. +func (c *Client) GetBalance(account string) (amt.Amount, error) { + return c.GetBalanceAsync(account).Receive() +} + +// GetBalanceMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetBalanceMinConf for the blocking version and more details. +func (c *Client) GetBalanceMinConfAsync(account string, minConfirms int) FutureGetBalanceResult { + cmd := btcjson.NewGetBalanceCmd(&account, &minConfirms) + return c.sendCmd(cmd) +} + +// GetBalanceMinConf returns the available balance from the server for the specified account using the specified number +// of minimum confirmations. The account may be "*" for all accounts. +// +// See GetBalance to use the default minimum number of confirmations. +func (c *Client) GetBalanceMinConf(account string, minConfirms int) (amt.Amount, error) { + if c.config.EnableBCInfoHacks { + response := c.GetBalanceMinConfAsync(account, minConfirms) + return FutureGetBalanceParseResult(response).Receive() + } + return c.GetBalanceMinConfAsync(account, minConfirms).Receive() +} + +// FutureGetReceivedByAccountResult is a future promise to deliver the result of a GetReceivedByAccountAsync or +// GetReceivedByAccountMinConfAsync RPC invocation (or an applicable error). +type FutureGetReceivedByAccountResult chan *response + +// Receive waits for the response promised by the future and returns the total amount received with the specified +// account. +func (r FutureGetReceivedByAccountResult) Receive() (amt.Amount, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as a floating point number. + var balance float64 + e = js.Unmarshal(res, &balance) + if e != nil { + return 0, e + } + amount, e := amt.NewAmount(balance) + if e != nil { + return 0, e + } + return amount, nil +} + +// GetReceivedByAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetReceivedByAccount for the blocking version and more details. +func (c *Client) GetReceivedByAccountAsync(account string) FutureGetReceivedByAccountResult { + cmd := btcjson.NewGetReceivedByAccountCmd(account, nil) + return c.sendCmd(cmd) +} + +// GetReceivedByAccount returns the total amount received with the specified account with at least the default number of +// minimum confirmations. +// +// See GetReceivedByAccountMinConf to override the minimum number of confirmations. +func (c *Client) GetReceivedByAccount(account string) (amt.Amount, error) { + return c.GetReceivedByAccountAsync(account).Receive() +} + +// GetReceivedByAccountMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See GetReceivedByAccountMinConf for the blocking version and more details. +func (c *Client) GetReceivedByAccountMinConfAsync(account string, minConfirms int) FutureGetReceivedByAccountResult { + cmd := btcjson.NewGetReceivedByAccountCmd(account, &minConfirms) + return c.sendCmd(cmd) +} + +// GetReceivedByAccountMinConf returns the total amount received with the specified account with at least the specified +// number of minimum confirmations. +// +// See GetReceivedByAccount to use the default minimum number of confirmations. +func (c *Client) GetReceivedByAccountMinConf(account string, minConfirms int) (amt.Amount, error) { + return c.GetReceivedByAccountMinConfAsync(account, minConfirms).Receive() +} + +// FutureGetUnconfirmedBalanceResult is a future promise to deliver the result of a GetUnconfirmedBalanceAsync RPC +// invocation (or an applicable error). +type FutureGetUnconfirmedBalanceResult chan *response + +// Receive waits for the response promised by the future and returns returns the unconfirmed balance from the server for +// the specified account. +func (r FutureGetUnconfirmedBalanceResult) Receive() (amt.Amount, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as a floating point number. + var balance float64 + e = js.Unmarshal(res, &balance) + if e != nil { + return 0, e + } + amount, e := amt.NewAmount(balance) + if e != nil { + return 0, e + } + return amount, nil +} + +// GetUnconfirmedBalanceAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetUnconfirmedBalance for the blocking version and more details. +func (c *Client) GetUnconfirmedBalanceAsync(account string) FutureGetUnconfirmedBalanceResult { + cmd := btcjson.NewGetUnconfirmedBalanceCmd(&account) + return c.sendCmd(cmd) +} + +// GetUnconfirmedBalance returns the unconfirmed balance from the server for the specified account. +func (c *Client) GetUnconfirmedBalance(account string) (amt.Amount, error) { + return c.GetUnconfirmedBalanceAsync(account).Receive() +} + +// FutureGetReceivedByAddressResult is a future promise to deliver the result of a GetReceivedByAddressAsync or +// GetReceivedByAddressMinConfAsync RPC invocation (or an applicable error). +type FutureGetReceivedByAddressResult chan *response + +// Receive waits for the response promised by the future and returns the total amount received by the specified address. +func (r FutureGetReceivedByAddressResult) Receive() (amt.Amount, error) { + res, e := receiveFuture(r) + if e != nil { + return 0, e + } + // Unmarshal result as a floating point number. + var balance float64 + e = js.Unmarshal(res, &balance) + if e != nil { + return 0, e + } + amount, e := amt.NewAmount(balance) + if e != nil { + return 0, e + } + return amount, nil +} + +// GetReceivedByAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See GetReceivedByAddress for the blocking version and more details. +func (c *Client) GetReceivedByAddressAsync(address btcaddr.Address) FutureGetReceivedByAddressResult { + addr := address.EncodeAddress() + cmd := btcjson.NewGetReceivedByAddressCmd(addr, nil) + return c.sendCmd(cmd) +} + +// GetReceivedByAddress returns the total amount received by the specified address with at least the default number of +// minimum confirmations. +// +// See GetReceivedByAddressMinConf to override the minimum number of confirmations. +func (c *Client) GetReceivedByAddress(address btcaddr.Address) (amt.Amount, error) { + return c.GetReceivedByAddressAsync(address).Receive() +} + +// GetReceivedByAddressMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See GetReceivedByAddressMinConf for the blocking version and more details. +func (c *Client) GetReceivedByAddressMinConfAsync( + address btcaddr.Address, + minConfirms int, +) FutureGetReceivedByAddressResult { + addr := address.EncodeAddress() + cmd := btcjson.NewGetReceivedByAddressCmd(addr, &minConfirms) + return c.sendCmd(cmd) +} + +// GetReceivedByAddressMinConf returns the total amount received by the specified address with at least the specified +// number of minimum confirmations. +// +// See GetReceivedByAddress to use the default minimum number of confirmations. +func (c *Client) GetReceivedByAddressMinConf(address btcaddr.Address, minConfirms int) (amt.Amount, error) { + return c.GetReceivedByAddressMinConfAsync(address, minConfirms).Receive() +} + +// FutureListReceivedByAccountResult is a future promise to deliver the result of a ListReceivedByAccountAsync, +// ListReceivedByAccountMinConfAsync, or ListReceivedByAccountIncludeEmptyAsync RPC invocation (or an applicable error). +type FutureListReceivedByAccountResult chan *response + +// Receive waits for the response promised by the future and returns a list of balances by account. +func (r FutureListReceivedByAccountResult) Receive() ([]btcjson.ListReceivedByAccountResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal as an array of listreceivedbyaccount result objects. + var received []btcjson.ListReceivedByAccountResult + e = js.Unmarshal(res, &received) + if e != nil { + return nil, e + } + return received, nil +} + +// ListReceivedByAccountAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ListReceivedByAccount for the blocking version and more details. +func (c *Client) ListReceivedByAccountAsync() FutureListReceivedByAccountResult { + cmd := btcjson.NewListReceivedByAccountCmd(nil, nil, nil) + return c.sendCmd(cmd) +} + +// ListReceivedByAccount lists balances by account using the default number of minimum confirmations and including +// accounts that haven't received any payments. +// +// See ListReceivedByAccountMinConf to override the minimum number of confirmations and +// ListReceivedByAccountIncludeEmpty to filter accounts that haven't received any payments from the results. +func (c *Client) ListReceivedByAccount() ([]btcjson.ListReceivedByAccountResult, error) { + return c.ListReceivedByAccountAsync().Receive() +} + +// ListReceivedByAccountMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See ListReceivedByAccountMinConf for the blocking version and more details. +func (c *Client) ListReceivedByAccountMinConfAsync(minConfirms int) FutureListReceivedByAccountResult { + cmd := btcjson.NewListReceivedByAccountCmd(&minConfirms, nil, nil) + return c.sendCmd(cmd) +} + +// ListReceivedByAccountMinConf lists balances by account using the specified number of minimum confirmations not +// including accounts that haven't received any payments. +// +// See ListReceivedByAccount to use the default minimum number of confirmations and ListReceivedByAccountIncludeEmpty to +// also include accounts that haven't received any payments in the results. +func (c *Client) ListReceivedByAccountMinConf(minConfirms int) ([]btcjson.ListReceivedByAccountResult, error) { + return c.ListReceivedByAccountMinConfAsync(minConfirms).Receive() +} + +// ListReceivedByAccountIncludeEmptyAsync returns an instance of a type that can be used to get the result of the RPC at +// some future time by invoking the Receive function on the returned instance. +// +// See ListReceivedByAccountIncludeEmpty for the blocking version and more details. +func (c *Client) ListReceivedByAccountIncludeEmptyAsync( + minConfirms int, + includeEmpty bool, +) FutureListReceivedByAccountResult { + cmd := btcjson.NewListReceivedByAccountCmd( + &minConfirms, &includeEmpty, + nil, + ) + return c.sendCmd(cmd) +} + +// ListReceivedByAccountIncludeEmpty lists balances by account using the specified number of minimum confirmations and +// including accounts that haven't received any payments depending on specified flag. +// +// See ListReceivedByAccount and ListReceivedByAccountMinConf to use defaults. +func (c *Client) ListReceivedByAccountIncludeEmpty( + minConfirms int, + includeEmpty bool, +) ([]btcjson.ListReceivedByAccountResult, error) { + return c.ListReceivedByAccountIncludeEmptyAsync( + minConfirms, + includeEmpty, + ).Receive() +} + +// FutureListReceivedByAddressResult is a future promise to deliver the result of a ListReceivedByAddressAsync, +// ListReceivedByAddressMinConfAsync, or ListReceivedByAddressIncludeEmptyAsync RPC invocation (or an applicable error). +type FutureListReceivedByAddressResult chan *response + +// Receive waits for the response promised by the future and returns a list of balances by address. +func (r FutureListReceivedByAddressResult) Receive() ([]btcjson.ListReceivedByAddressResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal as an array of listreceivedbyaddress result objects. + var received []btcjson.ListReceivedByAddressResult + e = js.Unmarshal(res, &received) + if e != nil { + return nil, e + } + return received, nil +} + +// ListReceivedByAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ListReceivedByAddress for the blocking version and more details. +func (c *Client) ListReceivedByAddressAsync() FutureListReceivedByAddressResult { + cmd := btcjson.NewListReceivedByAddressCmd(nil, nil, nil) + return c.sendCmd(cmd) +} + +// ListReceivedByAddress lists balances by address using the default number of minimum confirmations not including +// addresses that haven't received any payments or watching only addresses. +// +// See ListReceivedByAddressMinConf to override the minimum number of confirmations and +// ListReceivedByAddressIncludeEmpty to also include addresses that haven't received any payments in the results. +func (c *Client) ListReceivedByAddress() ([]btcjson.ListReceivedByAddressResult, error) { + return c.ListReceivedByAddressAsync().Receive() +} + +// ListReceivedByAddressMinConfAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See ListReceivedByAddressMinConf for the blocking version and more details. +func (c *Client) ListReceivedByAddressMinConfAsync(minConfirms int) FutureListReceivedByAddressResult { + cmd := btcjson.NewListReceivedByAddressCmd(&minConfirms, nil, nil) + return c.sendCmd(cmd) +} + +// ListReceivedByAddressMinConf lists balances by address using the specified number of minimum confirmations not +// including addresses that haven't received any payments. +// +// See ListReceivedByAddress to use the default minimum number of confirmations and ListReceivedByAddressIncludeEmpty to +// also include addresses that haven't received any payments in the results. +func (c *Client) ListReceivedByAddressMinConf(minConfirms int) ([]btcjson.ListReceivedByAddressResult, error) { + return c.ListReceivedByAddressMinConfAsync(minConfirms).Receive() +} + +// ListReceivedByAddressIncludeEmptyAsync returns an instance of a type that can be used to get the result of the RPC at +// some future time by invoking the Receive function on the returned instance. +// +// See ListReceivedByAccountIncludeEmpty for the blocking version and more details. +func (c *Client) ListReceivedByAddressIncludeEmptyAsync( + minConfirms int, + includeEmpty bool, +) FutureListReceivedByAddressResult { + cmd := btcjson.NewListReceivedByAddressCmd( + &minConfirms, &includeEmpty, + nil, + ) + return c.sendCmd(cmd) +} + +// ListReceivedByAddressIncludeEmpty lists balances by address using the specified number of minimum confirmations and +// including addresses that haven't received any payments depending on specified flag. +// +// See ListReceivedByAddress and ListReceivedByAddressMinConf to use defaults. +func (c *Client) ListReceivedByAddressIncludeEmpty( + minConfirms int, + includeEmpty bool, +) ([]btcjson.ListReceivedByAddressResult, error) { + return c.ListReceivedByAddressIncludeEmptyAsync( + minConfirms, + includeEmpty, + ).Receive() +} + +// ************************ +// Wallet Locking Functions +// ************************ + +// FutureWalletLockResult is a future promise to deliver the result of a WalletLockAsync RPC invocation (or an +// applicable error). +type FutureWalletLockResult chan *response + +// Receive waits for the response promised by the future and returns the result of locking the wallet. +func (r FutureWalletLockResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// WalletLockAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See WalletLock for the blocking version and more details. +func (c *Client) WalletLockAsync() FutureWalletLockResult { + cmd := btcjson.NewWalletLockCmd() + return c.sendCmd(cmd) +} + +// WalletLock locks the wallet by removing the encryption key from memory. +// +// After calling this function, the WalletPassphrase function must be used to unlock the wallet prior to calling any +// other function which requires the wallet to be unlocked. +func (c *Client) WalletLock() (e error) { + return c.WalletLockAsync().Receive() +} + +// WalletPassphrase unlocks the wallet by using the passphrase to derive the decryption key which is then stored in +// memory for the specified timeout (in seconds). +func (c *Client) WalletPassphrase(passphrase string, timeoutSecs int64) (e error) { + cmd := btcjson.NewWalletPassphraseCmd(passphrase, timeoutSecs) + _, e = c.sendCmdAndWait(cmd) + return e +} + +// FutureWalletPassphraseChangeResult is a future promise to deliver the result of a WalletPassphraseChangeAsync RPC +// invocation (or an applicable error). +type FutureWalletPassphraseChangeResult chan *response + +// Receive waits for the response promised by the future and returns the result of changing the wallet passphrase. +func (r FutureWalletPassphraseChangeResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// WalletPassphraseChangeAsync returns an instance of a type that can be used to get the result of the RPC at some +// future time by invoking the Receive function on the returned instance. +// +// See WalletPassphraseChange for the blocking version and more details. +func (c *Client) WalletPassphraseChangeAsync(old, new string) FutureWalletPassphraseChangeResult { + cmd := btcjson.NewWalletPassphraseChangeCmd(old, new) + return c.sendCmd(cmd) +} + +// WalletPassphraseChange changes the wallet passphrase from the specified old to new passphrase. +func (c *Client) WalletPassphraseChange(old, new string) (e error) { + return c.WalletPassphraseChangeAsync(old, new).Receive() +} + +// ************************* +// Message Signing Functions +// ************************* + +// FutureSignMessageResult is a future promise to deliver the result of a SignMessageAsync RPC invocation (or an +// applicable error). +type FutureSignMessageResult chan *response + +// Receive waits for the response promised by the future and returns the message signed with the private key of the +// specified address. +func (r FutureSignMessageResult) Receive() (string, error) { + res, e := receiveFuture(r) + if e != nil { + return "", e + } + // Unmarshal result as a string. + var b64 string + e = js.Unmarshal(res, &b64) + if e != nil { + return "", e + } + return b64, nil +} + +// SignMessageAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. See SignMessage for the blocking version and more details. +func (c *Client) SignMessageAsync(address btcaddr.Address, message string) FutureSignMessageResult { + addr := address.EncodeAddress() + cmd := btcjson.NewSignMessageCmd(addr, message) + return c.sendCmd(cmd) +} + +// SignMessage signs a message with the private key of the specified address. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) SignMessage(address btcaddr.Address, message string) (string, error) { + return c.SignMessageAsync(address, message).Receive() +} + +// FutureVerifyMessageResult is a future promise to deliver the result of a VerifyMessageAsync RPC invocation (or an +// applicable error). +type FutureVerifyMessageResult chan *response + +// Receive waits for the response promised by the future and returns whether or not the message was successfully +// verified. +func (r FutureVerifyMessageResult) Receive() (bool, error) { + res, e := receiveFuture(r) + if e != nil { + return false, e + } + // Unmarshal result as a boolean. + var verified bool + e = js.Unmarshal(res, &verified) + if e != nil { + return false, e + } + return verified, nil +} + +// VerifyMessageAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See VerifyMessage for the blocking version and more details. +func (c *Client) VerifyMessageAsync(address btcaddr.Address, signature, message string) FutureVerifyMessageResult { + addr := address.EncodeAddress() + cmd := btcjson.NewVerifyMessageCmd(addr, signature, message) + return c.sendCmd(cmd) +} + +// VerifyMessage verifies a signed message. +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) VerifyMessage(address btcaddr.Address, signature, message string) (bool, error) { + return c.VerifyMessageAsync(address, signature, message).Receive() +} + +// ********************* +// Dump/Import Functions +// ********************* + +// FutureDumpPrivKeyResult is a future promise to deliver the result of a DumpPrivKeyAsync RPC invocation (or an +// applicable error). +type FutureDumpPrivKeyResult chan *response + +// Receive waits for the response promised by the future and returns the private key corresponding to the passed address +// encoded in the wallet import format (WIF) +func (r FutureDumpPrivKeyResult) Receive() (*util.WIF, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a string. + var privKeyWIF string + e = js.Unmarshal(res, &privKeyWIF) + if e != nil { + return nil, e + } + return util.DecodeWIF(privKeyWIF) +} + +// DumpPrivKeyAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See DumpPrivKey for the blocking version and more details. +func (c *Client) DumpPrivKeyAsync(address btcaddr.Address) FutureDumpPrivKeyResult { + addr := address.EncodeAddress() + cmd := btcjson.NewDumpPrivKeyCmd(addr) + return c.sendCmd(cmd) +} + +// DumpPrivKey gets the private key corresponding to the passed address encoded in the wallet import format (WIF). +// +// NOTE: This function requires to the wallet to be unlocked. See the WalletPassphrase function for more details. +func (c *Client) DumpPrivKey(address btcaddr.Address) (*util.WIF, error) { + return c.DumpPrivKeyAsync(address).Receive() +} + +// FutureImportAddressResult is a future promise to deliver the result of an ImportAddressAsync RPC invocation (or an +// applicable error). +type FutureImportAddressResult chan *response + +// Receive waits for the response promised by the future and returns the result of importing the passed public address. +func (r FutureImportAddressResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// ImportAddressAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See ImportAddress for the blocking version and more details. +func (c *Client) ImportAddressAsync(address string) FutureImportAddressResult { + cmd := btcjson.NewImportAddressCmd(address, "", nil) + return c.sendCmd(cmd) +} + +// ImportAddress imports the passed public address. +func (c *Client) ImportAddress(address string) (e error) { + return c.ImportAddressAsync(address).Receive() +} + +// ImportAddressRescanAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ImportAddress for the blocking version and more details. +func (c *Client) ImportAddressRescanAsync(address string, account string, rescan bool) FutureImportAddressResult { + cmd := btcjson.NewImportAddressCmd(address, account, &rescan) + return c.sendCmd(cmd) +} + +// ImportAddressRescan imports the passed public address. When rescan is true, the block history is scanned for +// transactions addressed to provided address. +func (c *Client) ImportAddressRescan(address string, account string, rescan bool) (e error) { + return c.ImportAddressRescanAsync(address, account, rescan).Receive() +} + +// FutureImportPrivKeyResult is a future promise to deliver the result of an ImportPrivKeyAsync RPC invocation (or an +// applicable error). +type FutureImportPrivKeyResult chan *response + +// Receive waits for the response promised by the future and returns the result of importing the passed private key +// which must be the wallet import format (WIF). +func (r FutureImportPrivKeyResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// ImportPrivKeyAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See ImportPrivKey for the blocking version and more details. +func (c *Client) ImportPrivKeyAsync(privKeyWIF *util.WIF) FutureImportPrivKeyResult { + wif := "" + if privKeyWIF != nil { + wif = privKeyWIF.String() + } + cmd := btcjson.NewImportPrivKeyCmd(wif, nil, nil) + return c.sendCmd(cmd) +} + +// ImportPrivKey imports the passed private key which must be the wallet import format (WIF). +func (c *Client) ImportPrivKey(privKeyWIF *util.WIF) (e error) { + return c.ImportPrivKeyAsync(privKeyWIF).Receive() +} + +// ImportPrivKeyLabelAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ImportPrivKey for the blocking version and more details. +func (c *Client) ImportPrivKeyLabelAsync(privKeyWIF *util.WIF, label string) FutureImportPrivKeyResult { + wif := "" + if privKeyWIF != nil { + wif = privKeyWIF.String() + } + cmd := btcjson.NewImportPrivKeyCmd(wif, &label, nil) + return c.sendCmd(cmd) +} + +// ImportPrivKeyLabel imports the passed private key which must be the wallet import format (WIF). It sets the account +// label to the one provided. +func (c *Client) ImportPrivKeyLabel(privKeyWIF *util.WIF, label string) (e error) { + return c.ImportPrivKeyLabelAsync(privKeyWIF, label).Receive() +} + +// ImportPrivKeyRescanAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ImportPrivKey for the blocking version and more details. +func (c *Client) ImportPrivKeyRescanAsync(privKeyWIF *util.WIF, label string, rescan bool) FutureImportPrivKeyResult { + wif := "" + if privKeyWIF != nil { + wif = privKeyWIF.String() + } + cmd := btcjson.NewImportPrivKeyCmd(wif, &label, &rescan) + return c.sendCmd(cmd) +} + +// ImportPrivKeyRescan imports the passed private key which must be the wallet import format (WIF). It sets the account +// label to the one provided. When rescan is true, the block history is scanned for transactions addressed to provided +// privKey. +func (c *Client) ImportPrivKeyRescan(privKeyWIF *util.WIF, label string, rescan bool) (e error) { + return c.ImportPrivKeyRescanAsync(privKeyWIF, label, rescan).Receive() +} + +// FutureImportPubKeyResult is a future promise to deliver the result of an ImportPubKeyAsync RPC invocation (or an +// applicable error). +type FutureImportPubKeyResult chan *response + +// Receive waits for the response promised by the future and returns the result of importing the passed public key. +func (r FutureImportPubKeyResult) Receive() (e error) { + _, e = receiveFuture(r) + return e +} + +// ImportPubKeyAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See ImportPubKey for the blocking version and more details. +func (c *Client) ImportPubKeyAsync(pubKey string) FutureImportPubKeyResult { + cmd := btcjson.NewImportPubKeyCmd(pubKey, nil) + return c.sendCmd(cmd) +} + +// ImportPubKey imports the passed public key. +func (c *Client) ImportPubKey(pubKey string) (e error) { + return c.ImportPubKeyAsync(pubKey).Receive() +} + +// ImportPubKeyRescanAsync returns an instance of a type that can be used to get the result of the RPC at some future +// time by invoking the Receive function on the returned instance. +// +// See ImportPubKey for the blocking version and more details. +func (c *Client) ImportPubKeyRescanAsync(pubKey string, rescan bool) FutureImportPubKeyResult { + cmd := btcjson.NewImportPubKeyCmd(pubKey, &rescan) + return c.sendCmd(cmd) +} + +// ImportPubKeyRescan imports the passed public key. When rescan is true, the block history is scanned for transactions +// addressed to provided pubkey. +func (c *Client) ImportPubKeyRescan(pubKey string, rescan bool) (e error) { + return c.ImportPubKeyRescanAsync(pubKey, rescan).Receive() +} + +// *********************** +// Miscellaneous Functions +// *********************** + +// NOTE: While getinfo is implemented here (in wallet.go), a pod chain server will respond to getinfo requests as well, +// excluding any wallet information. + +// FutureGetInfoResult is a future promise to deliver the result of a GetInfoAsync RPC invocation (or an applicable +// error). +type FutureGetInfoResult chan *response + +// Receive waits for the response promised by the future and returns the info provided by the server. +func (r FutureGetInfoResult) Receive() (*btcjson.InfoWalletResult, error) { + res, e := receiveFuture(r) + if e != nil { + return nil, e + } + // Unmarshal result as a getinfo result object. + var infoRes btcjson.InfoWalletResult + e = js.Unmarshal(res, &infoRes) + if e != nil { + return nil, e + } + return &infoRes, nil +} + +// GetInfoAsync returns an instance of a type that can be used to get the result of the RPC at some future time by +// invoking the Receive function on the returned instance. +// +// See GetInfo for the blocking version and more details. +func (c *Client) GetInfoAsync() FutureGetInfoResult { + cmd := btcjson.NewGetInfoCmd() + return c.sendCmd(cmd) +} + +// GetInfo returns miscellaneous info regarding the RPC server. The returned info object may be void of wallet +// information if the remote server does not include wallet functionality. +func (c *Client) GetInfo() (*btcjson.InfoWalletResult, error) { + return c.GetInfoAsync().Receive() +} + +// TODO(davec): Implement +// backupwallet (NYI in btcwallet) +// encryptwallet (Won't be supported by btcwallet since it's always encrypted) +// getwalletinfo (NYI in btcwallet or json) +// listaddressgroupings (NYI in btcwallet) +// listreceivedbyaccount (NYI in btcwallet) +// DUMP +// importwallet (NYI in btcwallet) +// dumpwallet (NYI in btcwallet) diff --git a/pkg/rpchelp/gen/log.go b/pkg/rpchelp/gen/log.go new file mode 100644 index 0000000..da27aa5 --- /dev/null +++ b/pkg/rpchelp/gen/log.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/rpchelp/gen/main.go b/pkg/rpchelp/gen/main.go new file mode 100644 index 0000000..a8c8a32 --- /dev/null +++ b/pkg/rpchelp/gen/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "github.com/p9c/p9/pkg/rpchelp" + "os" + "strings" + + "github.com/p9c/p9/pkg/btcjson" +) + +var outputFile = func() *os.File { + fi, e := os.Create("../rpcserverhelp.go") + if e != nil { + F.Ln(e) + } + return fi +}() + +func writefln(format string, args ...interface{}) { + _, e := fmt.Fprintf(outputFile, format, args...) + if e != nil { + F.Ln(e) + } + _, e = outputFile.Write([]byte{'\n'}) + if e != nil { + F.Ln(e) + } +} +func writeLocaleHelp(locale, goLocale string, descs map[string]string) { + funcName := "helpDescs" + goLocale + writefln("func %s() map[string]string {", funcName) + writefln("return map[string]string{") + for i := range rpchelp.Methods { + m := &rpchelp.Methods[i] + helpText, e := btcjson.GenerateHelp(m.Method, descs, m.ResultTypes...) + if e != nil { + F.Ln(e) + } + writefln("%q: %q,", m.Method, helpText) + } + writefln("}") + writefln("}") +} +func writeLocales() { + writefln("var localeHelpDescs = map[string]func() map[string]string{") + for _, h := range rpchelp.HelpDescs { + writefln("%q: helpDescs%s,", h.Locale, h.GoLocale) + } + writefln("}") +} +func writeUsage() { + usageStrs := make([]string, len(rpchelp.Methods)) + var e error + for i := range rpchelp.Methods { + usageStrs[i], e = btcjson.MethodUsageText(rpchelp.Methods[i].Method) + if e != nil { + F.Ln(e) + } + } + usages := strings.Join(usageStrs, "\n") + writefln("var requestUsages = %q", usages) +} +func main() { + defer func() { + if e := outputFile.Close(); E.Chk(e) { + } + }() + packageName := "main" + if len(os.Args) > 1 { + packageName = os.Args[1] + } + writefln("// AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit.") + writefln("") + writefln("package %s", packageName) + writefln("") + for _, h := range rpchelp.HelpDescs { + writeLocaleHelp(h.Locale, h.GoLocale, h.Descs) + writefln("") + } + writeLocales() + writefln("") + writeUsage() +} diff --git a/pkg/rpchelp/helpdescs_en_US.go b/pkg/rpchelp/helpdescs_en_US.go new file mode 100644 index 0000000..975a137 --- /dev/null +++ b/pkg/rpchelp/helpdescs_en_US.go @@ -0,0 +1,347 @@ +// +build !generate + +package rpchelp + +var helpDescsEnUS = map[string]string{ + // AddMultisigAddressCmd help. + "addmultisigaddress--synopsis": "Generates and imports a multisig address and redeeming script to the 'imported' account.", + "addmultisigaddress-account": "DEPRECATED -- Unused (all imported addresses belong to the imported account)", + "addmultisigaddress-keys": "Pubkeys and/or pay-to-pubkey-hash addresses to partially control the multisig address", + "addmultisigaddress-nrequired": "The number of signatures required to redeem outputs paid to this address", + "addmultisigaddress--result0": "The imported pay-to-script-hash address", + // CreateMultisigCmd help. + "createmultisig--synopsis": "Generate a multisig address and redeem script.", + "createmultisig-keys": "Pubkeys and/or pay-to-pubkey-hash addresses to partially control the multisig address", + "createmultisig-nrequired": "The number of signatures required to redeem outputs paid to this address", + // CreateMultisigResult help. + "createmultisigresult-address": "The generated pay-to-script-hash address", + "createmultisigresult-redeemScript": "The script required to redeem outputs paid to the multisig address", + // DumpPrivKeyCmd help. + "dumpprivkey--synopsis": "Returns the private key in WIF encoding that controls some wallet address.", + "dumpprivkey-address": "The address to return a private key for", + "dumpprivkey--result0": "The WIF-encoded private key", + // GetAccountCmd help. + "getaccount--synopsis": "DEPRECATED -- Lookup the account name that some wallet address belongs to.", + "getaccount-address": "The address to query the account for", + "getaccount--result0": "The name of the account that 'address' belongs to", + // GetAccountAddressCmd help. + "getaccountaddress--synopsis": "DEPRECATED -- Returns the most recent external payment address for an account that has not been seen publicly.\n" + + "A new address is generated for the account if the most recently generated address has been seen on the blockchain or in mempool.", + "getaccountaddress-account": "The account of the returned address", + "getaccountaddress--result0": "The unused address for 'account'", + // GetAddressesByAccountCmd help. + "getaddressesbyaccount--synopsis": "DEPRECATED -- Returns all addresses strings controlled by a single account.", + "getaddressesbyaccount-account": "Account name to fetch addresses for", + "getaddressesbyaccount--result0": "All addresses controlled by 'account'", + // GetBalanceCmd help. + "getbalance--synopsis": "Calculates and returns the balance of one or all accounts.", + "getbalance-minconf": "Minimum number of block confirmations required before an unspent output's value is included in the balance", + "getbalance-account": "DEPRECATED -- The account name to query the balance for, or \"*\" to consider all accounts (default=\"*\")", + "getbalance--condition0": "account != \"*\"", + "getbalance--condition1": "account = \"*\"", + "getbalance--result0": "The balance of 'account' valued in bitcoin", + "getbalance--result1": "The balance of all accounts valued in bitcoin", + // GetBestBlockHashCmd help. + "getbestblockhash--synopsis": "Returns the hash of the newest block in the best chain that wallet has finished syncing with.", + "getbestblockhash--result0": "The hash of the most recent synced-to block", + // GetBlockCountCmd help. + "getblockcount--synopsis": "Returns the blockchain height of the newest block in the best chain that wallet has finished syncing with.", + "getblockcount--result0": "The blockchain height of the most recent synced-to block", + // GetInfoCmd help. + "getinfo--synopsis": "Returns a JSON object containing various state info.", + // InfoWalletResult help. + "infowalletresult-version": "The version of the server", + "infowalletresult-protocolversion": "The latest supported protocol version", + "infowalletresult-blocks": "The number of blocks processed", + "infowalletresult-timeoffset": "The time offset", + "infowalletresult-connections": "The number of connected peers", + "infowalletresult-proxy": "The proxy used by the server", + "infowalletresult-difficulty": "The current target difficulty", + "infowalletresult-testnet": "Whether or not server is using testnet", + "infowalletresult-relayfee": "The minimum relay fee for non-free transactions in DUO/KB", + "infowalletresult-errors": "Any current errors", + "infowalletresult-paytxfee": "The increment used each time more fee is required for an authored transaction", + "infowalletresult-balance": "The balance of all accounts calculated with one block confirmation", + "infowalletresult-walletversion": "The version of the address manager database", + "infowalletresult-unlocked_until": "Unset", + "infowalletresult-keypoolsize": "Unset", + "infowalletresult-keypoololdest": "Unset", + // GetNewAddressCmd help. + "getnewaddress--synopsis": "Generates and returns a new payment address.", + "getnewaddress-account": "DEPRECATED -- Account name the new address will belong to (default=\"default\")", + "getnewaddress--result0": "The payment address", + // GetRawChangeAddressCmd help. + "getrawchangeaddress--synopsis": "Generates and returns a new internal payment address for use as a change address in raw transactions.", + "getrawchangeaddress-account": "Account name the new internal address will belong to (default=\"default\")", + "getrawchangeaddress--result0": "The internal payment address", + // GetReceivedByAccountCmd help. + "getreceivedbyaccount--synopsis": "DEPRECATED -- Returns the total amount received by addresses of some account, including spent outputs.", + "getreceivedbyaccount-account": "Account name to query total received amount for", + "getreceivedbyaccount-minconf": "Minimum number of block confirmations required before an output's value is included in the total", + "getreceivedbyaccount--result0": "The total received amount valued in bitcoin", + // GetReceivedByAddressCmd help. + "getreceivedbyaddress--synopsis": "Returns the total amount received by a single address, including spent outputs.", + "getreceivedbyaddress-address": "Payment address which received outputs to include in total", + "getreceivedbyaddress-minconf": "Minimum number of block confirmations required before an output's value is included in the total", + "getreceivedbyaddress--result0": "The total received amount valued in bitcoin", + // GetTransactionCmd help. + "gettransaction--synopsis": "Returns a JSON object with details regarding a transaction relevant to this wallet.", + "gettransaction-txid": "Hash of the transaction to query", + "gettransaction-includewatchonly": "Also consider transactions involving watched addresses", + // HelpCmd help. + "help--synopsis": "Returns a list of all commands or help for a specified command.", + "help-command": "The command to retrieve help for", + "help--condition0": "no command provided", + "help--condition1": "command specified", + "help--result0": "List of commands", + "help--result1": "Help for specified command", + // GetTransactionResult help. + "gettransactionresult-amount": "The total amount this transaction credits to the wallet, valued in bitcoin", + "gettransactionresult-fee": "The total input value minus the total output value, or 0 if 'txid' is not a sent transaction", + "gettransactionresult-confirmations": "The number of block confirmations of the transaction", + "gettransactionresult-blockhash": "The hash of the block this transaction is mined in, or the empty string if unmined", + "gettransactionresult-blockindex": "Unset", + "gettransactionresult-blocktime": "The Unix time of the block header this transaction is mined in, or 0 if unmined", + "gettransactionresult-txid": "The transaction hash", + "gettransactionresult-walletconflicts": "Unset", + "gettransactionresult-time": "The earliest Unix time this transaction was known to exist", + "gettransactionresult-timereceived": "The earliest Unix time this transaction was known to exist", + "gettransactionresult-details": "Additional details for each recorded wallet credit and debit", + "gettransactionresult-hex": "The transaction encoded as a hexadecimal string", + // GetTransactionDetailsResult help. + "gettransactiondetailsresult-account": "DEPRECATED -- Unset", + "gettransactiondetailsresult-address": "The address an output was paid to, or the empty string if the output is nonstandard or this detail is regarding a transaction input", + "gettransactiondetailsresult-category": `The kind of detail: "send" for sent transactions, "immature" for immature coinbase outputs, "generate" for mature coinbase outputs, or "recv" for all other received outputs`, + "gettransactiondetailsresult-amount": "The amount of a received output", + "gettransactiondetailsresult-fee": "The included fee for a sent transaction", + "gettransactiondetailsresult-vout": "The transaction output index", + "gettransactiondetailsresult-involveswatchonly": "Unset", + // ImportPrivKeyCmd help. + "importprivkey--synopsis": "Imports a WIF-encoded private key to the 'imported' account.", + "importprivkey-privkey": "The WIF-encoded private key", + "importprivkey-label": "Unused (must be unset or 'imported')", + "importprivkey-rescan": "Rescan the blockchain (since the genesis block) for outputs controlled by the imported key", + // KeypoolRefillCmd help. + "keypoolrefill--synopsis": "DEPRECATED -- This request does nothing since no keypool is maintained.", + "keypoolrefill-newsize": "Unused", + // ListAccountsCmd help. + "listaccounts--synopsis": "DEPRECATED -- Returns a JSON object of all accounts and their balances.", + "listaccounts-minconf": "Minimum number of block confirmations required before an unspent output's value is included in the balance", + "listaccounts--result0--desc": "JSON object with account names as keys and bitcoin amounts as values", + "listaccounts--result0--key": "The account name", + "listaccounts--result0--value": "The account balance valued in bitcoin", + // ListLockUnspentCmd help. + "listlockunspent--synopsis": "Returns a JSON array of outpoints marked as locked (with lockunspent) for this wallet session.", + // TransactionInput help. + "transactioninput-txid": "The transaction hash of the referenced output", + "transactioninput-vout": "The output index of the referenced output", + // ListReceivedByAccountCmd help. + "listreceivedbyaccount--synopsis": "DEPRECATED -- Returns a JSON array of objects listing all accounts and the total amount received by each account.", + "listreceivedbyaccount-minconf": "Minimum number of block confirmations required before a transaction is considered", + "listreceivedbyaccount-includeempty": "Unused", + "listreceivedbyaccount-includewatchonly": "Unused", + // ListReceivedByAccountResult help. + "listreceivedbyaccountresult-account": "The name of the account", + "listreceivedbyaccountresult-amount": "Total amount received by payment addresses of the account valued in bitcoin", + "listreceivedbyaccountresult-confirmations": "Number of block confirmations of the most recent transaction relevant to the account", + // ListReceivedByAddressCmd help. + "listreceivedbyaddress--synopsis": "Returns a JSON array of objects listing wallet payment addresses and their total received amounts.", + "listreceivedbyaddress-minconf": "Minimum number of block confirmations required before a transaction is considered", + "listreceivedbyaddress-includeempty": "Unused", + "listreceivedbyaddress-includewatchonly": "Unused", + // ListReceivedByAddressResult help. + "listreceivedbyaddressresult-account": "DEPRECATED -- Unset", + "listreceivedbyaddressresult-address": "The payment address", + "listreceivedbyaddressresult-amount": "Total amount received by the payment address valued in bitcoin", + "listreceivedbyaddressresult-confirmations": "Number of block confirmations of the most recent transaction relevant to the address", + "listreceivedbyaddressresult-txids": "Transaction hashes of all transactions involving this address", + "listreceivedbyaddressresult-involvesWatchonly": "Unset", + // ListSinceBlockCmd help. + "listsinceblock--synopsis": "Returns a JSON array of objects listing details of all wallet transactions after some block.", + "listsinceblock-blockhash": "Hash of the parent block of the first block to consider transactions from, or unset to list all transactions", + "listsinceblock-targetconfirmations": "Minimum number of block confirmations of the last block in the result object. Must be 1 or greater. Note: The transactions array in the result object is not affected by this parameter", + "listsinceblock-includewatchonly": "Unused", + "listsinceblock--condition0": "blockhash specified", + "listsinceblock--condition1": "no blockhash specified", + "listsinceblock--result0": "Lists all transactions, including unmined transactions, since the specified block", + "listsinceblock--result1": "Lists all transactions since the genesis block", + // ListSinceBlockResult help. + "listsinceblockresult-transactions": "JSON array of objects containing verbose details of the each transaction", + "listsinceblockresult-lastblock": "Hash of the latest-synced block to be used in later calls to listsinceblock", + // ListTransactionsResult help. + "listtransactionsresult-account": "DEPRECATED -- Unset", + "listtransactionsresult-address": "Payment address for a transaction output", + "listtransactionsresult-category": `The kind of transaction: "send" for sent transactions, "immature" for immature coinbase outputs, "generate" for mature coinbase outputs, or "recv" for all other received outputs. Note: A single output may be included multiple times under different categories`, + "listtransactionsresult-amount": "The value of the transaction output valued in bitcoin", + "listtransactionsresult-fee": "The total input value minus the total output value for sent transactions", + "listtransactionsresult-confirmations": "The number of block confirmations of the transaction", + "listtransactionsresult-generated": "Whether the transaction output is a coinbase output", + "listtransactionsresult-blockhash": "The hash of the block this transaction is mined in, or the empty string if unmined", + "listtransactionsresult-blockindex": "Unset", + "listtransactionsresult-blocktime": "The Unix time of the block header this transaction is mined in, or 0 if unmined", + "listtransactionsresult-txid": "The hash of the transaction", + "listtransactionsresult-vout": "The transaction output index", + "listtransactionsresult-walletconflicts": "Unset", + "listtransactionsresult-time": "The earliest Unix time this transaction was known to exist", + "listtransactionsresult-timereceived": "The earliest Unix time this transaction was known to exist", + "listtransactionsresult-involveswatchonly": "Unset", + "listtransactionsresult-comment": "Unset", + "listtransactionsresult-otheraccount": "Unset", + "listtransactionsresult-trusted": "Unset", + "listtransactionsresult-bip125-replaceable": "Unset", + "listtransactionsresult-abandoned": "Unset", + // ListTransactionsCmd help. + "listtransactions--synopsis": "Returns a JSON array of objects containing verbose details for wallet transactions.", + "listtransactions-account": "DEPRECATED -- Unused (must be unset or \"*\")", + "listtransactions-count": "Maximum number of transactions to create results from", + "listtransactions-from": "Number of transactions to skip before results are created", + "listtransactions-includewatchonly": "Unused", + // ListUnspentCmd help. + "listunspent--synopsis": "Returns a JSON array of objects representing unlocked unspent outputs controlled by wallet keys.", + "listunspent-minconf": "Minimum number of block confirmations required before a transaction output is considered", + "listunspent-maxconf": "Maximum number of block confirmations required before a transaction output is excluded", + "listunspent-addresses": "If set, limits the returned details to unspent outputs received by any of these payment addresses", + // ListUnspentResult help. + "listunspentresult-txid": "The transaction hash of the referenced output", + "listunspentresult-vout": "The output index of the referenced output", + "listunspentresult-address": "The payment address that received the output", + "listunspentresult-account": "The account associated with the receiving payment address", + "listunspentresult-scriptPubKey": "The output script encoded as a hexadecimal string", + "listunspentresult-redeemScript": "Unset", + "listunspentresult-amount": "The amount of the output valued in bitcoin", + "listunspentresult-confirmations": "The number of block confirmations of the transaction", + "listunspentresult-spendable": "Whether the output is entirely controlled by wallet keys/scripts (false for partially controlled multisig outputs or outputs to watch-only addresses)", + // LockUnspentCmd help. + "lockunspent--synopsis": "Locks or unlocks an unspent output.\n" + + "Locked outputs are not chosen for transaction inputs of authored transactions and are not included in 'listunspent' results.\n" + + "Locked outputs are volatile and are not saved across wallet restarts.\n" + + "If unlock is true and no transaction outputs are specified, all locked outputs are marked unlocked.", + "lockunspent-unlock": "True to unlock outputs, false to lock", + "lockunspent-transactions": "Transaction outputs to lock or unlock", + "lockunspent--result0": "The boolean 'true'", + // SendFromCmd help. + "sendfrom--synopsis": "DEPRECATED -- Authors, signs, and sends a transaction that outputs some amount to a payment address.\n" + + "A change output is automatically included to send extra output value back to the original account.", + "sendfrom-fromaccount": "Account to pick unspent outputs from", + "sendfrom-toaddress": "Address to pay", + "sendfrom-amount": "Amount to send to the payment address valued in bitcoin", + "sendfrom-minconf": "Minimum number of block confirmations required before a transaction output is eligible to be spent", + "sendfrom-comment": "Unused", + "sendfrom-commentto": "Unused", + "sendfrom--result0": "The transaction hash of the sent transaction", + // SendManyCmd help. + "sendmany--synopsis": "Authors, signs, and sends a transaction that outputs to many payment addresses.\n" + + "A change output is automatically included to send extra output value back to the original account.", + "sendmany-fromaccount": "DEPRECATED -- Account to pick unspent outputs from", + "sendmany-amounts": "Pairs of payment addresses and the output amount to pay each", + "sendmany-amounts--desc": "JSON object using payment addresses as keys and output amounts valued in bitcoin to send to each address", + "sendmany-amounts--key": "Address to pay", + "sendmany-amounts--value": "Amount to send to the payment address valued in bitcoin", + "sendmany-minconf": "Minimum number of block confirmations required before a transaction output is eligible to be spent", + "sendmany-comment": "Unused", + "sendmany--result0": "The transaction hash of the sent transaction", + // SendToAddressCmd help. + "sendtoaddress--synopsis": "Authors, signs, and sends a transaction that outputs some amount to a payment address.\n" + + "Unlike sendfrom, outputs are always chosen from the default account.\n" + + "A change output is automatically included to send extra output value back to the original account.", + "sendtoaddress-address": "Address to pay", + "sendtoaddress-amount": "Amount to send to the payment address valued in bitcoin", + "sendtoaddress-comment": "Unused", + "sendtoaddress-commentto": "Unused", + "sendtoaddress--result0": "The transaction hash of the sent transaction", + // SetTxFeeCmd help. + "settxfee--synopsis": "Modify the increment used each time more fee is required for an authored transaction.", + "settxfee-amount": "The new fee increment valued in bitcoin", + "settxfee--result0": "The boolean 'true'", + // SignMessageCmd help. + "signmessage--synopsis": "Signs a message using the private key of a payment address.", + "signmessage-address": "Payment address of private key used to sign the message with", + "signmessage-message": "Message to sign", + "signmessage--result0": "The signed message encoded as a base64 string", + // SignRawTransactionCmd help. + "signrawtransaction--synopsis": "Signs transaction inputs using private keys from this wallet and request.\n" + + "The valid flags options are ALL, NONE, SINGLE, ALL|ANYONECANPAY, NONE|ANYONECANPAY, and SINGLE|ANYONECANPAY.", + "signrawtransaction-rawtx": "Unsigned or partially unsigned transaction to sign encoded as a hexadecimal string", + "signrawtransaction-inputs": "Additional data regarding inputs that this wallet may not be tracking", + "signrawtransaction-privkeys": "Additional WIF-encoded private keys to use when creating signatures", + "signrawtransaction-flags": "Sighash flags", + // SignRawTransactionResult help. + "signrawtransactionresult-hex": "The resulting transaction encoded as a hexadecimal string", + "signrawtransactionresult-complete": "Whether all input signatures have been created", + "signrawtransactionresult-errors": "Script verification errors (if exists)", + // SignRawTransactionError help. + "signrawtransactionerror-error": "Verification or signing error related to the input", + "signrawtransactionerror-sequence": "Script sequence number", + "signrawtransactionerror-scriptSig": "The hex-encoded signature script", + "signrawtransactionerror-txid": "The transaction hash of the referenced previous output", + "signrawtransactionerror-vout": "The output index of the referenced previous output", + // ValidateAddressCmd help. + "validateaddress--synopsis": "Verify that an address is valid.\n" + + "Extra details are returned if the address is controlled by this wallet.\n" + + "The following fields are valid only when the address is controlled by this wallet (ismine=true): isscript, pubkey, iscompressed, account, addresses, hex, script, and sigsrequired.\n" + + "The following fields are only valid when address has an associated public key: pubkey, iscompressed.\n" + + "The following fields are only valid when address is a pay-to-script-hash address: addresses, hex, and script.\n" + + "If the address is a multisig address controlled by this wallet, the multisig fields will be left unset if the wallet is locked since the redeem script cannot be decrypted.", + "validateaddress-address": "Address to validate", + // ValidateAddressWalletResult help. + "validateaddresswalletresult-isvalid": "Whether or not the address is valid", + "validateaddresswalletresult-address": "The payment address (only when isvalid is true)", + "validateaddresswalletresult-ismine": "Whether this address is controlled by the wallet (only when isvalid is true)", + "validateaddresswalletresult-iswatchonly": "Unset", + "validateaddresswalletresult-isscript": "Whether the payment address is a pay-to-script-hash address (only when isvalid is true)", + "validateaddresswalletresult-pubkey": "The associated public key of the payment address, if any (only when isvalid is true)", + "validateaddresswalletresult-iscompressed": "Whether the address was created by hashing a compressed public key, if any (only when isvalid is true)", + "validateaddresswalletresult-account": "The account this payment address belongs to (only when isvalid is true)", + "validateaddresswalletresult-addresses": "All associated payment addresses of the script if address is a multisig address (only when isvalid is true)", + "validateaddresswalletresult-hex": "The redeem script ", + "validateaddresswalletresult-script": "The class of redeem script for a multisig address", + "validateaddresswalletresult-sigsrequired": "The number of required signatures to redeem outputs to the multisig address", + // VerifyMessageCmd help. + "verifymessage--synopsis": "Verify a message was signed with the associated private key of some address.", + "verifymessage-address": "Address used to sign message", + "verifymessage-signature": "The signature to verify", + "verifymessage-message": "The message to verify", + "verifymessage--result0": "Whether the message was signed with the private key of 'address'", + // WalletLockCmd help. + "walletlock--synopsis": "Lock the wallet.", + // WalletPassphraseCmd help. + "walletpassphrase--synopsis": "Unlock the wallet.", + "walletpassphrase-passphrase": "The wallet passphrase", + "walletpassphrase-timeout": "The number of seconds to wait before the wallet automatically locks", + // WalletPassphraseChangeCmd help. + "walletpassphrasechange--synopsis": "Change the wallet passphrase.", + "walletpassphrasechange-oldpassphrase": "The old wallet passphrase", + "walletpassphrasechange-newpassphrase": "The new wallet passphrase", + // CreateNewAccountCmd help. + "createnewaccount--synopsis": "Creates a new account.\n" + + "The wallet must be unlocked for this request to succeed.", + "createnewaccount-account": "Name of the new account", + // ExportWatchingWalletCmd help. + "exportwatchingwallet--synopsis": "Creates and returns a duplicate of the wallet database without any private keys to be used as a watching-only wallet.", + "exportwatchingwallet-account": "Unused (must be unset or \"*\")", + "exportwatchingwallet-download": "Unused", + "exportwatchingwallet--result0": "The watching-only database encoded as a base64 string", + // GetBestBlockCmd help. + "getbestblock--synopsis": "Returns the hash and height of the newest block in the best chain that wallet has finished syncing with.", + // GetBestBlockResult help. + "getbestblockresult-hash": "The hash of the block", + "getbestblockresult-height": "The blockchain height of the block", + // GetUnconfirmedBalanceCmd help. + "getunconfirmedbalance--synopsis": "Calculates the unspent output value of all unmined transaction outputs for an account.", + "getunconfirmedbalance-account": "The account to query the unconfirmed balance for (default=\"default\")", + "getunconfirmedbalance--result0": "Total amount of all unmined unspent outputs of the account valued in bitcoin.", + // ListAddressTransactionsCmd help. + "listaddresstransactions--synopsis": "Returns a JSON array of objects containing verbose details for wallet transactions pertaining some addresses.", + "listaddresstransactions-addresses": "Addresses to filter transaction results by", + "listaddresstransactions-account": "Unused (must be unset or \"*\")", + // ListAllTransactionsCmd help. + "listalltransactions--synopsis": "Returns a JSON array of objects in the same format as 'listtransactions' without limiting the number of returned objects.", + "listalltransactions-account": "Unused (must be unset or \"*\")", + // RenameAccountCmd help. + "renameaccount--synopsis": "Renames an account.", + "renameaccount-oldaccount": "The old account name to rename", + "renameaccount-newaccount": "The new name for the account", + // WalletIsLockedCmd help. + "walletislocked--synopsis": "Returns whether or not the wallet is locked.", + "walletislocked--result0": "Whether the wallet is locked", +} diff --git a/pkg/rpchelp/log.go b/pkg/rpchelp/log.go new file mode 100644 index 0000000..d5035d1 --- /dev/null +++ b/pkg/rpchelp/log.go @@ -0,0 +1,43 @@ +package rpchelp + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/rpchelp/methods.go b/pkg/rpchelp/methods.go new file mode 100644 index 0000000..fc76a0e --- /dev/null +++ b/pkg/rpchelp/methods.go @@ -0,0 +1,83 @@ +// +build !generate + +package rpchelp + +import ( + "github.com/p9c/p9/pkg/btcjson" +) + +// HelpDescs contains the locale-specific help strings along with the locale. +var HelpDescs = []struct { + Locale string // Actual locale, e.g. en_US + GoLocale string // Locale used in Go names, e.g. EnUS + Descs map[string]string +}{ + {"en_US", "EnUS", helpDescsEnUS}, // helpdescs_en_US.go +} + +// Methods contains all methods and result types that help is generated for, for every locale. +var Methods = []struct { + Method string + ResultTypes []interface{} +}{ + {"addmultisigaddress", returnsString}, + {"createmultisig", []interface{}{(*btcjson.CreateMultiSigResult)(nil)}}, + {"dumpprivkey", returnsString}, + {"getaccount", returnsString}, + {"getaccountaddress", returnsString}, + {"getaddressesbyaccount", returnsStringArray}, + {"getbalance", append(returnsNumber, returnsNumber[0])}, + {"getbestblockhash", returnsString}, + {"getblockcount", returnsNumber}, + {"getinfo", []interface{}{(*btcjson.InfoWalletResult)(nil)}}, + {"getnewaddress", returnsString}, + {"getrawchangeaddress", returnsString}, + {"getreceivedbyaccount", returnsNumber}, + {"getreceivedbyaddress", returnsNumber}, + {"gettransaction", []interface{}{(*btcjson.GetTransactionResult)(nil)}}, + {"help", append(returnsString, returnsString[0])}, + {"importprivkey", nil}, + {"keypoolrefill", nil}, + {"listaccounts", []interface{}{(*map[string]float64)(nil)}}, + {"listlockunspent", []interface{}{(*[]btcjson.TransactionInput)(nil)}}, + {"listreceivedbyaccount", []interface{}{(*[]btcjson.ListReceivedByAccountResult)(nil)}}, + {"listreceivedbyaddress", []interface{}{(*[]btcjson.ListReceivedByAddressResult)(nil)}}, + {"listsinceblock", []interface{}{(*btcjson.ListSinceBlockResult)(nil)}}, + {"listtransactions", returnsLTRArray}, + {"listunspent", []interface{}{(*btcjson.ListUnspentResult)(nil)}}, + {"lockunspent", returnsBool}, + {"sendfrom", returnsString}, + {"sendmany", returnsString}, + {"sendtoaddress", returnsString}, + {"settxfee", returnsBool}, + {"signmessage", returnsString}, + {"signrawtransaction", []interface{}{(*btcjson.SignRawTransactionResult)(nil)}}, + {"validateaddress", []interface{}{(*btcjson.ValidateAddressWalletResult)(nil)}}, + {"verifymessage", returnsBool}, + {"walletlock", nil}, + {"walletpassphrase", nil}, + {"walletpassphrasechange", nil}, + {"createnewaccount", nil}, + {"exportwatchingwallet", returnsString}, + {"getbestblock", []interface{}{(*btcjson.GetBestBlockResult)(nil)}}, + {"getunconfirmedbalance", returnsNumber}, + {"listaddresstransactions", returnsLTRArray}, + {"listalltransactions", returnsLTRArray}, + {"renameaccount", nil}, + {"walletislocked", returnsBool}, +} + +// Common return types. +var returnsBool = []interface{}{(*bool)(nil)} + +// Common return types. +var returnsLTRArray = []interface{}{(*[]btcjson.ListTransactionsResult)(nil)} + +// Common return types. +var returnsNumber = []interface{}{(*float64)(nil)} + +// Common return types. +var returnsString = []interface{}{(*string)(nil)} + +// Common return types. +var returnsStringArray = []interface{}{(*[]string)(nil)} diff --git a/pkg/snacl/log.go b/pkg/snacl/log.go new file mode 100644 index 0000000..2bd5666 --- /dev/null +++ b/pkg/snacl/log.go @@ -0,0 +1,43 @@ +package snacl + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/snacl/snacl.go b/pkg/snacl/snacl.go new file mode 100644 index 0000000..fdaef81 --- /dev/null +++ b/pkg/snacl/snacl.go @@ -0,0 +1,232 @@ +package snacl + +import ( + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "errors" + "io" + "runtime/debug" + + "github.com/btcsuite/golangcrypto/nacl/secretbox" + "github.com/btcsuite/golangcrypto/scrypt" + + "github.com/p9c/p9/pkg/util/zero" +) + +var ( + prng = rand.Reader + + // ErrInvalidPassword ... + ErrInvalidPassword = errors.New("invalid password") + // ErrMalformed ... + ErrMalformed = errors.New("malformed data") + // ErrDecryptFailed ... + ErrDecryptFailed = errors.New("unable to decrypt") +) + +// Various constants needed for encryption scheme. +const ( + // Overhead const here expose secretbox's for convenience. + Overhead = secretbox.Overhead + keySize = 32 + NonceSize = 24 + DefaultN = 16384 // 2^14 + DefaultR = 8 + DefaultP = 1 +) + +// CryptoKey represents a secret key which can be used to encrypt and decrypt data. +type CryptoKey [keySize]byte + +// Encrypt encrypts the passed data. +func (ck *CryptoKey) Encrypt(in []byte) ([]byte, error) { + var nonce [NonceSize]byte + _, e := io.ReadFull(prng, nonce[:]) + if e != nil { + E.Ln(e) + return nil, e + } + blob := secretbox.Seal(nil, in, &nonce, (*[keySize]byte)(ck)) + return append(nonce[:], blob...), nil +} + +// Decrypt decrypts the passed data. The must be the output of the Encrypt function. +func (ck *CryptoKey) Decrypt(in []byte) ([]byte, error) { + if len(in) < NonceSize { + return nil, ErrMalformed + } + var nonce [NonceSize]byte + copy(nonce[:], in[:NonceSize]) + blob := in[NonceSize:] + opened, ok := secretbox.Open(nil, blob, &nonce, (*[keySize]byte)(ck)) + if !ok { + return nil, ErrDecryptFailed + } + return opened, nil +} + +// Zero clears the key by manually zeroing all memory. This is for security conscience application which wish to zero +// the memory after they've used it rather than waiting until it's reclaimed by the garbage collector. The key is no +// longer usable after this call. +func (ck *CryptoKey) Zero() { + zero.Bytea32((*[keySize]byte)(ck)) +} + +// GenerateCryptoKey generates a new crypotgraphically random key. +func GenerateCryptoKey() (*CryptoKey, error) { + var key CryptoKey + _, e := io.ReadFull(prng, key[:]) + if e != nil { + E.Ln(e) + return nil, e + } + return &key, nil +} + +// Parameters are not secret and can be stored in plain text. +type Parameters struct { + Salt [keySize]byte + Digest [sha256.Size]byte + N int + R int + P int +} + +// SecretKey houses a crypto key and the parameters needed to derive it from a passphrase. It should only be used in +// memory. +type SecretKey struct { + Key *CryptoKey + Parameters Parameters +} + +// deriveKey fills out the Key field. +func (sk *SecretKey) deriveKey(password *[]byte) (e error) { + key, e := scrypt.Key(*password, sk.Parameters.Salt[:], + sk.Parameters.N, + sk.Parameters.R, + sk.Parameters.P, + len(sk.Key), + ) + if e != nil { + E.Ln(e) + return e + } + copy(sk.Key[:], key) + zero.Bytes(key) + // I'm not a fan of forced garbage collections, but scrypt allocates a ton of memory and calling it back to back + // without a GC cycle in between means you end up needing twice the amount of memory. + // + // For example, if your scrypt parameters are such that you require 1GB and you call it twice in a row, without this + // you end up allocating 2GB since the first GB probably hasn't been released yet. + debug.FreeOSMemory() + return nil +} + +// Marshal returns the Parameters field marshalled into a format suitable for storage. +// +// This result of this can be stored in clear text. +func (sk *SecretKey) Marshal() []byte { + params := &sk.Parameters + // The marshalled format for the the netparams is as follows: + // + //

+ // + // KeySize + sha256.Size + N (8 bytes) + R (8 bytes) + P (8 bytes) + marshalled := make([]byte, keySize+sha256.Size+24) + b := marshalled + copy(b[:keySize], params.Salt[:]) + b = b[keySize:] + copy(b[:sha256.Size], params.Digest[:]) + b = b[sha256.Size:] + binary.LittleEndian.PutUint64(b[:8], uint64(params.N)) + b = b[8:] + binary.LittleEndian.PutUint64(b[:8], uint64(params.R)) + b = b[8:] + binary.LittleEndian.PutUint64(b[:8], uint64(params.P)) + return marshalled +} + +// Unmarshal unmarshalls the parameters needed to derive the secret key from a passphrase into sk. +func (sk *SecretKey) Unmarshal(marshalled []byte) (e error) { + if sk.Key == nil { + sk.Key = (*CryptoKey)(&[keySize]byte{}) + } + // The marshalled format for the the netparams is as follows: + // + //

+ // + // KeySize + sha256.Size + N (8 bytes) + R (8 bytes) + P (8 bytes) + if len(marshalled) != keySize+sha256.Size+24 { + return ErrMalformed + } + params := &sk.Parameters + copy(params.Salt[:], marshalled[:keySize]) + marshalled = marshalled[keySize:] + copy(params.Digest[:], marshalled[:sha256.Size]) + marshalled = marshalled[sha256.Size:] + params.N = int(binary.LittleEndian.Uint64(marshalled[:8])) + marshalled = marshalled[8:] + params.R = int(binary.LittleEndian.Uint64(marshalled[:8])) + marshalled = marshalled[8:] + params.P = int(binary.LittleEndian.Uint64(marshalled[:8])) + return nil +} + +// Zero zeroes the underlying secret key while leaving the parameters intact. +// +// This effectively makes the key unusable until it is derived again via the DeriveKey function. +func (sk *SecretKey) Zero() { + sk.Key.Zero() +} + +// DeriveKey derives the underlying secret key and ensures it matches the expected digest. +// +// This should only be called after previously calling the Zero function or on an initial Unmarshal. +func (sk *SecretKey) DeriveKey(password *[]byte) (e error) { + if e = sk.deriveKey(password); E.Chk(e) { + return + } + // verify password + digest := sha256.Sum256(sk.Key[:]) + if subtle.ConstantTimeCompare(digest[:], sk.Parameters.Digest[:]) != 1 { + return ErrInvalidPassword + } + return nil +} + +// Encrypt encrypts in bytes and returns a JSON blob. +func (sk *SecretKey) Encrypt(in []byte) ([]byte, error) { + return sk.Key.Encrypt(in) +} + +// Decrypt takes in a JSON blob and returns it's decrypted form. +func (sk *SecretKey) Decrypt(in []byte) ([]byte, error) { + return sk.Key.Decrypt(in) +} + +// NewSecretKey returns a SecretKey structure based on the passed parameters. +func NewSecretKey(password *[]byte, N, r, p int) (sk *SecretKey, e error) { + sk = &SecretKey{ + Key: (*CryptoKey)(&[keySize]byte{}), + } + // setup parameters + sk.Parameters.N = N + sk.Parameters.R = r + sk.Parameters.P = p + _, e = io.ReadFull(prng, sk.Parameters.Salt[:]) + if e != nil { + E.Ln(e) + return nil, e + } + // derive key + e = sk.deriveKey(password) + if e != nil { + E.Ln(e) + return nil, e + } + // store digest + sk.Parameters.Digest = sha256.Sum256(sk.Key[:]) + return sk, nil +} diff --git a/pkg/snacl/snacl_test.go b/pkg/snacl/snacl_test.go new file mode 100644 index 0000000..46f1690 --- /dev/null +++ b/pkg/snacl/snacl_test.go @@ -0,0 +1,94 @@ +package snacl + +import ( + "bytes" + "testing" +) + +var ( + password = []byte("sikrit") + message = []byte("this is a secret message of sorts") + key *SecretKey + params []byte + blob []byte +) + +func TestNewSecretKey(t *testing.T) { + var e error + key, e = NewSecretKey(&password, DefaultN, DefaultR, DefaultP) + if e != nil { + return + } +} +func TestMarshalSecretKey(t *testing.T) { + params = key.Marshal() +} +func TestUnmarshalSecretKey(t *testing.T) { + var sk SecretKey + if e := sk.Unmarshal(params); E.Chk(e) { + t.Errorf("unexpected unmarshal error: %v", e) + return + } + if e := sk.DeriveKey(&password); E.Chk(e) { + t.Errorf("unexpected DeriveKey error: %v", e) + return + } + if !bytes.Equal(sk.Key[:], key.Key[:]) { + t.Errorf("keys not equal") + } +} +func TestUnmarshalSecretKeyInvalid(t *testing.T) { + var sk SecretKey + if e := sk.Unmarshal(params); E.Chk(e) { + t.Errorf("unexpected unmarshal error: %v", e) + return + } + p := []byte("wrong password") + if e := sk.DeriveKey(&p); e != ErrInvalidPassword { + t.Errorf("wrong password didn't fail") + return + } +} +func TestEncrypt(t *testing.T) { + var e error + blob, e = key.Encrypt(message) + if e != nil { + return + } +} +func TestDecrypt(t *testing.T) { + decryptedMessage, e := key.Decrypt(blob) + if e != nil { + return + } + if !bytes.Equal(decryptedMessage, message) { + t.Errorf("decryption failed") + return + } +} +func TestDecryptCorrupt(t *testing.T) { + blob[len(blob)-15] = blob[len(blob)-15] + 1 + _, e := key.Decrypt(blob) + if e == nil { + t.Errorf("corrupt message decrypted") + return + } +} +func TestZero(t *testing.T) { + var zeroKey [32]byte + key.Zero() + if !bytes.Equal(key.Key[:], zeroKey[:]) { + t.Errorf("zero key failed") + } +} +func TestDeriveKey(t *testing.T) { + if e := key.DeriveKey(&password); E.Chk(e) { + t.Errorf("unexpected DeriveKey key failure: %v", e) + } +} +func TestDeriveKeyInvalid(t *testing.T) { + bogusPass := []byte("bogus") + if e := key.DeriveKey(&bogusPass); e != ErrInvalidPassword { + t.Errorf("unexpected DeriveKey key failure: %v", e) + } +} diff --git a/pkg/transport/_reuseport.go b/pkg/transport/_reuseport.go new file mode 100644 index 0000000..7bf87e4 --- /dev/null +++ b/pkg/transport/_reuseport.go @@ -0,0 +1,16 @@ +// +build !windows + +package transport + +import ( + "syscall" +) + +func reusePort(network, address string, conn syscall.RawConn) (e error) { + return conn.Control(func(descriptor uintptr) { + e := syscall.SetsockoptInt(int(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if e != nil { + } + }, + ) +} diff --git a/pkg/transport/_reuseport_windows.go b/pkg/transport/_reuseport_windows.go new file mode 100644 index 0000000..797f5d8 --- /dev/null +++ b/pkg/transport/_reuseport_windows.go @@ -0,0 +1,16 @@ +// +build windows + +package transport + +import ( + "syscall" +) + +func reusePort(network, address string, conn syscall.RawConn) (e error) { + return conn.Control(func(descriptor uintptr) { + e := syscall.SetsockoptInt(syscall.Handle(descriptor), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if e != nil { + } + }, + ) +} diff --git a/pkg/transport/bufiter.go b/pkg/transport/bufiter.go new file mode 100644 index 0000000..aec68dd --- /dev/null +++ b/pkg/transport/bufiter.go @@ -0,0 +1,45 @@ +package transport + +// +// type ( +// Buf [][]byte +// BufIter struct { +// Buf +// cursor int +// } +// ) +// +// // NewBufIter returns a new BufIter loaded with a given slice of Buf +// func NewBufIter(buf Buf) *BufIter { +// return &BufIter{Buf: buf} +// } +// +// // Len returns the length of the buffer +// func (b *BufIter) Len() int { +// return len(b.Buf) +// } +// +// // Get returns the currently selected Buf +// func (b *BufIter) Get() []byte { +// return b.Buf[b.cursor] +// } +// +// // More returns true if the cursor is not at the end +// func (b *BufIter) More() bool { +// return b.cursor < len(b.Buf) +// } +// +// // Reset sets a buf iter to zero, not necessary to use first time iterating +// func (b *BufIter) Reset() { +// b.cursor = 0 +// } +// +// // Next returns the next item in a buffer +// func (b *BufIter) Next() (buf []byte) { +// if b.cursor > len(b.Buf) { +// } else { +// buf = b.Buf[b.cursor] +// b.cursor++ +// } +// return +// } diff --git a/pkg/transport/channels.go b/pkg/transport/channels.go new file mode 100644 index 0000000..ad6b950 --- /dev/null +++ b/pkg/transport/channels.go @@ -0,0 +1,374 @@ +package transport + +import ( + "crypto/cipher" + "errors" + "fmt" + "net" + "runtime" + "strings" + "time" + + "github.com/p9c/p9/pkg/log" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/fec" + "github.com/p9c/p9/pkg/gcm" + "github.com/p9c/p9/pkg/multicast" +) + +const ( + UDPMulticastAddress = "224.0.0.1" + success int = iota // this is implicit zero of an int but starts the iota + closed + other + DefaultPort = 11049 +) + +var DefaultIP = net.IPv4(224, 0, 0, 1) +var MulticastAddress = &net.UDPAddr{IP: DefaultIP, Port: DefaultPort} + +type ( + MsgBuffer struct { + Buffers [][]byte + First time.Time + Decoded bool + Source net.Addr + } + // HandlerFunc is a function that is used to process a received message + HandlerFunc func( + ctx interface{}, src net.Addr, dst string, b []byte, + ) (e error) + Handlers map[string]HandlerFunc + Channel struct { + buffers map[string]*MsgBuffer + Ready qu.C + context interface{} + Creator string + firstSender *string + lastSent *time.Time + MaxDatagramSize int + receiveCiph cipher.AEAD + Receiver *net.UDPConn + sendCiph cipher.AEAD + Sender *net.UDPConn + } +) + +// SetDestination changes the address the outbound connection of a multicast +// directs to +func (c *Channel) SetDestination(dst string) (e error) { + D.Ln("sending to", dst) + if c.Sender, e = NewSender(dst, c.MaxDatagramSize); E.Chk(e) { + } + return +} + +// Send fires off some data through the configured multicast's outbound. +func (c *Channel) Send(magic []byte, nonce []byte, data []byte) ( + n int, e error, +) { + if len(data) == 0 { + e = errors.New("not sending empty packet") + E.Ln(e) + return + } + var msg []byte + if msg, e = EncryptMessage(c.Creator, c.sendCiph, magic, nonce, data); E.Chk(e) { + } + n, e = c.Sender.Write(msg) + // D.Ln(msg) + return +} + +// SendMany sends a BufIter of shards as produced by GetShards +func (c *Channel) SendMany(magic []byte, b [][]byte) (e error) { + D.Ln("magic", string(magic), log.Caller("sending from", 1)) + var nonce []byte + if nonce, e = GetNonce(c.sendCiph); E.Chk(e) { + } else { + for i := 0; i < len(b); i++ { + // D.Ln(i) + // D.Ln("segment length", len(b[i])) + if _, e = c.Send(magic, nonce, b[i]); E.Chk(e) { + // debug.PrintStack() + } + } + // T.Ln(c.Creator, "sent packets", string(magic), hex.EncodeToString(nonce), c.Sender.LocalAddr(), c.Sender.RemoteAddr()) + } + return +} + +// Close the multicast +func (c *Channel) Close() (e error) { + // if e = c.Sender.Close(); E.Chk(e) { + // } + // if e = c.Receiver.Close(); E.Chk(e) { + // } + return +} + +// GetShards returns a buffer iterator to feed to Channel.SendMany containing +// fec encoded shards built from the provided buffer +func GetShards(data []byte) (shards [][]byte) { + var e error + if shards, e = fec.Encode(data); E.Chk(e) { + } + return +} + +// NewUnicastChannel sets up a listener and sender for a specified destination +func NewUnicastChannel( + creator string, ctx interface{}, key []byte, sender, receiver string, + maxDatagramSize int, + handlers Handlers, quit qu.C, +) (channel *Channel, e error) { + channel = &Channel{ + Creator: creator, + MaxDatagramSize: maxDatagramSize, + buffers: make(map[string]*MsgBuffer), + context: ctx, + } + var magics []string + bytes := make([]byte, len(key)) + copy(bytes, key) + for i := range handlers { + magics = append(magics, i) + } + if channel.sendCiph, e = gcm.GetCipher(bytes); E.Chk(e) { + } + copy(bytes, key) + if channel.receiveCiph, e = gcm.GetCipher(bytes); E.Chk(e) { + } + for i := range bytes { + bytes[i] = 0 + key[i] = 0 + } + if channel.Receiver, e = Listen(receiver, channel, maxDatagramSize, handlers, quit); E.Chk(e) { + } + if channel.Sender, e = NewSender(sender, maxDatagramSize); E.Chk(e) { + } + D.Ln("starting unicast multicast:", channel.Creator, sender, receiver, magics) + return +} + +// NewSender creates a new UDP connection to a specified address +func NewSender(address string, maxDatagramSize int) ( + conn *net.UDPConn, e error, +) { + var addr *net.UDPAddr + if addr, e = net.ResolveUDPAddr("udp4", address); E.Chk(e) { + return + } else if conn, e = net.DialUDP("udp4", nil, addr); E.Chk(e) { + // debug.PrintStack() + return + } + D.Ln("started new sender on", conn.LocalAddr(), "->", conn.RemoteAddr()) + if e = conn.SetWriteBuffer(maxDatagramSize); E.Chk(e) { + } + return +} + +// Listen binds to the UDP Address and port given and writes packets received +// from that Address to a buffer which is passed to a handler +func Listen( + address string, channel *Channel, maxDatagramSize int, handlers Handlers, + quit qu.C, +) (conn *net.UDPConn, e error) { + var addr *net.UDPAddr + if addr, e = net.ResolveUDPAddr("udp4", address); E.Chk(e) { + return + } else if conn, e = net.ListenUDP("udp4", addr); E.Chk(e) { + return + } else if conn == nil { + return nil, errors.New("unable to start connection ") + } + D.Ln("starting listener on", conn.LocalAddr(), "->", conn.RemoteAddr()) + if e = conn.SetReadBuffer(maxDatagramSize); E.Chk(e) { + // not a critical error but should not happen + } + go Handle(address, channel, handlers, maxDatagramSize, quit) + return +} + +// NewBroadcastChannel returns a broadcaster and listener with a given handler +// on a multicast address and specified port. The handlers define the messages +// that will be processed and any other messages are ignored +func NewBroadcastChannel( + creator string, ctx interface{}, key []byte, port int, maxDatagramSize int, + handlers Handlers, + quit qu.C, +) (channel *Channel, e error) { + channel = &Channel{ + Creator: creator, + MaxDatagramSize: maxDatagramSize, + buffers: make(map[string]*MsgBuffer), + context: ctx, + Ready: qu.T(), + } + bytes := make([]byte, len(key)) + copy(bytes, key) + if channel.sendCiph, e = gcm.GetCipher(bytes); E.Chk(e) { + panic(e) + } + copy(bytes, key) + if channel.receiveCiph, e = gcm.GetCipher(bytes); E.Chk(e) { + panic(e) + } + for i := range bytes { + key[i] = 0 + bytes[i] = 0 + } + if channel.Receiver, e = ListenBroadcast(port, channel, maxDatagramSize, handlers, quit); E.Chk(e) { + } + if channel.Sender, e = NewBroadcaster(port, maxDatagramSize); E.Chk(e) { + } + channel.Ready.Q() + return +} + +// NewBroadcaster creates a new UDP multicast connection on which to broadcast +func NewBroadcaster(port int, maxDatagramSize int) ( + conn *net.UDPConn, e error, +) { + address := net.JoinHostPort(UDPMulticastAddress, fmt.Sprint(port)) + if conn, e = NewSender(address, maxDatagramSize); E.Chk(e) { + } + return +} + +// ListenBroadcast binds to the UDP Address and port given and writes packets +// received from that Address to a buffer which is passed to a handler +func ListenBroadcast( + port int, + channel *Channel, + maxDatagramSize int, + handlers Handlers, + quit qu.C, +) (conn *net.UDPConn, e error) { + if conn, e = multicast.Conn(port); E.Chk(e) { + return + } + address := conn.LocalAddr().String() + var magics []string + for i := range handlers { + magics = append(magics, i) + } + // D.S(handlers) + // D.Ln("magics", magics, PrevCallers()) + D.Ln("starting broadcast listener", channel.Creator, address, magics) + if e = conn.SetReadBuffer(maxDatagramSize); E.Chk(e) { + } + channel.Receiver = conn + go Handle(address, channel, handlers, maxDatagramSize, quit) + return +} + +func handleNetworkError(address string, e error) (result int) { + if len(strings.Split(e.Error(), "use of closed network connection")) >= 2 { + D.Ln("connection closed", address) + result = closed + } else { + E.F("ReadFromUDP failed: '%s'", e) + result = other + } + return +} + +// Handle listens for messages, decodes them, aggregates them, recovers the data +// from the reed solomon fec shards received and invokes the handler provided +// matching the magic on the complete received messages +func Handle( + address string, channel *Channel, + handlers Handlers, maxDatagramSize int, quit qu.C, +) { + buffer := make([]byte, maxDatagramSize) + T.Ln("starting handler for", channel.Creator, "listener") + // Loop forever reading from the socket until it is closed + // seenNonce := "" + var e error + var numBytes int + var src net.Addr + // var seenNonce string + <-channel.Ready +out: + for { + select { + case <-quit.Wait(): + break out + default: + } + if numBytes, src, e = channel.Receiver.ReadFromUDP(buffer); E.Chk(e) { + switch handleNetworkError(address, e) { + case closed: + break out + case other: + continue + case success: + } + } + // Filter messages by magic, if there is no match in the map the packet is + // ignored + magic := string(buffer[:4]) + if handler, ok := handlers[magic]; ok { + if channel.lastSent != nil && channel.firstSender != nil { + *channel.lastSent = time.Now() + } + msg := buffer[:numBytes] + nL := channel.receiveCiph.NonceSize() + nonceBytes := msg[4 : 4+nL] + nonce := string(nonceBytes) + var shard []byte + if shard, e = channel.receiveCiph.Open(nil, nonceBytes, msg[4+len(nonceBytes):], nil); e != nil { + continue + } + // D.Ln("read", numBytes, "from", src, e, hex.EncodeToString(msg)) + if bn, ok := channel.buffers[nonce]; ok { + if !bn.Decoded { + bn.Buffers = append(bn.Buffers, shard) + if len(bn.Buffers) >= 3 { + // try to decode it + var cipherText []byte + if cipherText, e = fec.Decode(bn.Buffers); E.Chk(e) { + continue + } + // D.F("received packet with magic %s from %s len %d bytes", magic, src.String(), len(cipherText)) + bn.Decoded = true + if e = handler(channel.context, src, address, cipherText); E.Chk(e) { + continue + } + // src = nil + // buffer = buffer[:0] + } + } else { + for i := range channel.buffers { + if i != nonce && channel.buffers[i].Decoded { + // superseded messages can be deleted from the buffers, we don't add more data + // for the already decoded. todo: this will be changed to track stats for the + // puncture rate and redundancy scaling + delete(channel.buffers, i) + } + } + } + } else { + channel.buffers[nonce] = &MsgBuffer{ + [][]byte{}, + time.Now(), false, src, + } + channel.buffers[nonce].Buffers = append( + channel.buffers[nonce]. + Buffers, shard, + ) + } + } + } +} + +func PrevCallers() (out string) { + for i := 0; i < 10; i++ { + _, loc, iline, _ := runtime.Caller(i) + out += fmt.Sprintf("%s:%d \n", loc, iline) + } + return +} diff --git a/pkg/transport/cmd/listenservemulticast/logmain.go b/pkg/transport/cmd/listenservemulticast/logmain.go new file mode 100644 index 0000000..ad256c8 --- /dev/null +++ b/pkg/transport/cmd/listenservemulticast/logmain.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/transport/cmd/listenservemulticast/main.go b/pkg/transport/cmd/listenservemulticast/main.go new file mode 100644 index 0000000..14ac38f --- /dev/null +++ b/pkg/transport/cmd/listenservemulticast/main.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "github.com/p9c/p9/pkg/log" + "net" + "time" + + "github.com/p9c/p9/pkg/qu" + + "github.com/p9c/p9/pkg/transport" + "github.com/p9c/p9/pkg/util/loop" +) + +const ( + TestMagic = "TEST" +) + +var ( + TestMagicB = []byte(TestMagic) +) + +func main() { + log.SetLogLevel("trace") + D.Ln("starting test") + quit := qu.T() + var c *transport.Channel + var e error + if c, e = transport.NewBroadcastChannel( + "test", nil, []byte("cipher"), + 1234, 8192, transport.Handlers{ + TestMagic: func( + ctx interface{}, src net.Addr, dst string, + b []byte, + ) (e error) { + I.F("%s <- %s [%d] '%s'", src.String(), dst, len(b), string(b)) + return + }, + }, + quit, + ); E.Chk(e) { + panic(e) + } + time.Sleep(time.Second) + var n int + loop.To( + 10, func(i int) { + text := []byte(fmt.Sprintf("this is a test %d", i)) + I.F("%s -> %s [%d] '%s'", c.Sender.LocalAddr(), c.Sender.RemoteAddr(), n-4, text) + if e = c.SendMany(TestMagicB, transport.GetShards(text)); E.Chk(e) { + } else { + } + }, + ) + time.Sleep(time.Second * 5) + if e = c.Close(); !E.Chk(e) { + time.Sleep(time.Second * 1) + } + quit.Q() +} diff --git a/pkg/transport/cmd/multicast/logmain.go b/pkg/transport/cmd/multicast/logmain.go new file mode 100644 index 0000000..ad256c8 --- /dev/null +++ b/pkg/transport/cmd/multicast/logmain.go @@ -0,0 +1,43 @@ +package main + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/transport/cmd/multicast/main.go b/pkg/transport/cmd/multicast/main.go new file mode 100644 index 0000000..89f62c3 --- /dev/null +++ b/pkg/transport/cmd/multicast/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "net" + "time" + + "golang.org/x/net/ipv4" +) + +var ipv4Addr = &net.UDPAddr{IP: net.IPv4(224, 0, 0, 1), Port: 1234} + +func main() { + conn, e := net.ListenUDP("udp4", ipv4Addr) + if e != nil { + fmt.Printf("ListenUDP error %v\n", e) + return + } + + pc := ipv4.NewPacketConn(conn) + var ifaces []net.Interface + var iface net.Interface + if ifaces, e = net.Interfaces(); E.Chk(e) { + } + // This grabs the first physical interface with multicast that is up + for i := range ifaces { + if ifaces[i].Flags&net.FlagMulticast != 0 && + ifaces[i].Flags&net.FlagUp != 0 && + ifaces[i].HardwareAddr != nil { + iface = ifaces[i] + break + } + } + if e = pc.JoinGroup(&iface, &net.UDPAddr{IP: net.IPv4(224, 0, 0, 1)}); E.Chk(e) { + return + } + // test + var loop bool + if loop, e = pc.MulticastLoopback(); e == nil { + fmt.Printf("MulticastLoopback status:%v\n", loop) + if !loop { + if e = pc.SetMulticastLoopback(true); E.Chk(e) { + fmt.Printf("SetMulticastLoopback error:%v\n", e) + } + } + } + go func() { + for { + if _, e = conn.WriteTo([]byte("hello"), ipv4Addr); E.Chk(e) { + fmt.Printf("Write failed, %v\n", e) + } + time.Sleep(time.Second) + } + }() + + buf := make([]byte, 1024) + for { + if n, addr, e := conn.ReadFrom(buf); E.Chk(e) { + fmt.Printf("error %v", e) + } else { + fmt.Printf("recv %s from %v\n", string(buf[:n]), addr) + } + } + + // return +} + +// +// func main() { +// var ifs []net.Interface +// var e error +// if ifs, e = net.Interfaces(); E.Chk(e) { +// } +// D.S(ifs) +// var addrs []net.Addr +// var addr net.Addr +// for i := range ifs { +// if ifs[i].Flags&net.FlagUp != 0 && ifs[i].Flags&net.FlagMulticast != 0 { +// if addrs, e = ifs[i].MulticastAddrs(); E.Chk(e) { +// } +// for j := range addrs { +// if addrs[j].String() == Multicast { +// addr = addrs[j] +// break +// } +// } +// } +// } +// D.S(addr) +// } diff --git a/pkg/transport/crypto.go b/pkg/transport/crypto.go new file mode 100644 index 0000000..addbc93 --- /dev/null +++ b/pkg/transport/crypto.go @@ -0,0 +1,46 @@ +package transport + +import ( + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" +) + +// DecryptMessage attempts to decode the received message +func DecryptMessage(creator string, ciph cipher.AEAD, data []byte) (msg []byte, e error) { + nonceSize := ciph.NonceSize() + msg, e = ciph.Open(nil, data[:nonceSize], data[nonceSize:], nil) + if e != nil { + e = errors.New(fmt.Sprintf("%s %s", creator, e.Error())) + } else { + D.Ln("decrypted message", hex.EncodeToString(data[:nonceSize])) + } + return +} + +// EncryptMessage encrypts a message, if the nonce is given it uses that +// otherwise it generates a new one. If there is no cipher this just returns a +// message with the given magic prepended. +func EncryptMessage(creator string, ciph cipher.AEAD, magic []byte, nonce, data []byte) (msg []byte, e error) { + if ciph != nil { + if nonce == nil { + nonce, e = GetNonce(ciph) + } + msg = append(append(magic, nonce...), ciph.Seal(nil, nonce, data, nil)...) + } else { + msg = append(magic, data...) + } + return +} + +// GetNonce reads from a cryptographicallly secure random number source +func GetNonce(ciph cipher.AEAD) (nonce []byte, e error) { + // get a nonce for the packet, it is both message ID and salt + nonce = make([]byte, ciph.NonceSize()) + if _, e = io.ReadFull(rand.Reader, nonce); E.Chk(e) { + } + return +} diff --git a/pkg/transport/doc.go b/pkg/transport/doc.go new file mode 100644 index 0000000..b54debd --- /dev/null +++ b/pkg/transport/doc.go @@ -0,0 +1,3 @@ +// Package transport provides a listener and sender channel for unicast and multicast UDP IPv4 short message chat +// protocol with a pre shared key, forward error correction facilities with a nice friendly declaration syntax +package transport diff --git a/pkg/transport/log.go b/pkg/transport/log.go new file mode 100644 index 0000000..a580ddd --- /dev/null +++ b/pkg/transport/log.go @@ -0,0 +1,43 @@ +package transport + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/transport/transport.go b/pkg/transport/transport.go new file mode 100644 index 0000000..93d21c0 --- /dev/null +++ b/pkg/transport/transport.go @@ -0,0 +1,269 @@ +package transport + +import ( + "context" + "crypto/cipher" + "crypto/rand" + "errors" + "io" + "net" + "sync" + "time" + + "github.com/p9c/p9/pkg/fec" +) + +// HandleFunc is a map of handlers for working on received, decoded packets +type HandleFunc map[string]func(ctx interface{}) func(b []byte) (e error) + +// Connection is the state and working memory references for a simple reliable UDP lan transport, encrypted by a GCM AES +// cipher, with the simple protocol of sending out 9 packets containing encrypted FEC shards containing a slice of +// bytes. +// +// This protocol probably won't work well outside of a multicast lan in adverse conditions but it is designed for local +// network control systems todo: it is if the updated fec segmenting code is put in +type Connection struct { + maxDatagramSize int + buffers map[string]*MsgBuffer + sendAddress *net.UDPAddr + SendConn net.Conn + listenAddress *net.UDPAddr + listenConn *net.PacketConn + ciph cipher.AEAD + ctx context.Context + mx *sync.Mutex +} + +// +// // NewConnection creates a new connection with a defined default send +// // connection and listener and pre shared key password for encryption on the +// // local network +// func NewConnection(send, listen, preSharedKey string, +// maxDatagramSize int, ctx context.Context) (c *Connection, e error) { +// sendAddr := &net.UDPAddr{} +// var sendConn net.Conn +// listenAddr := &net.UDPAddr{} +// var listenConn net.PacketConn +// if listen != "" { +// config := &net.ListenConfig{Control: reusePort} +// listenConn, e = config.ListenPacket(context.Background(), "udp4", listen) +// if e != nil { +// E.Ln(e) +// } +// } +// if send != "" { +// // sendAddr, e = net.ResolveUDPAddr("udp4", send) +// // if e != nil { +// // E.Ln(e) +// // } +// sendConn, e = net.Dial("udp4", send) +// if e != nil { +// Error(err, sendAddr) +// } +// // L.Spew(sendConn) +// } +// var ciph cipher.AEAD +// if ciph, e = gcm.GetCipher(preSharedKey); E.Chk(e) { +// } +// return &Connection{ +// maxDatagramSize: maxDatagramSize, +// buffers: make(map[string]*MsgBuffer), +// sendAddress: sendAddr, +// SendConn: sendConn, +// listenAddress: listenAddr, +// listenConn: &listenConn, +// ciph: ciph, // gcm.GetCipher(*cx.Config.MinerPass), +// ctx: ctx, +// mx: &sync.Mutex{}, +// }, err +// } + +// SetSendConn sets up an outbound connection +func (c *Connection) SetSendConn(ad string) (e error) { + // c.sendAddress, e = net.ResolveUDPAddr("udp4", ad) + // if e != nil { + // // } + var sC net.Conn + if sC, e = net.Dial("udp4", ad); !E.Chk(e) { + c.SendConn = sC + } + return +} + +// CreateShards takes a slice of bites and generates 3 +func (c *Connection) CreateShards(b, magic []byte) ( + shards [][]byte, + e error, +) { + magicLen := 4 + // get a nonce for the packet, it is both message ID and salt + nonceLen := c.ciph.NonceSize() + nonce := make([]byte, nonceLen) + if _, e = io.ReadFull(rand.Reader, nonce); E.Chk(e) { + return + } + // generate the shards + if shards, e = fec.Encode(b); E.Chk(e) { + } + for i := range shards { + encryptedShard := c.ciph.Seal(nil, nonce, shards[i], nil) + shardLen := len(encryptedShard) + // assemble the packet: magic, nonce, and encrypted shard + outBytes := make([]byte, shardLen+magicLen+nonceLen) + copy(outBytes, magic[:magicLen]) + copy(outBytes[magicLen:], nonce) + copy(outBytes[magicLen+nonceLen:], encryptedShard) + shards[i] = outBytes + } + return +} + +func send(shards [][]byte, sendConn net.Conn) (e error) { + for i := range shards { + if _, e = sendConn.Write(shards[i]); E.Chk(e) { + } + } + return +} + +func (c *Connection) Send(b, magic []byte) (e error) { + if len(magic) != 4 { + e = errors.New("magic must be 4 bytes long") + return + } + var shards [][]byte + shards, e = c.CreateShards(b, magic) + if e = send(shards, c.SendConn); E.Chk(e) { + } + return +} + +func (c *Connection) SendTo(addr *net.UDPAddr, b, magic []byte) (e error) { + if len(magic) != 4 { + if e = errors.New("magic must be 4 bytes long"); E.Chk(e) { + return + } + } + var sendConn *net.UDPConn + if sendConn, e = net.DialUDP("udp", nil, addr); E.Chk(e) { + return + } + var shards [][]byte + if shards, e = c.CreateShards(b, magic); E.Chk(e) { + } + if e = send(shards, sendConn); E.Chk(e) { + } + return +} + +func (c *Connection) SendShards(shards [][]byte) (e error) { + if e = send(shards, c.SendConn); E.Chk(e) { + } + return +} + +func (c *Connection) SendShardsTo(shards [][]byte, addr *net.UDPAddr) (e error) { + var sendConn *net.UDPConn + if sendConn, e = net.DialUDP("udp", nil, addr); !E.Chk(e) { + if e = send(shards, sendConn); E.Chk(e) { + } + } + return +} + +// Listen runs a goroutine that collects and attempts to decode the FEC shards +// once it has enough intact pieces +func (c *Connection) Listen(handlers HandleFunc, ifc interface{}, lastSent *time.Time, firstSender *string,) (e error) { + F.Ln("setting read buffer") + buffer := make([]byte, c.maxDatagramSize) + go func() { + F.Ln("starting connection handler") + out: + // read from socket until context is cancelled + for { + var src net.Addr + var n int + n, src, e = (*c.listenConn).ReadFrom(buffer) + buf := buffer[:n] + if E.Chk(e) { + // Error("ReadFromUDP failed:", e) + continue + } + magic := string(buf[:4]) + if _, ok := handlers[magic]; ok { + // if caller needs to know the liveness status of the controller it is working on, the code below + if lastSent != nil && firstSender != nil { + *lastSent = time.Now() + } + nonceBytes := buf[4:16] + nonce := string(nonceBytes) + // decipher + var shard []byte + if shard, e = c.ciph.Open(nil, nonceBytes, buf[16:], nil); E.Chk(e) { + // corrupted or irrelevant message + continue + } + var bn *MsgBuffer + if bn, ok = c.buffers[nonce]; ok { + if !bn.Decoded { + bn.Buffers = append(bn.Buffers, shard) + if len(bn.Buffers) >= 3 { + // try to decode it + var cipherText []byte + if cipherText, e = fec.Decode(bn.Buffers); E.Chk(e) { + continue + } + bn.Decoded = true + if e = handlers[magic](ifc)(cipherText); E.Chk(e) { + continue + } + } + } else { + for i := range c.buffers { + if i != nonce { + // superseded messages can be deleted from the buffers, we don't add more data + // for the already decoded. + // F.Ln("deleting superseded buffer", hex.EncodeToString([]byte(i))) + delete(c.buffers, i) + } + } + } + } else { + // F.Ln("new message arriving", + // hex.EncodeToString([]byte(nonce))) + c.buffers[nonce] = &MsgBuffer{ + [][]byte{}, + time.Now(), false, src, + } + c.buffers[nonce].Buffers = append( + c.buffers[nonce]. + Buffers, shard, + ) + } + } + select { + case <-c.ctx.Done(): + break out + default: + } + } + }() + return +} + +// +// func GetUDPAddr(address string) (sendAddr *net.UDPAddr) { +// sendHost, sendPort, e := net.SplitHostPort(address) +// if e != nil { +// // return +// } +// sendPortI, e := strconv.ParseInt(sendPort, 10, 64) +// if e != nil { +// // return +// } +// sendAddr = &net.UDPAddr{IP: net.ParseIP(sendHost), +// Port: int(sendPortI)} +// // D.Ln("multicast", Address) +// // L.Spew(sendAddr) +// return +// } diff --git a/pkg/txauthor/author.go b/pkg/txauthor/author.go new file mode 100644 index 0000000..0d6fd69 --- /dev/null +++ b/pkg/txauthor/author.go @@ -0,0 +1,332 @@ +// Package txauthor provides transaction creation code for wallets. +package txauthor + +import ( + "errors" + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/txrules" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/txsizes" + h "github.com/p9c/p9/pkg/util/helpers" + "github.com/p9c/p9/pkg/wire" +) + +type ( + // InputSource provides transaction inputs referencing spendable outputs to construct a transaction outputting some + // target amount. If the target amount can not be satisified, this can be signaled by returning a total amount less + // than the target or by returning a more detailed error implementing InputSourceError. + InputSource func(target amt.Amount) ( + total amt.Amount, inputs []*wire.TxIn, + inputValues []amt.Amount, scripts [][]byte, e error, + ) + // InputSourceError describes the failure to provide enough input value from unspent transaction outputs to meet a + // target amount. A typed error is used so input sources can provide their own implementations describing the reason + // for the error, for example, due to spendable policies or locked coins rather than the wallet not having enough + // available input value. + InputSourceError interface { + error + InputSourceError() + } + // Default implementation of InputSourceError. + insufficientFundsError struct{} + // AuthoredTx holds the state of a newly-created transaction and the change output (if one was added). + AuthoredTx struct { + Tx *wire.MsgTx + PrevScripts [][]byte + PrevInputValues []amt.Amount + TotalInput amt.Amount + ChangeIndex int // negative if no change + } + // ChangeSource provides P2PKH change output scripts for transaction creation. + ChangeSource func() ([]byte, error) + // SecretsSource provides private keys and redeem scripts necessary for constructing transaction input signatures. + // Secrets are looked up by the corresponding Address for the previous output script. Addresses for lookup are + // created using the source's blockchain parameters and means a single SecretsSource can only manage secrets for a + // single chain. + // + // TODO: Rewrite this interface to look up private keys and redeem scripts for pubkeys, pubkey hashes, script + // hashes, etc. as separate interface methods. + // + // This would remove the ChainParams requirement of the interface and could avoid unnecessary conversions from + // previous output scripts to Addresses. This can not be done without modifications to the txscript package. + SecretsSource interface { + txscript.KeyDB + txscript.ScriptDB + ChainParams() *chaincfg.Params + } +) + +func (insufficientFundsError) InputSourceError() { +} +func (insufficientFundsError) Error() string { + return "insufficient funds available to construct transaction" +} + +// NewUnsignedTransaction creates an unsigned transaction paying to one or more non-change outputs. An appropriate +// transaction fee is included based on the transaction size. +// +// Transaction inputs are chosen from repeated calls to fetchInputs with increasing targets amounts. +// +// If any remaining output value can be returned to the wallet via a change output without violating mempool dust rules, +// a P2WPKH change output is appended to the transaction outputs. Since the change output may not be necessary, +// fetchChange is called zero or one times to generate this script. This function must return a P2WPKH script or +// smaller, otherwise fee estimation will be incorrect. +// +// If successful, the transaction, total input value spent, and all previous output scripts are returned. If the input +// source was unable to provide enough input value to pay for every output any any necessary fees, an InputSourceError +// is returned. +// +// BUGS: Fee estimation may be off when redeeming non-compressed P2PKH outputs. +func NewUnsignedTransaction( + outputs []*wire.TxOut, relayFeePerKb amt.Amount, + fetchInputs InputSource, fetchChange ChangeSource, +) (*AuthoredTx, error) { + targetAmount := h.SumOutputValues(outputs) + estimatedSize := txsizes.EstimateVirtualSize(1, 0, 0, outputs, true) + targetFee := txrules.FeeForSerializeSize(relayFeePerKb, estimatedSize) + for { + inputAmount, inputs, inputValues, scripts, e := fetchInputs(targetAmount + targetFee) + if e != nil { + return nil, e + } + if inputAmount < targetAmount+targetFee { + return nil, insufficientFundsError{} + } + // We count the types of inputs, which we'll use to estimate the vsize of the transaction. + var nested, p2wpkh, p2pkh int + for _, /*pkScript*/ _ = range scripts { + switch { + // // If this is a p2sh output, we assume this is a nested P2WKH. + // case txscript.IsPayToScriptHash(pkScript): + // nested++ + // case txscript.IsPayToWitnessPubKeyHash(pkScript): + // p2wpkh++ + default: + p2pkh++ + } + } + maxSignedSize := txsizes.EstimateVirtualSize( + p2pkh, p2wpkh, + nested, outputs, true, + ) + maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) + remainingAmount := inputAmount - targetAmount + if remainingAmount < maxRequiredFee { + targetFee = maxRequiredFee + continue + } + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: 0, + } + changeIndex := -1 + changeAmount := inputAmount - targetAmount - maxRequiredFee + if changeAmount != 0 && !txrules.IsDustAmount( + changeAmount, + txsizes.P2PKHPkScriptSize, relayFeePerKb, + ) { + changeScript, e := fetchChange() + if e != nil { + return nil, e + } + if len(changeScript) > txsizes.P2PKHPkScriptSize { + return nil, errors.New( + "fee estimation requires change " + + "scripts no larger than P2WPKH output scripts", + ) + } + change := wire.NewTxOut(int64(changeAmount), changeScript) + l := len(outputs) + unsignedTransaction.TxOut = append(outputs[:l:l], change) + changeIndex = l + } + return &AuthoredTx{ + Tx: unsignedTransaction, + PrevScripts: scripts, + PrevInputValues: inputValues, + TotalInput: inputAmount, + ChangeIndex: changeIndex, + }, nil + } +} + +// RandomizeOutputPosition randomizes the position of a transaction's output by swapping it with a random output. The +// new index is returned. This should be done before signing. +func RandomizeOutputPosition(outputs []*wire.TxOut, index int) int { + r := cprng.Int31n(int32(len(outputs))) + outputs[r], outputs[index] = outputs[index], outputs[r] + return int(r) +} + +// RandomizeChangePosition randomizes the position of an authored transaction's change output. This should be done +// before signing. +func (tx *AuthoredTx) RandomizeChangePosition() { + tx.ChangeIndex = RandomizeOutputPosition(tx.Tx.TxOut, tx.ChangeIndex) +} + +// AddAllInputScripts modifies transaction a transaction by adding inputs +// scripts for each input. Previous output scripts being redeemed by each input +// are passed in prevPkScripts and the slice length must match the number of +// inputs. Private keys and redeem scripts are looked up using a SecretsSource +// based on the previous output script. +func AddAllInputScripts( + tx *wire.MsgTx, prevPkScripts [][]byte, inputValues []amt.Amount, + secrets SecretsSource, +) (e error) { + inputs := tx.TxIn + // hashCache := txscript.NewTxSigHashes(tx) + chainParams := secrets.ChainParams() + if len(inputs) != len(prevPkScripts) { + return errors.New( + "tx.TxIn and prevPkScripts slices must " + + "have equal length", + ) + } + for i := range inputs { + pkScript := prevPkScripts[i] + switch { + // // If this is a p2sh output, who's script hash pre-image is a witness program, + // // then we'll need to use a modified signing function which generates both the + // // sigScript, and the witness script. + // case txscript.IsPayToScriptHash(pkScript): + // e := spendNestedWitnessPubKeyHash(inputs[i], pkScript, + // int64(inputValues[i]), chainParams, secrets, + // tx, hashCache, i) + // if e != nil { + // // return e + // } + // case txscript.IsPayToWitnessPubKeyHash(pkScript): + // e := spendWitnessKeyHash(inputs[i], pkScript, + // int64(inputValues[i]), chainParams, secrets, + // tx, hashCache, i) + // if e != nil { + // // return e + // } + default: + sigScript := inputs[i].SignatureScript + var script []byte + script, e = txscript.SignTxOutput( + chainParams, tx, i, + pkScript, txscript.SigHashAll, secrets, secrets, + sigScript, + ) + if e != nil { + return e + } + inputs[i].SignatureScript = script + } + } + return nil +} + +// spendWitnessKeyHash generates, and sets a valid witness for spending the +// // passed pkScript with the specified input amount. The input amount *must* +// // correspond to the output value of the previous pkScript, or else verification +// // will fail since the new sighash digest algorithm defined in BIP0143 includes +// // the input value in the sighash. +// func spendWitnessKeyHash(txIn *wire.TxIn, pkScript []byte, +// inputValue int64, chainParams *chaincfg.Params, secrets SecretsSource, +// tx *wire.MsgTx, hashCache *txscript.TxSigHashes, idx int) (e error) { +// // First obtain the key pair associated with this p2wkh address. +// _, addrs, _, e = txscript.ExtractPkScriptAddrs(pkScript, +// chainParams) +// if e != nil { +// // return e +// } +// privKey, compressed, e := secrets.GetKey(addrs[0]) +// if e != nil { +// // return e +// } +// pubKey := privKey.PubKey() +// // Once we have the key pair, generate a p2wkh address type, respecting the compression type of the generated key. +// var pubKeyHash []byte +// if compressed { +// pubKeyHash = btcaddr.Hash160(pubKey.SerializeCompressed()) +// } else { +// pubKeyHash = btcaddr.Hash160(pubKey.SerializeUncompressed()) +// } +// p2wkhAddr, e := util.NewAddressWitnessPubKeyHash(pubKeyHash, chainParams) +// if e != nil { +// // return e +// } +// // With the concrete address type, we can now generate the corresponding witness +// // program to be used to generate a valid witness which will allow us to spend +// // this output. +// witnessProgram, e := txscript.PayToAddrScript(p2wkhAddr) +// if e != nil { +// // return e +// } +// witnessScript, e := txscript.WitnessSignature(tx, hashCache, idx, +// inputValue, witnessProgram, txscript.SigHashAll, privKey, true) +// if e != nil { +// // return e +// } +// txIn.Witness = witnessScript +// return nil +// } + +// spendNestedWitnessPubKey generates both a sigScript, and valid witness for +// // spending the passed pkScript with the specified input amount. The generated +// // sigScript is the version 0 p2wkh witness program corresponding to the queried +// // key. The witness stack is identical to that of one which spends a regular +// // p2wkh output. The input amount *must* correspond to the output value of the +// // previous pkScript, or else verification will fail since the new sighash +// // digest algorithm defined in BIP0143 includes the input value in the sighash. +// func spendNestedWitnessPubKeyHash(txIn *wire.TxIn, pkScript []byte, +// inputValue int64, chainParams *chaincfg.Params, secrets SecretsSource, +// tx *wire.MsgTx, hashCache *txscript.TxSigHashes, idx int) (e error) { +// // First we need to obtain the key pair related to this p2sh output. +// _, addrs, _, e = txscript.ExtractPkScriptAddrs(pkScript, +// chainParams) +// if e != nil { +// // return e +// } +// privKey, compressed, e := secrets.GetKey(addrs[0]) +// if e != nil { +// // return e +// } +// pubKey := privKey.PubKey() +// var pubKeyHash []byte +// if compressed { +// pubKeyHash = btcaddr.Hash160(pubKey.SerializeCompressed()) +// } else { +// pubKeyHash = btcaddr.Hash160(pubKey.SerializeUncompressed()) +// } +// // Next, we'll generate a valid sigScript that'll allow us to spend the p2sh +// // output. The sigScript will contain only a single push of the p2wkh witness +// // program corresponding to the matching public key of this address. +// p2wkhAddr, e := util.NewAddressWitnessPubKeyHash(pubKeyHash, chainParams) +// if e != nil { +// // return e +// } +// witnessProgram, e := txscript.PayToAddrScript(p2wkhAddr) +// if e != nil { +// // return e +// } +// bldr := txscript.NewScriptBuilder() +// bldr.AddData(witnessProgram) +// sigScript, e := bldr.Script() +// if e != nil { +// // return e +// } +// txIn.SignatureScript = sigScript +// // With the sigScript in place, we'll next generate the proper witness that'll +// // allow us to spend the p2wkh output. +// witnessScript, e := txscript.WitnessSignature(tx, hashCache, idx, +// inputValue, witnessProgram, txscript.SigHashAll, privKey, compressed) +// if e != nil { +// // return e +// } +// txIn.Witness = witnessScript +// return nil +// } + +// AddAllInputScripts modifies an authored transaction by adding inputs scripts for each input of an authored +// transaction. Private keys and redeem scripts are looked up using a SecretsSource based on the previous output script. +func (tx *AuthoredTx) AddAllInputScripts(secrets SecretsSource) (e error) { + return AddAllInputScripts(tx.Tx, tx.PrevScripts, tx.PrevInputValues, secrets) +} diff --git a/pkg/txauthor/author_test.go b/pkg/txauthor/author_test.go new file mode 100644 index 0000000..8ed9650 --- /dev/null +++ b/pkg/txauthor/author_test.go @@ -0,0 +1,225 @@ +// Copyright (c) 2016 The btcsuite developers +package txauthor + +import ( + "github.com/p9c/p9/pkg/amt" + "testing" + + "github.com/p9c/p9/pkg/txrules" + "github.com/p9c/p9/pkg/txsizes" + "github.com/p9c/p9/pkg/wire" +) + +func p2pkhOutputs(amounts ...amt.Amount) []*wire.TxOut { + v := make([]*wire.TxOut, 0, len(amounts)) + for _, a := range amounts { + outScript := make([]byte, txsizes.P2PKHOutputSize) + v = append(v, wire.NewTxOut(int64(a), outScript)) + } + return v +} +func makeInputSource(unspents []*wire.TxOut) InputSource { + // Return outputs in order. + currentTotal := amt.Amount(0) + currentInputs := make([]*wire.TxIn, 0, len(unspents)) + currentInputValues := make([]amt.Amount, 0, len(unspents)) + f := func(target amt.Amount) (amt.Amount, []*wire.TxIn, []amt.Amount, [][]byte, error) { + for currentTotal < target && len(unspents) != 0 { + u := unspents[0] + unspents = unspents[1:] + nextInput := wire.NewTxIn(&wire.OutPoint{}, nil, nil) + currentTotal += amt.Amount(u.Value) + currentInputs = append(currentInputs, nextInput) + currentInputValues = append(currentInputValues, amt.Amount(u.Value)) + } + return currentTotal, currentInputs, currentInputValues, make([][]byte, len(currentInputs)), nil + } + return f +} +func TestNewUnsignedTransaction(t *testing.T) { + tests := []struct { + UnspentOutputs []*wire.TxOut + Outputs []*wire.TxOut + RelayFee amt.Amount + ChangeAmount amt.Amount + InputSourceError bool + InputCount int + }{ + 0: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e8), + RelayFee: 1e3, + InputSourceError: true, + }, + 1: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e6), + RelayFee: 1e3, + ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6), true), + ), + InputCount: 1, + }, + 2: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e6), + RelayFee: 1e4, + ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e4, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6), true), + ), + InputCount: 1, + }, + 3: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e6, 1e6, 1e6), + RelayFee: 1e4, + ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(1e4, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6, 1e6, 1e6), true), + ), + InputCount: 1, + }, + 4: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e6, 1e6, 1e6), + RelayFee: 2.55e3, + ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(2.55e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(1e6, 1e6, 1e6), true), + ), + InputCount: 1, + }, + // Test dust thresholds (546 for a 1e3 relay fee). + 5: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true), + ), + ), + RelayFee: 1e3, + ChangeAmount: 545, + InputCount: 1, + }, + 6: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true), + ), + ), + RelayFee: 1e3, + ChangeAmount: 546, + InputCount: 1, + }, + // Test dust thresholds (1392.3 for a 2.55e3 relay fee). + 7: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e8 - 1392 - txrules.FeeForSerializeSize(2.55e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true), + ), + ), + RelayFee: 2.55e3, + ChangeAmount: 1392, + InputCount: 1, + }, + 8: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e8 - 1393 - txrules.FeeForSerializeSize(2.55e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true), + ), + ), + RelayFee: 2.55e3, + ChangeAmount: 1393, + InputCount: 1, + }, + // Test two unspent outputs available but only one needed (tested fee only includes one input rather than using + // a serialize size for each). + 9: { + UnspentOutputs: p2pkhOutputs(1e8, 1e8), + Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true), + ), + ), + RelayFee: 1e3, + ChangeAmount: 546, + InputCount: 1, + }, + // Test that second output is not included to make the change output not dust and be included in the + // transaction. + // + // It's debatable whether or not this is a good idea, but it's how the function was written, so test it anyways. + 10: { + UnspentOutputs: p2pkhOutputs(1e8, 1e8), + Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(1, 0, 0, p2pkhOutputs(0), true), + ), + ), + RelayFee: 1e3, + ChangeAmount: 545, + InputCount: 1, + }, + // Test two unspent outputs available where both are needed. + 11: { + UnspentOutputs: p2pkhOutputs(1e8, 1e8), + Outputs: p2pkhOutputs(1e8), + RelayFee: 1e3, + ChangeAmount: 1e8 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(2, 0, 0, p2pkhOutputs(1e8), true), + ), + InputCount: 2, + }, + // Test that zero change outputs are not included (ChangeAmount=0 means don't include any change output). + 12: { + UnspentOutputs: p2pkhOutputs(1e8), + Outputs: p2pkhOutputs(1e8), + RelayFee: 0, + ChangeAmount: 0, + InputCount: 1, + }, + } + changeSource := func() ([]byte, error) { + // Only length matters for these tests. + return make([]byte, txsizes.P2PKHPkScriptSize), nil + } + for i, test := range tests { + inputSource := makeInputSource(test.UnspentOutputs) + tx, e := NewUnsignedTransaction(test.Outputs, test.RelayFee, inputSource, changeSource) + switch e := e.(type) { + case nil: + case InputSourceError: + if !test.InputSourceError { + t.Errorf("Test %d: Returned InputSourceError but expected "+ + "change output with amount %v", i, test.ChangeAmount, + ) + } + continue + default: + t.Errorf("Test %d: Unexpected error: %v", i, e) + continue + } + if tx.ChangeIndex < 0 { + if test.ChangeAmount != 0 { + t.Errorf("Test %d: No change output added but expected output with amount %v", + i, test.ChangeAmount, + ) + continue + } + } else { + changeAmount := amt.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value) + if test.ChangeAmount == 0 { + t.Errorf("Test %d: Included change output with value %v but expected no change", + i, changeAmount, + ) + continue + } + if changeAmount != test.ChangeAmount { + t.Errorf("Test %d: Got change amount %v, Expected %v", + i, changeAmount, test.ChangeAmount, + ) + continue + } + } + if len(tx.Tx.TxIn) != test.InputCount { + t.Errorf("Test %d: Used %d outputs from input source, Expected %d", + i, len(tx.Tx.TxIn), test.InputCount, + ) + } + } +} diff --git a/pkg/txauthor/cprng.go b/pkg/txauthor/cprng.go new file mode 100644 index 0000000..f4b0cf4 --- /dev/null +++ b/pkg/txauthor/cprng.go @@ -0,0 +1,33 @@ +package txauthor + +import ( + "crypto/rand" + "encoding/binary" + mrand "math/rand" + "sync" +) + +// cprng is a cryptographically random-seeded math/rand prng. It is seeded +// during package init. Any initialization errors result in panics. It is safe +// for concurrent access. +var cprng = cprngType{} + +type cprngType struct { + r *mrand.Rand + mu sync.Mutex +} + +func init() { + buf := make([]byte, 8) + _, e := rand.Read(buf) + if e != nil { + panic("Failed to seed prng: " + e.Error()) + } + seed := int64(binary.LittleEndian.Uint64(buf)) + cprng.r = mrand.New(mrand.NewSource(seed)) +} +func (c *cprngType) Int31n(n int32) int32 { + defer c.mu.Unlock() // Int31n may panic + c.mu.Lock() + return c.r.Int31n(n) +} diff --git a/pkg/txauthor/log.go b/pkg/txauthor/log.go new file mode 100644 index 0000000..135edcf --- /dev/null +++ b/pkg/txauthor/log.go @@ -0,0 +1,43 @@ +package txauthor + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/txrules/rules.go b/pkg/txrules/rules.go new file mode 100644 index 0000000..aaabbc5 --- /dev/null +++ b/pkg/txrules/rules.go @@ -0,0 +1,85 @@ +// Package txrules provides transaction rules that should be followed by +// transaction authors for wide mempool acceptance and quick mining. +package txrules + +import ( + "errors" + "github.com/p9c/p9/pkg/amt" + + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/wire" +) + +// DefaultRelayFeePerKb is the default minimum relay fee policy for a mempool. +const DefaultRelayFeePerKb amt.Amount = 1e3 + +// Transaction rule violations +var ( + ErrAmountNegative = errors.New("transaction output amount is negative") + ErrAmountExceedsMax = errors.New("transaction output amount exceeds maximum value") + ErrOutputIsDust = errors.New("transaction output is dust") +) + +// GetDustThreshold is used to define the amount below which output will be determined as dust. Threshold is determined +// as 3 times the relay fee. +func GetDustThreshold(scriptSize int, relayFeePerKb amt.Amount) amt.Amount { + // Calculate the total (estimated) cost to the network. This is calculated using the serialize size of the output + // plus the serial size of a transaction input which redeems it. The output is assumed to be compressed P2PKH as + // this is the most common script type. Use the average size of a compressed P2PKH redeem input (148) rather than + // the largest possible (txsizes.RedeemP2PKHInputSize). + totalSize := 8 + wire.VarIntSerializeSize(uint64(scriptSize)) + + scriptSize + 148 + byteFee := relayFeePerKb / 1000 + relayFee := amt.Amount(totalSize) * byteFee + return 3 * relayFee +} + +// IsDustAmount determines whether a transaction output value and script length would cause the output to be considered +// dust. Transactions with dust outputs are not standard and are rejected by mempools with default policies. +func IsDustAmount(amount amt.Amount, scriptSize int, relayFeePerKb amt.Amount) bool { + return amount < GetDustThreshold(scriptSize, relayFeePerKb) +} + +// IsDustOutput determines whether a transaction output is considered dust. Transactions with dust outputs are not +// standard and are rejected by mempools with default policies. +func IsDustOutput(output *wire.TxOut, relayFeePerKb amt.Amount) bool { + // Unspendable outputs which solely carry data are not checked for dust. + if txscript.GetScriptClass(output.PkScript) == txscript.NullDataTy { + return false + } + // All other unspendable outputs are considered dust. + if txscript.IsUnspendable(output.PkScript) { + return true + } + return IsDustAmount( + amt.Amount(output.Value), len(output.PkScript), + relayFeePerKb, + ) +} + +// CheckOutput performs simple consensus and policy tests on a transaction output. +func CheckOutput(output *wire.TxOut, relayFeePerKb amt.Amount) (e error) { + if output.Value < 0 { + return ErrAmountNegative + } + if output.Value > int64(amt.MaxSatoshi) { + return ErrAmountExceedsMax + } + if IsDustOutput(output, relayFeePerKb) { + return ErrOutputIsDust + } + return nil +} + +// FeeForSerializeSize calculates the required fee for a transaction of some arbitrary size given a mempool's relay fee +// policy. +func FeeForSerializeSize(relayFeePerKb amt.Amount, txSerializeSize int) amt.Amount { + fee := relayFeePerKb * amt.Amount(txSerializeSize) / 1000 + if fee == 0 && relayFeePerKb > 0 { + fee = relayFeePerKb + } + if fee < 0 || fee > amt.MaxSatoshi { + fee = amt.MaxSatoshi + } + return fee +} diff --git a/pkg/txscript/README.md b/pkg/txscript/README.md new file mode 100755 index 0000000..6027660 --- /dev/null +++ b/pkg/txscript/README.md @@ -0,0 +1,41 @@ +# txscript + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://godoc.org/github.com/p9c/p9/txscript?status.png)](http://godoc.org/github.com/p9c/p9/txscript) + +Package txscript implements the bitcoin transaction script language. There is a +comprehensive test suite. + +This package has intentionally been designed so it can be used as a standalone +package for any projects needing to use or validate bitcoin transaction scripts. + +## Bitcoin Scripts + +Bitcoin provides a stack-based, FORTH-like language for the scripts in the +bitcoin transactions. This language is not turing complete although it is still +fairly powerful. A description of the language can be found +at https://en.bitcoin.it/wiki/Script + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/txscript +``` + +## Examples + +- [Standard Pay-to-pubkey-hash Script](http://godoc.org/github.com/p9c/p9/txscript#example-PayToAddrScript) + Demonstrates creating a script which pays to a bitcoin address. It also prints + the created script hex and uses the DisasmString function to display the + disassembled script. + +- [Extracting Details from Standard Scripts](http://godoc.org/github.com/p9c/p9/txscript#example-ExtractPkScriptAddrs) + Demonstrates extracting information from a standard public key script. + +- [Manually Signing a Transaction Output](http://godoc.org/github.com/p9c/p9/txscript#example-SignTxOutput) + Demonstrates manually creating and signing a redeem transaction. + +## License + +Package txscript is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/txscript/_example_test.go b/pkg/txscript/_example_test.go new file mode 100644 index 0000000..ac435aa --- /dev/null +++ b/pkg/txscript/_example_test.go @@ -0,0 +1,135 @@ +package txscript_test + +import ( + "encoding/hex" + "fmt" + + "github.com/p9c/p9/pkg/chain/config/netparams" + chainhash "github.com/p9c/p9/pkg/chainhash" + txscript "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/wire" + ecc "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/util" +) + +// This example demonstrates creating a script which pays to a bitcoin address. It also prints the created script hex +// and uses the DisasmString function to display the disassembled script. +func ExamplePayToAddrScript() { + // Parse the address to send the coins to into a util.Address which is useful to ensure the accuracy of the address and determine the address type. It is also required for the upcoming call to + // PayToAddrScript. + addressStr := "12gpXQVcCL2qhTNQgyLVdCFG2Qs2px98nV" + address, e := util.DecodeAddress(addressStr, &netparams.MainNetParams) + if e != nil { + return + } + // Create a public key script that pays to the address. + script, e := txscript.PayToAddrScript(address) + if e != nil { + return + } + fmt.Printf("Script Hex: %x\n", script) + disasm, e := txscript.DisasmString(script) + if e != nil { + return + } + fmt.Println("Script Disassembly:", disasm) + // Output: + // Script Hex: 76a914128004ff2fcaf13b2b91eb654b1dc2b674f7ec6188ac + // Script Disassembly: OP_DUP OP_HASH160 128004ff2fcaf13b2b91eb654b1dc2b674f7ec61 OP_EQUALVERIFY OP_CHECKSIG +} + +// // This example demonstrates extracting information from a standard public key script. +// func ExampleExtractPkScriptAddrs() { +// // Start with a standard pay-to-pubkey-hash script. +// scriptHex := "76a914128004ff2fcaf13b2b91eb654b1dc2b674f7ec6188ac" +// script, e := hex.DecodeString(scriptHex) +// if e != nil { +// L.Script// return +// } +// // Extract and print details from the script. +// scriptClass, addresses, reqSigs, e := txscript.ExtractPkScriptAddrs( +// script, &netparams.MainNetParams) +// if e != nil { +// L.Script// return +// } +// fmt.Println("Script Class:", scriptClass) +// fmt.Println("Addresses:", addresses) +// fmt.Println("Required Signatures:", reqSigs) +// // Output: +// // Script Class: pubkeyhash +// // Addresses: [12gpXQVcCL2qhTNQgyLVdCFG2Qs2px98nV] +// // Required Signatures: 1 +// } + +// This example demonstrates manually creating and signing a redeem transaction. +func ExampleSignTxOutput() { + // Ordinarily the private key would come from whatever storage mechanism is being used, but for this example just hard code it. + privKeyBytes, e := hex.DecodeString("22a47fa09a223f2aa079edf85a7c2" + + "d4f8720ee63e502ee2869afab7de234b80c", + ) + if e != nil { + return + } + privKey, pubKey := ecc.PrivKeyFromBytes(ecc.S256(), privKeyBytes) + pubKeyHash := util.Hash160(pubKey.SerializeCompressed()) + addr, e := util.NewAddressPubKeyHash(pubKeyHash, + &netparams.MainNetParams, + ) + if e != nil { + return + } + // For this example, create a fake transaction that represents what would ordinarily be the real transaction that is being spent. It contains a single output that pays to address in the amount of 1 DUO. + originTx := wire.NewMsgTx(wire.TxVersion) + prevOut := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) + txIn := wire.NewTxIn(prevOut, []byte{txscript.OP_0, txscript.OP_0}, nil) + originTx.AddTxIn(txIn) + pkScript, e := txscript.PayToAddrScript(addr) + if e != nil { + return + } + txOut := wire.NewTxOut(100000000, pkScript) + originTx.AddTxOut(txOut) + originTxHash := originTx.TxHash() + // Create the transaction to redeem the fake transaction. + redeemTx := wire.NewMsgTx(wire.TxVersion) + // Add the input(s) the redeeming transaction will spend. There is no signature script at this point since it hasn't been created or signed yet, hence nil is provided for it. + prevOut = wire.NewOutPoint(&originTxHash, 0) + txIn = wire.NewTxIn(prevOut, nil, nil) + redeemTx.AddTxIn(txIn) + // Ordinarily this would contain that actual destination of the funds, but for this example don't bother. + txOut = wire.NewTxOut(0, nil) + redeemTx.AddTxOut(txOut) + // Sign the redeeming transaction. + lookupKey := func(a util.Address) (*ecc.PrivateKey, bool, error) { + // Ordinarily this function would involve looking up the private key for the provided address, but since the only thing being signed in this example uses the address associated with the private key from above, simply return it with the compressed flag set since the address is using the associated compressed public key. + // NOTE: If you want to prove the code is actually signing the transaction properly, uncomment the following line which intentionally returns an invalid key to sign with, which in turn will result in a failure during the script execution when verifying the signature. + // privKey.D.SetInt64(12345) + // + return privKey, true, nil + } + // Notice that the script database parameter is nil here since it isn't used. It must be specified when pay-to-script-hash transactions are being signed. + sigScript, e := txscript.SignTxOutput(&netparams.MainNetParams, + redeemTx, 0, originTx.TxOut[0].PkScript, txscript.SigHashAll, + txscript.KeyClosure(lookupKey), nil, nil, + ) + if e != nil { + return + } + redeemTx.TxIn[0].SignatureScript = sigScript + // Prove that the transaction has been validly signed by executing the script pair. + flags := txscript.ScriptBip16 | txscript.ScriptVerifyDERSignatures | + txscript.ScriptStrictMultiSig | + txscript.ScriptDiscourageUpgradableNops + vm, e := txscript.NewEngine(originTx.TxOut[0].PkScript, redeemTx, 0, + flags, nil, nil, -1, + ) + if e != nil { + return + } + if e := vm.Execute(); dbg.Chk(e) { + return + } + fmt.Println("Transaction successfully signed") + // Output: + // Transaction successfully signed +} diff --git a/pkg/txscript/_sign_test.go b/pkg/txscript/_sign_test.go new file mode 100644 index 0000000..5b7d5ee --- /dev/null +++ b/pkg/txscript/_sign_test.go @@ -0,0 +1,2022 @@ +package txscript + +import ( + "errors" + "fmt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "testing" + + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/wire" +) + +type addressToKey struct { + key *ec.PrivateKey + compressed bool +} + +func mkGetKey(keys map[string]addressToKey) KeyDB { + if keys == nil { + return KeyClosure( + func(addr btcaddr.Address) ( + *ec.PrivateKey, + bool, error, + ) { + return nil, false, errors.New("nope") + }, + ) + } + return KeyClosure( + func(addr btcaddr.Address) ( + *ec.PrivateKey, + bool, error, + ) { + a2k, ok := keys[addr.EncodeAddress()] + if !ok { + return nil, false, errors.New("nope") + } + return a2k.key, a2k.compressed, nil + }, + ) +} +func mkGetScript(scripts map[string][]byte) ScriptDB { + if scripts == nil { + return ScriptClosure( + func(addr btcaddr.Address) ([]byte, error) { + return nil, errors.New("nope") + }, + ) + } + return ScriptClosure( + func(addr btcaddr.Address) ([]byte, error) { + script, ok := scripts[addr.EncodeAddress()] + if !ok { + return nil, errors.New("nope") + } + return script, nil + }, + ) +} +func checkScripts(msg string, tx *wire.MsgTx, idx int, inputAmt int64, sigScript, pkScript []byte) (e error) { + tx.TxIn[idx].SignatureScript = sigScript + vm, e := NewEngine( + pkScript, tx, idx, + ScriptBip16|ScriptVerifyDERSignatures, nil, nil, inputAmt, + ) + if e != nil { + return fmt.Errorf( + "failed to make script engine for %s: %v", + msg, e, + ) + } + e = vm.Execute() + if e != nil { + return fmt.Errorf( + "invalid script signature for %s: %v", msg, + e, + ) + } + return nil +} +func signAndCheck( + msg string, tx *wire.MsgTx, idx int, inputAmt int64, pkScript []byte, + hashType SigHashType, kdb KeyDB, sdb ScriptDB, + previousScript []byte, +) (e error) { + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, tx, idx, + pkScript, hashType, kdb, sdb, nil, + ) + if e != nil { + return fmt.Errorf("failed to sign output %s: %v", msg, e) + } + return checkScripts(msg, tx, idx, inputAmt, sigScript, pkScript) +} +func TestSignTxOutput(t *testing.T) { + t.Parallel() + // make key + // make script based on key. + // sign with magic pixie dust. + hashTypes := []SigHashType{ + SigHashOld, // no longer used but should act like all + SigHashAll, + SigHashNone, + SigHashSingle, + SigHashAll | SigHashAnyOneCanPay, + SigHashNone | SigHashAnyOneCanPay, + SigHashSingle | SigHashAnyOneCanPay, + } + inputAmounts := []int64{5, 10, 15} + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0, + }, + Sequence: 4294967295, + }, + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 1, + }, + Sequence: 4294967295, + }, + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 2, + }, + Sequence: 4294967295, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1, + }, + { + Value: 2, + }, + { + Value: 3, + }, + }, + LockTime: 0, + } + // Pay to Pubkey Hash (uncompressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript(nil), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to Pubkey Hash (uncompressed) (merging with correct) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript(nil), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript(nil), sigScript, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts(msg, tx, i, inputAmounts[i], sigScript, pkScript) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Pay to Pubkey Hash (compressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript(nil), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to Pubkey Hash (compressed) with duplicate merge + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript(nil), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript(nil), sigScript, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, pkScript, + ) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Pay to PubKey (uncompressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript(nil), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to PubKey (uncompressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript(nil), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript(nil), sigScript, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts(msg, tx, i, inputAmounts[i], sigScript, pkScript) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Pay to PubKey (compressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript(nil), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to PubKey (compressed) with duplicate merge + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript(nil), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, pkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript(nil), sigScript, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, pkScript, + ) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // As before, but with p2sh now. Pay to Pubkey Hash (uncompressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + var ad *btcaddr.PubKeyHash + ad, e = btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(ad) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + break + } + scriptAddr, e := NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript( + scriptAddr, + ) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to Pubkey Hash (uncompressed) with duplicate merge + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + break + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript( + scriptAddr, + ) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, scriptPkScript, + ) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Pay to Pubkey Hash (compressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript( + scriptAddr, + ) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to Pubkey Hash (compressed) with duplicate merge + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKeyHash( + btcaddr.Hash160(pk), &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript( + scriptAddr, + ) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, scriptPkScript, + ) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Pay to PubKey (uncompressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript( + scriptAddr, + ) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to PubKey (uncompressed) with duplicate merge + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeUncompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript(scriptAddr) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, false}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, scriptPkScript, + ) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Pay to PubKey (compressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript(scriptAddr) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ); E.Chk(e) { + break + } + } + } + // Pay to PubKey (compressed) + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk := (*ec.PublicKey)(&key.PublicKey). + SerializeCompressed() + address, e := btcaddr.NewPubKey( + pk, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + pkScript, e := PayToAddrScript(address) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := address.NewAddressScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript(scriptAddr) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // by the above loop, this should be valid, now sign again and merge. + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address.EncodeAddress(): {key, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s a "+ + "second time: %v", msg, e, + ) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, scriptPkScript, + ) + if e != nil { + t.Errorf( + "twice signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Basic Multisig + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key1, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk1 := (*ec.PublicKey)(&key1.PublicKey). + SerializeCompressed() + address1, e := btcaddr.NewPubKey( + pk1, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + key2, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey 2 for %s: %v", + msg, e, + ) + break + } + pk2 := (*ec.PublicKey)(&key2.PublicKey). + SerializeCompressed() + address2, e := btcaddr.NewPubKey( + pk2, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address 2 for %s: %v", + msg, e, + ) + break + } + pkScript, e := MultiSigScript( + []*btcaddr.PubKey{address1, address2}, + 2, + ) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := btcaddr.NewScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript(scriptAddr) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + if e := signAndCheck( + msg, tx, i, inputAmounts[i], + scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address1.EncodeAddress(): {key1, true}, + address2.EncodeAddress(): {key2, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ); E.Chk(e) { + break + } + } + } + // Two part multisig, sign with one key then the other. + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key1, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk1 := (*ec.PublicKey)(&key1.PublicKey). + SerializeCompressed() + address1, e := btcaddr.NewPubKey( + pk1, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + key2, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey 2 for %s: %v", + msg, e, + ) + break + } + pk2 := (*ec.PublicKey)(&key2.PublicKey). + SerializeCompressed() + address2, e := btcaddr.NewPubKey( + pk2, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address 2 for %s: %v", + msg, e, + ) + break + } + pkScript, e := MultiSigScript( + []*btcaddr.PubKey{address1, address2}, + 2, + ) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := btcaddr.NewScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript(scriptAddr) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address1.EncodeAddress(): {key1, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // Only 1 out of 2 signed, this *should* fail. + if checkScripts( + msg, tx, i, inputAmounts[i], sigScript, + scriptPkScript, + ) == nil { + t.Errorf("part signed script valid for %s", msg) + break + } + // Sign with the other key and merge + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address2.EncodeAddress(): {key2, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), sigScript, + ) + if e != nil { + t.Errorf("failed to sign output %s: %v", msg, e) + break + } + e = checkScripts( + msg, tx, i, inputAmounts[i], sigScript, + scriptPkScript, + ) + if e != nil { + t.Errorf( + "fully signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } + // Two part multisig, sign with one key then both, check key dedup correctly. + for _, hashType := range hashTypes { + for i := range tx.TxIn { + msg := fmt.Sprintf("%d:%d", hashType, i) + key1, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey for %s: %v", + msg, e, + ) + break + } + pk1 := (*ec.PublicKey)(&key1.PublicKey). + SerializeCompressed() + address1, e := btcaddr.NewPubKey( + pk1, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address for %s: %v", + msg, e, + ) + break + } + key2, e := ec.NewPrivateKey(ec.S256()) + if e != nil { + t.Errorf( + "failed to make privKey 2 for %s: %v", + msg, e, + ) + break + } + pk2 := (*ec.PublicKey)(&key2.PublicKey). + SerializeCompressed() + address2, e := btcaddr.NewPubKey( + pk2, + &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make address 2 for %s: %v", + msg, e, + ) + break + } + pkScript, e := MultiSigScript( + []*btcaddr.PubKey{address1, address2}, + 2, + ) + if e != nil { + t.Errorf( + "failed to make pkscript "+ + "for %s: %v", msg, e, + ) + } + scriptAddr, e := btcaddr.NewScriptHash( + pkScript, &chaincfg.TestNet3Params, + ) + if e != nil { + t.Errorf( + "failed to make p2sh addr for %s: %v", + msg, e, + ) + break + } + scriptPkScript, e := PayToAddrScript(scriptAddr) + if e != nil { + t.Errorf( + "failed to make script pkscript for "+ + "%s: %v", msg, e, + ) + break + } + sigScript, e := SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address1.EncodeAddress(): {key1, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), nil, + ) + if e != nil { + t.Errorf( + "failed to sign output %s: %v", msg, + e, + ) + break + } + // Only 1 out of 2 signed, this *should* fail. + if checkScripts( + msg, tx, i, inputAmounts[i], sigScript, + scriptPkScript, + ) == nil { + t.Errorf("part signed script valid for %s", msg) + break + } + // Sign with the other key and merge + sigScript, e = SignTxOutput( + &chaincfg.TestNet3Params, + tx, i, scriptPkScript, hashType, + mkGetKey( + map[string]addressToKey{ + address1.EncodeAddress(): {key1, true}, + address2.EncodeAddress(): {key2, true}, + }, + ), mkGetScript( + map[string][]byte{ + scriptAddr.EncodeAddress(): pkScript, + }, + ), sigScript, + ) + if e != nil { + t.Errorf("failed to sign output %s: %v", msg, e) + break + } + // Now we should pass. + e = checkScripts( + msg, tx, i, inputAmounts[i], + sigScript, scriptPkScript, + ) + if e != nil { + t.Errorf( + "fully signed script invalid for "+ + "%s: %v", msg, e, + ) + break + } + } + } +} + +type tstInput struct { + txout *wire.TxOut + sigscriptGenerates bool + inputValidates bool + indexOutOfRange bool +} +type tstSigScript struct { + name string + inputs []tstInput + hashType SigHashType + compress bool + scriptAtWrongIndex bool +} + +var coinbaseOutPoint = &wire.OutPoint{ + Index: (1 << 32) - 1, +} + +// Pregenerated private key, with associated public key and pkScripts for the uncompressed and compressed hash160. +var ( + privKeyD = []byte{ + 0x6b, 0x0f, 0xd8, 0xda, 0x54, 0x22, 0xd0, 0xb7, + 0xb4, 0xfc, 0x4e, 0x55, 0xd4, 0x88, 0x42, 0xb3, 0xa1, 0x65, + 0xac, 0x70, 0x7f, 0x3d, 0xa4, 0x39, 0x5e, 0xcb, 0x3b, 0xb0, + 0xd6, 0x0e, 0x06, 0x92, + } + // pubkeyX = []byte{0xb2, 0x52, 0xf0, 0x49, 0x85, 0x78, 0x03, 0x03, 0xc8, + // 0x7d, 0xce, 0x51, 0x7f, 0xa8, 0x69, 0x0b, 0x91, 0x95, 0xf4, + // 0xf3, 0x5c, 0x26, 0x73, 0x05, 0x05, 0xa2, 0xee, 0xbc, 0x09, + // 0x38, 0x34, 0x3a} + // pubkeyY = []byte{0xb7, 0xc6, 0x7d, 0xb2, 0xe1, 0xff, 0xc8, 0x43, 0x1f, + // 0x63, 0x32, 0x62, 0xaa, 0x60, 0xc6, 0x83, 0x30, 0xbd, 0x24, + // 0x7e, 0xef, 0xdb, 0x6f, 0x2e, 0x8d, 0x56, 0xf0, 0x3c, 0x9f, + // 0x6d, 0xb6, 0xf8} + uncompressedPkScript = []byte{ + 0x76, 0xa9, 0x14, 0xd1, 0x7c, 0xb5, + 0xeb, 0xa4, 0x02, 0xcb, 0x68, 0xe0, 0x69, 0x56, 0xbf, 0x32, + 0x53, 0x90, 0x0e, 0x0a, 0x86, 0xc9, 0xfa, 0x88, 0xac, + } + compressedPkScript = []byte{ + 0x76, 0xa9, 0x14, 0x27, 0x4d, 0x9f, 0x7f, + 0x61, 0x7e, 0x7c, 0x7a, 0x1c, 0x1f, 0xb2, 0x75, 0x79, 0x10, + 0x43, 0x65, 0x68, 0x27, 0x9d, 0x86, 0x88, 0xac, + } + shortPkScript = []byte{ + 0x76, 0xa9, 0x14, 0xd1, 0x7c, 0xb5, + 0xeb, 0xa4, 0x02, 0xcb, 0x68, 0xe0, 0x69, 0x56, 0xbf, 0x32, + 0x53, 0x90, 0x0e, 0x0a, 0x88, 0xac, + } + // uncompressedAddrStr = "1L6fd93zGmtzkK6CsZFVVoCwzZV3MUtJ4F" + // compressedAddrStr = "14apLppt9zTq6cNw8SDfiJhk9PhkZrQtYZ" +) + +// Pretend output amounts. +const coinbaseVal = 2500000000 +const fee = 5000000 + +var sigScriptTests = []tstSigScript{ + { + name: "one input uncompressed", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "two inputs uncompressed", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + { + txout: wire.NewTxOut(coinbaseVal+fee, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "one input compressed", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, compressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: true, + scriptAtWrongIndex: false, + }, + { + name: "two inputs compressed", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, compressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + { + txout: wire.NewTxOut(coinbaseVal+fee, compressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: true, + scriptAtWrongIndex: false, + }, + { + name: "hashType SigHashNone", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashNone, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "hashType SigHashSingle", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashSingle, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "hashType SigHashAnyoneCanPay", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAnyOneCanPay, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "hashType non-standard", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: 0x04, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "invalid compression", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: false, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: true, + scriptAtWrongIndex: false, + }, + { + name: "short PkScript", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, shortPkScript), + sigscriptGenerates: false, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: false, + scriptAtWrongIndex: false, + }, + { + name: "valid script at wrong index", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + { + txout: wire.NewTxOut(coinbaseVal+fee, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: false, + scriptAtWrongIndex: true, + }, + { + name: "index out of range", + inputs: []tstInput{ + { + txout: wire.NewTxOut(coinbaseVal, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + { + txout: wire.NewTxOut(coinbaseVal+fee, uncompressedPkScript), + sigscriptGenerates: true, + inputValidates: true, + indexOutOfRange: false, + }, + }, + hashType: SigHashAll, + compress: false, + scriptAtWrongIndex: true, + }, +} + +// Test the sigscript generation for valid and invalid inputs, all hashTypes, and with and without compression. This test creates sigscripts to spend fake coinbase inputs, as sigscripts cannot be created for the MsgTxs in txTests, since they come from the blockchain and we don't have the private keys. +func TestSignatureScript(t *testing.T) { + t.Parallel() + privKey, _ := ec.PrivKeyFromBytes(ec.S256(), privKeyD) +nexttest: + for i := range sigScriptTests { + tx := wire.NewMsgTx(wire.TxVersion) + output := wire.NewTxOut(500, []byte{OP_RETURN}) + tx.AddTxOut(output) + for range sigScriptTests[i].inputs { + txin := wire.NewTxIn(coinbaseOutPoint, nil, nil) + tx.AddTxIn(txin) + } + var script []byte + var e error + for j := range tx.TxIn { + var idx int + if sigScriptTests[i].inputs[j].indexOutOfRange { + t.Errorf("at test %v", sigScriptTests[i].name) + idx = len(sigScriptTests[i].inputs) + } else { + idx = j + } + script, e = SignatureScript( + tx, idx, + sigScriptTests[i].inputs[j].txout.PkScript, + sigScriptTests[i].hashType, privKey, + sigScriptTests[i].compress, + ) + if (e == nil) != sigScriptTests[i].inputs[j].sigscriptGenerates { + if e == nil { + t.Errorf( + "passed test '%v' incorrectly", + sigScriptTests[i].name, + ) + } else { + t.Errorf( + "failed test '%v': %v", + sigScriptTests[i].name, e, + ) + } + continue nexttest + } + if !sigScriptTests[i].inputs[j].sigscriptGenerates { + // done with this test + continue nexttest + } + tx.TxIn[j].SignatureScript = script + } + // If testing using a correct sigscript but for an incorrect index, use last input script for first input. Requires > 0 inputs for test. + if sigScriptTests[i].scriptAtWrongIndex { + tx.TxIn[0].SignatureScript = script + sigScriptTests[i].inputs[0].inputValidates = false + } + // Validate tx input scripts + scriptFlags := ScriptBip16 | ScriptVerifyDERSignatures + for j := range tx.TxIn { + vm, e := NewEngine( + sigScriptTests[i]. + inputs[j].txout.PkScript, tx, j, scriptFlags, nil, nil, 0, + ) + if e != nil { + t.Errorf( + "cannot create script vm for test %v: %v", + sigScriptTests[i].name, e, + ) + continue nexttest + } + e = vm.Execute() + if (e == nil) != sigScriptTests[i].inputs[j].inputValidates { + if e == nil { + t.Errorf( + "passed test '%v' validation incorrectly: %v", + sigScriptTests[i].name, e, + ) + } else { + t.Errorf( + "failed test '%v' validation: %v", + sigScriptTests[i].name, e, + ) + } + continue nexttest + } + } + } +} diff --git a/pkg/txscript/consensus.go b/pkg/txscript/consensus.go new file mode 100644 index 0000000..09076e9 --- /dev/null +++ b/pkg/txscript/consensus.go @@ -0,0 +1,7 @@ +package txscript + +const ( + // LockTimeThreshold is the number below which a lock time is interpreted to be a block number. Since an average of + // one block is generated per 10 minutes, this allows blocks for about 9,512 years. + LockTimeThreshold = 5e8 // Tue Nov 5 00:53:20 1985 UTC +) diff --git a/pkg/txscript/data/LICENSE b/pkg/txscript/data/LICENSE new file mode 100755 index 0000000..547716e --- /dev/null +++ b/pkg/txscript/data/LICENSE @@ -0,0 +1,6 @@ +The json files in this directory come from the bitcoind project +(https://github.com/bitcoin/bitcoin) and is released under the following +license: + Copyright (c) 2012-2014 The Bitcoin Core developers + Distributed under the MIT/X11 software license, see the accompanying + file COPYING or http://www.opensource.org/licenses/mit-license.php. diff --git a/pkg/txscript/data/script_tests.json b/pkg/txscript/data/script_tests.json new file mode 100755 index 0000000..3c0734f --- /dev/null +++ b/pkg/txscript/data/script_tests.json @@ -0,0 +1,8230 @@ +[ + [ + "Format is: [[wit..., amount]?, scriptSig, scriptPubKey, flags, expected_scripterror, ... comments]" + ], + [ + "It is evaluated as if there was a crediting coinbase transaction with two 0" + ], + [ + "pushes as scriptSig, and one output of 0 satoshi and given scriptPubKey," + ], + [ + "followed by a spending transaction which spends this output as only input (and" + ], + [ + "correct prevout hash), using the given scriptSig. All nLockTimes are 0, all" + ], + [ + "nSequences are max." + ], + [ + "", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "Test the test: we should have an empty stack after scriptSig evaluation" + ], + [ + " ", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "and multiple spaces should not change that." + ], + [ + " ", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + " ", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 2", + "2 EQUALVERIFY 1 EQUAL", + "P2SH,STRICTENC", + "OK", + "Similarly whitespace around and between symbols" + ], + [ + "1 2", + "2 EQUALVERIFY 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + " 1 2", + "2 EQUALVERIFY 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 2 ", + "2 EQUALVERIFY 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + " 1 2 ", + "2 EQUALVERIFY 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x02 0x01 0x00", + "", + "P2SH,STRICTENC", + "OK", + "all bytes are significant, not only the last one" + ], + [ + "0x09 0x00000000 0x00000000 0x10", + "", + "P2SH,STRICTENC", + "OK", + "equals zero when cast to Int64" + ], + [ + "0x01 0x0b", + "11 EQUAL", + "P2SH,STRICTENC", + "OK", + "push 1 byte" + ], + [ + "0x02 0x417a", + "'Az' EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x4b 0x417a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a", + "'Azzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' EQUAL", + "P2SH,STRICTENC", + "OK", + "push 75 bytes" + ], + [ + "0x4c 0x01 0x07", + "7 EQUAL", + "P2SH,STRICTENC", + "OK", + "0x4c is OP_PUSHDATA1" + ], + [ + "0x4d 0x0100 0x08", + "8 EQUAL", + "P2SH,STRICTENC", + "OK", + "0x4d is OP_PUSHDATA2" + ], + [ + "0x4e 0x01000000 0x09", + "9 EQUAL", + "P2SH,STRICTENC", + "OK", + "0x4e is OP_PUSHDATA4" + ], + [ + "0x4c 0x00", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x4d 0x0000", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x4e 0x00000000", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x4f 1000 ADD", + "999 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0x50 ENDIF 1", + "P2SH,STRICTENC", + "OK", + "0x50 is reserved (ok if not executed)" + ], + [ + "0x51", + "0x5f ADD 0x60 EQUAL", + "P2SH,STRICTENC", + "OK", + "0x51 through 0x60 push 1 through 16 onto stack" + ], + [ + "1", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF VER ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK", + "VER non-functional (ok if not executed)" + ], + [ + "0", + "IF RESERVED RESERVED1 RESERVED2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK", + "RESERVED ok in un-executed IF" + ], + [ + "1", + "DUP IF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "IF 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "DUP IF ELSE ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "IF 1 ELSE ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1", + "IF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "IF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1", + "IF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "IF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "NOTIF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1", + "NOTIF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "NOTIF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1", + "NOTIF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0 ELSE 1 ELSE 0 ENDIF", + "P2SH,STRICTENC", + "OK", + "Multiple ELSE's are valid and executed inverts on each ELSE encountered" + ], + [ + "1", + "IF 1 ELSE 0 ELSE ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "IF ELSE 0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "IF 1 ELSE 0 ELSE 1 ENDIF ADD 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'' 1", + "IF SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ENDIF 0x14 0x68ca4fec736264c13b859bac43d5173df6871682 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "NOTIF 0 ELSE 1 ELSE 0 ENDIF", + "P2SH,STRICTENC", + "OK", + "Multiple ELSE's are valid and execution inverts on each ELSE encountered" + ], + [ + "0", + "NOTIF 1 ELSE 0 ELSE ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "NOTIF ELSE 0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "NOTIF 1 ELSE 0 ELSE 1 ENDIF ADD 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'' 0", + "NOTIF SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ELSE ELSE SHA1 ENDIF 0x14 0x68ca4fec736264c13b859bac43d5173df6871682 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 1 IF RETURN ELSE RETURN ELSE RETURN ENDIF ELSE 1 IF 1 ELSE RETURN ELSE 1 ENDIF ELSE RETURN ENDIF ADD 2 EQUAL", + "P2SH,STRICTENC", + "OK", + "Nested ELSE ELSE" + ], + [ + "1", + "NOTIF 0 NOTIF RETURN ELSE RETURN ELSE RETURN ENDIF ELSE 0 NOTIF 1 ELSE RETURN ELSE 1 ENDIF ELSE RETURN ENDIF ADD 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF RETURN ENDIF 1", + "P2SH,STRICTENC", + "OK", + "RETURN only works if executed" + ], + [ + "1 1", + "VERIFY", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0x05 0x01 0x00 0x00 0x00 0x00", + "VERIFY", + "P2SH,STRICTENC", + "OK", + "values >4 bytes can be cast to boolean" + ], + [ + "1 0x01 0x80", + "IF 0 ENDIF", + "P2SH,STRICTENC", + "OK", + "negative 0 is false" + ], + [ + "10 0 11 TOALTSTACK DROP FROMALTSTACK", + "ADD 21 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'gavin_was_here' TOALTSTACK 11 FROMALTSTACK", + "'gavin_was_here' EQUALVERIFY 11 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 IFDUP", + "DEPTH 1 EQUALVERIFY 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 IFDUP", + "DEPTH 2 EQUALVERIFY 1 EQUALVERIFY 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x05 0x0100000000 IFDUP", + "DEPTH 2 EQUALVERIFY 0x05 0x0100000000 EQUAL", + "P2SH,STRICTENC", + "OK", + "IFDUP dups non ints" + ], + [ + "0 DROP", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "DUP 1 ADD 1 EQUALVERIFY 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1", + "NIP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "OVER DEPTH 3 EQUALVERIFY", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "0 PICK 20 EQUALVERIFY DEPTH 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "1 PICK 21 EQUALVERIFY DEPTH 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "2 PICK 22 EQUALVERIFY DEPTH 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "0 ROLL 20 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "1 ROLL 21 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "2 ROLL 22 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "ROT 22 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "ROT DROP 20 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "ROT DROP DROP 21 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "ROT ROT 21 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "22 21 20", + "ROT ROT ROT 20 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 24 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT DROP 25 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 2DROP 20 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 2DROP DROP 21 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 2DROP 2DROP 22 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 2DROP 2DROP DROP 23 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 2ROT 22 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "25 24 23 22 21 20", + "2ROT 2ROT 2ROT 20 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "SWAP 1 EQUALVERIFY 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1", + "TUCK DEPTH 3 EQUALVERIFY SWAP 2DROP", + "P2SH,STRICTENC", + "OK" + ], + [ + "13 14", + "2DUP ROT EQUALVERIFY EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 0 1 2", + "3DUP DEPTH 7 EQUALVERIFY ADD ADD 3 EQUALVERIFY 2DROP 0 EQUALVERIFY", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 2 3 5", + "2OVER ADD ADD 8 EQUALVERIFY ADD ADD 6 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 3 5 7", + "2SWAP ADD 4 EQUALVERIFY ADD 12 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "SIZE 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "SIZE 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "127", + "SIZE 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "128", + "SIZE 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "32767", + "SIZE 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "32768", + "SIZE 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "8388607", + "SIZE 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "8388608", + "SIZE 4 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647", + "SIZE 4 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483648", + "SIZE 5 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "549755813887", + "SIZE 5 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "549755813888", + "SIZE 6 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "9223372036854775807", + "SIZE 8 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1", + "SIZE 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-127", + "SIZE 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-128", + "SIZE 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-32767", + "SIZE 2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-32768", + "SIZE 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-8388607", + "SIZE 3 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-8388608", + "SIZE 4 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-2147483647", + "SIZE 4 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-2147483648", + "SIZE 5 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-549755813887", + "SIZE 5 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-549755813888", + "SIZE 6 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-9223372036854775807", + "SIZE 8 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'abcdefghijklmnopqrstuvwxyz'", + "SIZE 26 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "42", + "SIZE 1 EQUALVERIFY 42 EQUAL", + "P2SH,STRICTENC", + "OK", + "SIZE does not consume argument" + ], + [ + "2 -2 ADD", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647 -2147483647 ADD", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 -1 ADD", + "-2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1 ADD", + "2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1ADD", + "2 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "111 1SUB", + "110 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "111 1 ADD 12 SUB", + "100 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 ABS", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "16 ABS", + "16 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-16 ABS", + "-16 NEGATE EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 NOT", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 NOT", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 NOT", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0NOTEQUAL", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0NOTEQUAL", + "1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "111 0NOTEQUAL", + "1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-111 0NOTEQUAL", + "1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1 BOOLAND", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 BOOLAND", + "NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1 BOOLAND", + "NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0 BOOLAND", + "NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "16 17 BOOLAND", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 1 BOOLOR", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 BOOLOR", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1 BOOLOR", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0 BOOLOR", + "NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "16 17 BOOLOR", + "NOP", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10 1 ADD", + "NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10 1 ADD", + "NUMEQUALVERIFY 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10 1 ADD", + "NUMNOTEQUAL NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "111 10 1 ADD", + "NUMNOTEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10", + "LESSTHAN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "4 4", + "LESSTHAN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "10 11", + "LESSTHAN", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 11", + "LESSTHAN", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 -10", + "LESSTHAN", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10", + "GREATERTHAN", + "P2SH,STRICTENC", + "OK" + ], + [ + "4 4", + "GREATERTHAN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "10 11", + "GREATERTHAN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 11", + "GREATERTHAN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 -10", + "GREATERTHAN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10", + "LESSTHANOREQUAL NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "4 4", + "LESSTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "10 11", + "LESSTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 11", + "LESSTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 -10", + "LESSTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 10", + "GREATERTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "4 4", + "GREATERTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "10 11", + "GREATERTHANOREQUAL NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 11", + "GREATERTHANOREQUAL NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "-11 -10", + "GREATERTHANOREQUAL NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 MIN", + "0 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1 MIN", + "0 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 0 MIN", + "-1 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 -2147483647 MIN", + "-2147483647 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647 0 MAX", + "2147483647 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 100 MAX", + "100 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-100 0 MAX", + "0 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 -2147483647 MAX", + "0 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0 1", + "WITHIN", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 1", + "WITHIN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 -2147483647 2147483647", + "WITHIN", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 -100 100", + "WITHIN", + "P2SH,STRICTENC", + "OK" + ], + [ + "11 -100 100", + "WITHIN", + "P2SH,STRICTENC", + "OK" + ], + [ + "-2147483647 -100 100", + "WITHIN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647 -100 100", + "WITHIN NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647 2147483647 SUB", + "0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647 DUP ADD", + "4294967294 EQUAL", + "P2SH,STRICTENC", + "OK", + ">32 bit EQUAL is valid" + ], + [ + "2147483647 NEGATE DUP ADD", + "-4294967294 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "RIPEMD160 0x14 0x9c1185a5c5e9fc54612808977ee8f548b2258d31 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'a'", + "RIPEMD160 0x14 0x0bdc9d2d256b3ee9daae347be6f4dc835a467ffe EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'abcdefghijklmnopqrstuvwxyz'", + "RIPEMD160 0x14 0xf71c27109c692c1b56bbdceb5b9d2865b3708dbc EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "SHA1 0x14 0xda39a3ee5e6b4b0d3255bfef95601890afd80709 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'a'", + "SHA1 0x14 0x86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'abcdefghijklmnopqrstuvwxyz'", + "SHA1 0x14 0x32d10c7b8cf96570ca04ce37f2a19d84240d3a89 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "SHA256 0x20 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'a'", + "SHA256 0x20 0xca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'abcdefghijklmnopqrstuvwxyz'", + "SHA256 0x20 0x71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "DUP HASH160 SWAP SHA256 RIPEMD160 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "DUP HASH256 SWAP SHA256 SHA256 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "NOP HASH160 0x14 0xb472a266d0bd89c13706a4132ccfb16f7c3b9fcb EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'a'", + "HASH160 NOP 0x14 0x994355199e516ff76c4fa4aab39337b9d84cf12b EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'abcdefghijklmnopqrstuvwxyz'", + "HASH160 0x4c 0x14 0xc286a1af0947f58d1ad787385b1c2c4a976f9e71 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "''", + "HASH256 0x20 0x5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'a'", + "HASH256 0x20 0xbf5d3affb73efd2ec6c36ad3112dd933efed63c4e1cbffcfa88e2759c144f2d8 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'abcdefghijklmnopqrstuvwxyz'", + "HASH256 0x4c 0x20 0xca139bc10c2f660da42666f72e89a225936fc60f193c161124a672050c434671 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10 1 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "'NOP_1_to_10' NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10", + "'NOP_1_to_10' EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "NOP", + "P2SH,STRICTENC,DISCOURAGE_UPGRADABLE_NOPS", + "OK", + "Discourage NOPx flag allows OP_NOP" + ], + [ + "0", + "IF NOP10 ENDIF 1", + "P2SH,STRICTENC,DISCOURAGE_UPGRADABLE_NOPS", + "OK", + "Discouraged NOPs are allowed if not executed" + ], + [ + "0", + "IF 0xba ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK", + "opcodes above NOP10 invalid if executed" + ], + [ + "0", + "IF 0xbb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xbc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xbd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xbe ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xbf ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xc9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xca ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xcb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xcc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xcd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xce ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xcf ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xd9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xda ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xdb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xdc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xdd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xde ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xdf ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xe9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xea ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xeb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xec ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xed ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xee ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xef ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xf9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xfa ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xfb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xfc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xfd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xfe ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "IF 0xff ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'", + "P2SH,STRICTENC", + "OK", + "520 byte push" + ], + [ + "1", + "0x616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + "P2SH,STRICTENC", + "OK", + "201 opcodes executed. 0x61 is NOP" + ], + [ + "1 2 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "1 2 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "P2SH,STRICTENC", + "OK", + "1,000 stack size (0x6f is 3DUP)" + ], + [ + "1 TOALTSTACK 2 TOALTSTACK 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "1 2 3 4 5 6 7 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "P2SH,STRICTENC", + "OK", + "1,000 stack size (altstack cleared between scriptSig/scriptPubKey)" + ], + [ + "'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f 2DUP 0x616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + "P2SH,STRICTENC", + "OK", + "Max-size (10,000-byte), max-push(520 bytes), max-opcodes(201), max stack size(1,000 items). 0x6f is 3DUP, 0x61 is NOP" + ], + [ + "0", + "IF 0x5050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050505050 ENDIF 1", + "P2SH,STRICTENC", + "OK", + ">201 opcodes, but RESERVED (0x50) doesn't count towards opcode limit." + ], + [ + "NOP", + "1", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "0x01 0x01 EQUAL", + "P2SH,STRICTENC", + "OK", + "The following is useful for checking implementations of BN_bn2mpi" + ], + [ + "127", + "0x01 0x7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "128", + "0x02 0x8000 EQUAL", + "P2SH,STRICTENC", + "OK", + "Leave room for the sign bit" + ], + [ + "32767", + "0x02 0xFF7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "32768", + "0x03 0x008000 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "8388607", + "0x03 0xFFFF7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "8388608", + "0x04 0x00008000 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647", + "0x04 0xFFFFFF7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483648", + "0x05 0x0000008000 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "549755813887", + "0x05 0xFFFFFFFF7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "549755813888", + "0x06 0xFFFFFFFF7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "9223372036854775807", + "0x08 0xFFFFFFFFFFFFFF7F EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1", + "0x01 0x81 EQUAL", + "P2SH,STRICTENC", + "OK", + "Numbers are little-endian with the MSB being a sign bit" + ], + [ + "-127", + "0x01 0xFF EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-128", + "0x02 0x8080 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-32767", + "0x02 0xFFFF EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-32768", + "0x03 0x008080 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-8388607", + "0x03 0xFFFFFF EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-8388608", + "0x04 0x00008080 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-2147483647", + "0x04 0xFFFFFFFF EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-2147483648", + "0x05 0x0000008080 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-4294967295", + "0x05 0xFFFFFFFF80 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-549755813887", + "0x05 0xFFFFFFFFFF EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-549755813888", + "0x06 0x000000008080 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-9223372036854775807", + "0x08 0xFFFFFFFFFFFFFFFF EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "2147483647", + "1ADD 2147483648 EQUAL", + "P2SH,STRICTENC", + "OK", + "We can do math on 4-byte integers, and compare 5-byte ones" + ], + [ + "2147483647", + "1ADD 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "-2147483647", + "1ADD 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "0x02 0x0100 EQUAL NOT", + "P2SH,STRICTENC", + "OK", + "Not the same byte array..." + ], + [ + "1", + "0x02 0x0100 NUMEQUAL", + "P2SH,STRICTENC", + "OK", + "... but they are numerically equal" + ], + [ + "11", + "0x4c 0x03 0x0b0000 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "0x01 0x80 EQUAL NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "0x01 0x80 NUMEQUAL", + "P2SH,STRICTENC", + "OK", + "Zero numerically equals negative zero" + ], + [ + "0", + "0x02 0x0080 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x03 0x000080", + "0x04 0x00000080 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x03 0x100080", + "0x04 0x10000080 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x03 0x100000", + "0x04 0x10000000 NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP 1", + "P2SH,STRICTENC", + "OK", + "The following tests check the if(stack.size() < N) tests in each opcode" + ], + [ + "1", + "IF 1 ENDIF", + "P2SH,STRICTENC", + "OK", + "They are here to catch copy-and-paste errors" + ], + [ + "0", + "NOTIF 1 ENDIF", + "P2SH,STRICTENC", + "OK", + "Most of them are duplicated elsewhere," + ], + [ + "1", + "VERIFY 1", + "P2SH,STRICTENC", + "OK", + "but, hey, more is always better, right?" + ], + [ + "0", + "TOALTSTACK 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "TOALTSTACK FROMALTSTACK", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "2DROP 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1", + "2DUP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0 1", + "3DUP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1 0 0", + "2OVER", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1 0 0 0 0", + "2ROT", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1 0 0", + "2SWAP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "IFDUP", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "DEPTH 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "DROP 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "DUP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1", + "NIP", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "OVER", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 0 0 3", + "PICK", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "PICK", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 0 0 3", + "ROLL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "ROLL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0 0", + "ROT", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "SWAP", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 1", + "TUCK", + "P2SH,STRICTENC", + "OK" + ], + [ + "1", + "SIZE", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "EQUALVERIFY 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0 1", + "EQUAL EQUAL", + "P2SH,STRICTENC", + "OK", + "OP_0 and bools must have identical byte representations" + ], + [ + "0", + "1ADD", + "P2SH,STRICTENC", + "OK" + ], + [ + "2", + "1SUB", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1", + "NEGATE", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1", + "ABS", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "NOT", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1", + "0NOTEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "ADD", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "SUB", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 -1", + "BOOLAND", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 0", + "BOOLOR", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "NUMEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "NUMEQUALVERIFY 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 0", + "NUMNOTEQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 0", + "LESSTHAN", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "GREATERTHAN", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "LESSTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0", + "GREATERTHANOREQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 0", + "MIN", + "P2SH,STRICTENC", + "OK" + ], + [ + "1 0", + "MAX", + "P2SH,STRICTENC", + "OK" + ], + [ + "-1 -1 0", + "WITHIN", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "RIPEMD160", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "SHA1", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "SHA256", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "HASH160", + "P2SH,STRICTENC", + "OK" + ], + [ + "0", + "HASH256", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "CODESEPARATOR 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP1 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "CHECKLOCKTIMEVERIFY 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "CHECKSEQUENCEVERIFY 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP4 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP5 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP6 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP7 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP8 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP9 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "NOP", + "NOP10 1", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 0 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "CHECKMULTISIG is allowed to have zero keys and/or sigs" + ], + [ + "", + "0 0 0 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 0 1 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "Zero sigs means no sigs are checked" + ], + [ + "", + "0 0 0 1 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 0 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "CHECKMULTISIG is allowed to have zero keys and/or sigs" + ], + [ + "", + "0 0 0 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 0 1 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "Zero sigs means no sigs are checked" + ], + [ + "", + "0 0 0 1 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 2 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "Test from up to 20 pubkeys, all not checked" + ], + [ + "", + "0 0 'a' 'b' 'c' 3 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 4 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 5 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 6 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 7 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 8 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 9 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 10 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 11 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 12 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 13 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 14 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 15 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 16 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 17 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 18 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 19 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG VERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 1 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 2 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 3 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 4 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 5 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 6 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 7 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 8 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 9 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 10 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 11 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 12 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 13 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 14 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 15 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 16 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 17 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 18 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 19 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "0 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG", + "P2SH,STRICTENC", + "OK", + "nOpCount is incremented by the number of keys evaluated in addition to the usual one op per op. In this case we have zero keys, so we can execute 201 CHECKMULTISIGS" + ], + [ + "1", + "0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY 0 0 0 CHECKMULTISIGVERIFY", + "P2SH,STRICTENC", + "OK" + ], + [ + "", + "NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG", + "P2SH,STRICTENC", + "OK", + "Even though there are no signatures being checked nOpCount is incremented by the number of keys." + ], + [ + "1", + "NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY", + "P2SH,STRICTENC", + "OK" + ], + [ + "0 0x01 1", + "HASH160 0x14 0xda1745e9b549bd0bfa1a569971c77eba30cd5a4b EQUAL", + "P2SH,STRICTENC", + "OK", + "Very basic P2SH" + ], + [ + "0x4c 0 0x01 1", + "HASH160 0x14 0xda1745e9b549bd0bfa1a569971c77eba30cd5a4b EQUAL", + "P2SH,STRICTENC", + "OK" + ], + [ + "0x40 0x42424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242", + "0x4d 0x4000 0x42424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242 EQUAL", + "P2SH,STRICTENC", + "OK", + "Basic PUSH signedness check" + ], + [ + "0x4c 0x40 0x42424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242", + "0x4d 0x4000 0x42424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242 EQUAL", + "P2SH,STRICTENC", + "OK", + "Basic PUSHDATA1 signedness check" + ], + [ + "all PUSHDATA forms are equivalent" + ], + [ + "0x4c 0x4b 0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "0x4b 0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 EQUAL", + "", + "OK", + "PUSHDATA1 of 75 bytes equals direct push of it" + ], + [ + "0x4d 0xFF00 0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "0x4c 0xFF 0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 EQUAL", + "", + "OK", + "PUSHDATA2 of 255 bytes equals PUSHDATA1 of it" + ], + [ + "0x00", + "SIZE 0 EQUAL", + "P2SH,STRICTENC", + "OK", + "Basic OP_0 execution" + ], + [ + "Numeric pushes" + ], + [ + "0x01 0x81", + "0x4f EQUAL", + "", + "OK", + "OP1_NEGATE pushes 0x81" + ], + [ + "0x01 0x01", + "0x51 EQUAL", + "", + "OK", + "OP_1 pushes 0x01" + ], + [ + "0x01 0x02", + "0x52 EQUAL", + "", + "OK", + "OP_2 pushes 0x02" + ], + [ + "0x01 0x03", + "0x53 EQUAL", + "", + "OK", + "OP_3 pushes 0x03" + ], + [ + "0x01 0x04", + "0x54 EQUAL", + "", + "OK", + "OP_4 pushes 0x04" + ], + [ + "0x01 0x05", + "0x55 EQUAL", + "", + "OK", + "OP_5 pushes 0x05" + ], + [ + "0x01 0x06", + "0x56 EQUAL", + "", + "OK", + "OP_6 pushes 0x06" + ], + [ + "0x01 0x07", + "0x57 EQUAL", + "", + "OK", + "OP_7 pushes 0x07" + ], + [ + "0x01 0x08", + "0x58 EQUAL", + "", + "OK", + "OP_8 pushes 0x08" + ], + [ + "0x01 0x09", + "0x59 EQUAL", + "", + "OK", + "OP_9 pushes 0x09" + ], + [ + "0x01 0x0a", + "0x5a EQUAL", + "", + "OK", + "OP_10 pushes 0x0a" + ], + [ + "0x01 0x0b", + "0x5b EQUAL", + "", + "OK", + "OP_11 pushes 0x0b" + ], + [ + "0x01 0x0c", + "0x5c EQUAL", + "", + "OK", + "OP_12 pushes 0x0c" + ], + [ + "0x01 0x0d", + "0x5d EQUAL", + "", + "OK", + "OP_13 pushes 0x0d" + ], + [ + "0x01 0x0e", + "0x5e EQUAL", + "", + "OK", + "OP_14 pushes 0x0e" + ], + [ + "0x01 0x0f", + "0x5f EQUAL", + "", + "OK", + "OP_15 pushes 0x0f" + ], + [ + "0x01 0x10", + "0x60 EQUAL", + "", + "OK", + "OP_16 pushes 0x10" + ], + [ + "Equivalency of different numeric encodings" + ], + [ + "0x02 0x8000", + "128 NUMEQUAL", + "", + "OK", + "0x8000 equals 128" + ], + [ + "0x01 0x00", + "0 NUMEQUAL", + "", + "OK", + "0x00 numequals 0" + ], + [ + "0x01 0x80", + "0 NUMEQUAL", + "", + "OK", + "0x80 (negative zero) numequals 0" + ], + [ + "0x02 0x0080", + "0 NUMEQUAL", + "", + "OK", + "0x0080 numequals 0" + ], + [ + "0x02 0x0500", + "5 NUMEQUAL", + "", + "OK", + "0x0500 numequals 5" + ], + [ + "0x03 0xff7f80", + "0x02 0xffff NUMEQUAL", + "", + "OK", + "" + ], + [ + "0x03 0xff7f00", + "0x02 0xff7f NUMEQUAL", + "", + "OK", + "" + ], + [ + "0x04 0xffff7f80", + "0x03 0xffffff NUMEQUAL", + "", + "OK", + "" + ], + [ + "0x04 0xffff7f00", + "0x03 0xffff7f NUMEQUAL", + "", + "OK", + "" + ], + [ + "Unevaluated non-minimal pushes are ignored" + ], + [ + "0 IF 0x4c 0x00 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "non-minimal PUSHDATA1 ignored" + ], + [ + "0 IF 0x4d 0x0000 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "non-minimal PUSHDATA2 ignored" + ], + [ + "0 IF 0x4c 0x00000000 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "non-minimal PUSHDATA4 ignored" + ], + [ + "0 IF 0x01 0x81 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "1NEGATE equiv" + ], + [ + "0 IF 0x01 0x01 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_1 equiv" + ], + [ + "0 IF 0x01 0x02 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_2 equiv" + ], + [ + "0 IF 0x01 0x03 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_3 equiv" + ], + [ + "0 IF 0x01 0x04 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_4 equiv" + ], + [ + "0 IF 0x01 0x05 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_5 equiv" + ], + [ + "0 IF 0x01 0x06 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_6 equiv" + ], + [ + "0 IF 0x01 0x07 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_7 equiv" + ], + [ + "0 IF 0x01 0x08 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_8 equiv" + ], + [ + "0 IF 0x01 0x09 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_9 equiv" + ], + [ + "0 IF 0x01 0x0a ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_10 equiv" + ], + [ + "0 IF 0x01 0x0b ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_11 equiv" + ], + [ + "0 IF 0x01 0x0c ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_12 equiv" + ], + [ + "0 IF 0x01 0x0d ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_13 equiv" + ], + [ + "0 IF 0x01 0x0e ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_14 equiv" + ], + [ + "0 IF 0x01 0x0f ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_15 equiv" + ], + [ + "0 IF 0x01 0x10 ENDIF 1", + "", + "MINIMALDATA", + "OK", + "OP_16 equiv" + ], + [ + "Numeric minimaldata rules are only applied when a stack item is numerically evaluated; the push itself is allowed" + ], + [ + "0x01 0x00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x01 0x80", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0180", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0100", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0200", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0300", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0400", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0500", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0600", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0700", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0800", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0900", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0a00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0b00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0c00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0d00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0e00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x0f00", + "1", + "MINIMALDATA", + "OK" + ], + [ + "0x02 0x1000", + "1", + "MINIMALDATA", + "OK" + ], + [ + "Valid version of the 'Test every numeric-accepting opcode for correct handling of the numeric minimal encoding rule' script_invalid test" + ], + [ + "1 0x02 0x0000", + "PICK DROP", + "", + "OK" + ], + [ + "1 0x02 0x0000", + "ROLL DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000", + "1ADD DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000", + "1SUB DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000", + "NEGATE DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000", + "ABS DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000", + "NOT DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000", + "0NOTEQUAL DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "ADD DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "ADD DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "SUB DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "SUB DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "BOOLAND DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "BOOLAND DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "BOOLOR DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "BOOLOR DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "NUMEQUAL DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 1", + "NUMEQUAL DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "NUMEQUALVERIFY 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "NUMEQUALVERIFY 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "NUMNOTEQUAL DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "NUMNOTEQUAL DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "LESSTHAN DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "LESSTHAN DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "GREATERTHAN DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "GREATERTHAN DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "LESSTHANOREQUAL DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "LESSTHANOREQUAL DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "GREATERTHANOREQUAL DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "GREATERTHANOREQUAL DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "MIN DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "MIN DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000", + "MAX DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0", + "MAX DROP 1", + "", + "OK" + ], + [ + "0x02 0x0000 0 0", + "WITHIN DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000 0", + "WITHIN DROP 1", + "", + "OK" + ], + [ + "0 0 0x02 0x0000", + "WITHIN DROP 1", + "", + "OK" + ], + [ + "0 0 0x02 0x0000", + "CHECKMULTISIG DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000 0", + "CHECKMULTISIG DROP 1", + "", + "OK" + ], + [ + "0 0x02 0x0000 0 1", + "CHECKMULTISIG DROP 1", + "", + "OK" + ], + [ + "0 0 0x02 0x0000", + "CHECKMULTISIGVERIFY 1", + "", + "OK" + ], + [ + "0 0x02 0x0000 0", + "CHECKMULTISIGVERIFY 1", + "", + "OK" + ], + [ + "While not really correctly DER encoded, the empty signature is allowed by" + ], + [ + "STRICTENC to provide a compact way to provide a delibrately invalid signature." + ], + [ + "0", + "0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 CHECKSIG NOT", + "STRICTENC", + "OK" + ], + [ + "0 0", + "1 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 1 CHECKMULTISIG NOT", + "STRICTENC", + "OK" + ], + [ + "CHECKMULTISIG evaluation order tests. CHECKMULTISIG evaluates signatures and" + ], + [ + "pubkeys in a specific order, and will exit early if the number of signatures" + ], + [ + "left to check is greater than the number of keys left. As STRICTENC fails the" + ], + [ + "script when it reaches an invalidly encoded signature or pubkey, we can use it" + ], + [ + "to test the exact order in which signatures and pubkeys are evaluated by" + ], + [ + "distinguishing CHECKMULTISIG returning false on the stack and the script as a" + ], + [ + "whole failing." + ], + [ + "See also the corresponding inverted versions of these tests in script_invalid.json" + ], + [ + "0 0x47 0x3044022044dc17b0887c161bb67ba9635bf758735bdde503e4b0a0987f587f14a4e1143d022009a215772d49a85dae40d8ca03955af26ad3978a0ff965faa12915e9586249a501 0x47 0x3044022044dc17b0887c161bb67ba9635bf758735bdde503e4b0a0987f587f14a4e1143d022009a215772d49a85dae40d8ca03955af26ad3978a0ff965faa12915e9586249a501", + "2 0 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 2 CHECKMULTISIG NOT", + "STRICTENC", + "OK", + "2-of-2 CHECKMULTISIG NOT with the second pubkey invalid, and both signatures validly encoded. Valid pubkey fails, and CHECKMULTISIG exits early, prior to evaluation of second invalid pubkey." + ], + [ + "0 0 0x47 0x3044022044dc17b0887c161bb67ba9635bf758735bdde503e4b0a0987f587f14a4e1143d022009a215772d49a85dae40d8ca03955af26ad3978a0ff965faa12915e9586249a501", + "2 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 2 CHECKMULTISIG NOT", + "STRICTENC", + "OK", + "2-of-2 CHECKMULTISIG NOT with both pubkeys valid, but second signature invalid. Valid pubkey fails, and CHECKMULTISIG exits early, prior to evaluation of second invalid signature." + ], + [ + "Increase test coverage for DERSIG" + ], + [ + "0x4a 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0 CHECKSIG NOT", + "", + "OK", + "Overly long signature is correctly encoded" + ], + [ + "0x25 0x30220220000000000000000000000000000000000000000000000000000000000000000000", + "0 CHECKSIG NOT", + "", + "OK", + "Missing S is correctly encoded" + ], + [ + "0x27 0x3024021077777777777777777777777777777777020a7777777777777777777777777777777701", + "0 CHECKSIG NOT", + "", + "OK", + "S with invalid S length is correctly encoded" + ], + [ + "0x27 0x302403107777777777777777777777777777777702107777777777777777777777777777777701", + "0 CHECKSIG NOT", + "", + "OK", + "Non-integer R is correctly encoded" + ], + [ + "0x27 0x302402107777777777777777777777777777777703107777777777777777777777777777777701", + "0 CHECKSIG NOT", + "", + "OK", + "Non-integer S is correctly encoded" + ], + [ + "0x17 0x3014020002107777777777777777777777777777777701", + "0 CHECKSIG NOT", + "", + "OK", + "Zero-length R is correctly encoded" + ], + [ + "0x17 0x3014021077777777777777777777777777777777020001", + "0 CHECKSIG NOT", + "", + "OK", + "Zero-length S is correctly encoded for DERSIG" + ], + [ + "0x27 0x302402107777777777777777777777777777777702108777777777777777777777777777777701", + "0 CHECKSIG NOT", + "", + "OK", + "Negative S is correctly encoded" + ], + [ + "2147483648", + "CHECKSEQUENCEVERIFY", + "CHECKSEQUENCEVERIFY", + "OK", + "CSV passes if stack top bit 1 << 31 is set" + ], + [ + "", + "DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE", + "Test the test: we should have an empty stack after scriptSig evaluation" + ], + [ + " ", + "DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE", + "and multiple spaces should not change that." + ], + [ + " ", + "DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + " ", + "DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "", + "", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "", + "NOP", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "", + "NOP DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "NOP", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "NOP DEPTH", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "DEPTH", + "", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0x4c01", + "0x01 NOP", + "P2SH,STRICTENC", + "BAD_OPCODE", + "PUSHDATA1 with not enough bytes" + ], + [ + "0x4d0200ff", + "0x01 NOP", + "P2SH,STRICTENC", + "BAD_OPCODE", + "PUSHDATA2 with not enough bytes" + ], + [ + "0x4e03000000ffff", + "0x01 NOP", + "P2SH,STRICTENC", + "BAD_OPCODE", + "PUSHDATA4 with not enough bytes" + ], + [ + "1", + "IF 0x50 ENDIF 1", + "P2SH,STRICTENC", + "BAD_OPCODE", + "0x50 is reserved" + ], + [ + "0x52", + "0x5f ADD 0x60 EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE", + "0x51 through 0x60 push 1 through 16 onto stack" + ], + [ + "0", + "NOP", + "P2SH,STRICTENC", + "EVAL_FALSE", + "" + ], + [ + "1", + "IF VER ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "VER non-functional" + ], + [ + "0", + "IF VERIF ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "VERIF illegal everywhere" + ], + [ + "0", + "IF ELSE 1 ELSE VERIF ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "VERIF illegal everywhere" + ], + [ + "0", + "IF VERNOTIF ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "VERNOTIF illegal everywhere" + ], + [ + "0", + "IF ELSE 1 ELSE VERNOTIF ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "VERNOTIF illegal everywhere" + ], + [ + "1 IF", + "1 ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "IF/ENDIF can't span scriptSig/scriptPubKey" + ], + [ + "1 IF 0 ENDIF", + "1 ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1 ELSE 0 ENDIF", + "1", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "0 NOTIF", + "123", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "0", + "DUP IF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0", + "IF 1 ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0", + "DUP IF ELSE ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0", + "IF 1 ELSE ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0", + "NOTIF ELSE 1 ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0 1", + "IF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0 0", + "IF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1 0", + "IF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0 1", + "IF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0 0", + "NOTIF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0 1", + "NOTIF IF 1 ELSE 0 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1 1", + "NOTIF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "0 0", + "NOTIF IF 1 ELSE 0 ENDIF ELSE IF 0 ELSE 1 ENDIF ENDIF", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1", + "IF RETURN ELSE ELSE 1 ENDIF", + "P2SH,STRICTENC", + "OP_RETURN", + "Multiple ELSEs" + ], + [ + "1", + "IF 1 ELSE ELSE RETURN ENDIF", + "P2SH,STRICTENC", + "OP_RETURN" + ], + [ + "1", + "ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "Malformed IF/ELSE/ENDIF sequence" + ], + [ + "1", + "ELSE ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "ENDIF ELSE", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "ENDIF ELSE IF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "IF ELSE ENDIF ELSE", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "IF ELSE ENDIF ELSE ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "IF ENDIF ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "IF ELSE ELSE ENDIF ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL" + ], + [ + "1", + "RETURN", + "P2SH,STRICTENC", + "OP_RETURN" + ], + [ + "1", + "DUP IF RETURN ENDIF", + "P2SH,STRICTENC", + "OP_RETURN" + ], + [ + "1", + "RETURN 'data'", + "P2SH,STRICTENC", + "OP_RETURN", + "canonical prunable txout format" + ], + [ + "0 IF", + "RETURN ENDIF 1", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "still prunable because IF/ENDIF can't span scriptSig/scriptPubKey" + ], + [ + "0", + "VERIFY 1", + "P2SH,STRICTENC", + "VERIFY" + ], + [ + "1", + "VERIFY", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1", + "VERIFY 0", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1 TOALTSTACK", + "FROMALTSTACK 1", + "P2SH,STRICTENC", + "INVALID_ALTSTACK_OPERATION", + "alt stack not shared between sig/pubkey" + ], + [ + "IFDUP", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "DROP", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "DUP", + "DEPTH 0 EQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "DUP 1 ADD 2 EQUALVERIFY 0 EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "NIP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "1 NIP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "1 0 NIP", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "OVER 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "OVER", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "0 1", + "OVER DEPTH 3 EQUALVERIFY", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "19 20 21", + "PICK 19 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "0 PICK", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "-1 PICK", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "19 20 21", + "0 PICK 20 EQUALVERIFY DEPTH 3 EQUAL", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "19 20 21", + "1 PICK 21 EQUALVERIFY DEPTH 3 EQUAL", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "19 20 21", + "2 PICK 22 EQUALVERIFY DEPTH 3 EQUAL", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "NOP", + "0 ROLL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "-1 ROLL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "19 20 21", + "0 ROLL 20 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "19 20 21", + "1 ROLL 21 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "19 20 21", + "2 ROLL 22 EQUALVERIFY DEPTH 2 EQUAL", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "NOP", + "ROT 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "1 ROT 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "1 2 ROT 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "0 1 2 ROT", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "SWAP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "SWAP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "0 1", + "SWAP 1 EQUALVERIFY", + "P2SH,STRICTENC", + "EQUALVERIFY" + ], + [ + "NOP", + "TUCK 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "TUCK 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 0", + "TUCK DEPTH 3 EQUALVERIFY SWAP 2DROP", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "NOP", + "2DUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "2DUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "3DUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "3DUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 2", + "3DUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "2OVER 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "2 3 2OVER 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "2SWAP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "2 3 2SWAP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "'a' 'b'", + "CAT", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "CAT disabled" + ], + [ + "'a' 'b' 0", + "IF CAT ELSE 1 ENDIF", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "CAT disabled" + ], + [ + "'abc' 1 1", + "SUBSTR", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "SUBSTR disabled" + ], + [ + "'abc' 1 1 0", + "IF SUBSTR ELSE 1 ENDIF", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "SUBSTR disabled" + ], + [ + "'abc' 2 0", + "IF LEFT ELSE 1 ENDIF", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "LEFT disabled" + ], + [ + "'abc' 2 0", + "IF RIGHT ELSE 1 ENDIF", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "RIGHT disabled" + ], + [ + "NOP", + "SIZE 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "'abc'", + "IF INVERT ELSE 1 ENDIF", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "INVERT disabled" + ], + [ + "1 2 0 IF AND ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "AND disabled" + ], + [ + "1 2 0 IF OR ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "OR disabled" + ], + [ + "1 2 0 IF XOR ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "XOR disabled" + ], + [ + "2 0 IF 2MUL ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "2MUL disabled" + ], + [ + "2 0 IF 2DIV ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "2DIV disabled" + ], + [ + "2 2 0 IF MUL ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "MUL disabled" + ], + [ + "2 2 0 IF DIV ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "DIV disabled" + ], + [ + "2 2 0 IF MOD ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "MOD disabled" + ], + [ + "2 2 0 IF LSHIFT ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "LSHIFT disabled" + ], + [ + "2 2 0 IF RSHIFT ELSE 1 ENDIF", + "NOP", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "RSHIFT disabled" + ], + [ + "", + "EQUAL NOT", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION", + "EQUAL must error when there are no stack items" + ], + [ + "0", + "EQUAL NOT", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION", + "EQUAL must error when there are not 2 stack items" + ], + [ + "0 1", + "EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1 1 ADD", + "0 EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "11 1 ADD 12 SUB", + "11 EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "2147483648 0 ADD", + "NOP", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "arithmetic operands must be in range [-2^31...2^31] " + ], + [ + "-2147483648 0 ADD", + "NOP", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "arithmetic operands must be in range [-2^31...2^31] " + ], + [ + "2147483647 DUP ADD", + "4294967294 NUMEQUAL", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "NUMEQUAL must be in numeric range" + ], + [ + "'abcdef' NOT", + "0 EQUAL", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "NOT is an arithmetic operand" + ], + [ + "2 DUP MUL", + "4 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "2 DUP DIV", + "1 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "2 2MUL", + "4 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "2 2DIV", + "1 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "7 3 MOD", + "1 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "2 2 LSHIFT", + "8 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "2 1 RSHIFT", + "1 EQUAL", + "P2SH,STRICTENC", + "DISABLED_OPCODE", + "disabled" + ], + [ + "1", + "NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10 2 EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "'NOP_1_to_10' NOP1 CHECKLOCKTIMEVERIFY CHECKSEQUENCEVERIFY NOP4 NOP5 NOP6 NOP7 NOP8 NOP9 NOP10", + "'NOP_1_to_11' EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "Ensure 100% coverage of discouraged NOPS" + ], + [ + "1", + "NOP1", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "CHECKLOCKTIMEVERIFY", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "CHECKSEQUENCEVERIFY", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP4", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP5", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP6", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP7", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP8", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP9", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "1", + "NOP10", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS" + ], + [ + "NOP10", + "1", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS", + "Discouraged NOP10 in scriptSig" + ], + [ + "1 0x01 0xb9", + "HASH160 0x14 0x15727299b05b45fdaf9ac9ecf7565cfe27c3e567 EQUAL", + "P2SH,DISCOURAGE_UPGRADABLE_NOPS", + "DISCOURAGE_UPGRADABLE_NOPS", + "Discouraged NOP10 in redeemScript" + ], + [ + "0x50", + "1", + "P2SH,STRICTENC", + "BAD_OPCODE", + "opcode 0x50 is reserved" + ], + [ + "1", + "IF 0xba ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "opcodes above NOP10 invalid if executed" + ], + [ + "1", + "IF 0xbb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xbc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xbd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xbe ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xbf ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xc9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xca ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xcb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xcc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xcd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xce ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xcf ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xd9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xda ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xdb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xdc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xdd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xde ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xdf ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xe9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xea ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xeb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xec ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xed ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xee ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xef ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf0 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf1 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf2 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf3 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf4 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf5 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf6 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf7 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf8 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xf9 ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xfa ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xfb ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xfc ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xfd ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xfe ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1", + "IF 0xff ELSE 1 ENDIF", + "P2SH,STRICTENC", + "BAD_OPCODE" + ], + [ + "1 IF 1 ELSE", + "0xff ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "invalid because scriptSig and scriptPubKey are processed separately" + ], + [ + "NOP", + "RIPEMD160", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "SHA1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "SHA256", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "HASH160", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "HASH256", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'", + "P2SH,STRICTENC", + "PUSH_SIZE", + ">520 byte push" + ], + [ + "0", + "IF 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ENDIF 1", + "P2SH,STRICTENC", + "PUSH_SIZE", + ">520 byte push in non-executed IF branch" + ], + [ + "1", + "0x61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + "P2SH,STRICTENC", + "OP_COUNT", + ">201 opcodes executed. 0x61 is NOP" + ], + [ + "0", + "IF 0x6161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 ENDIF 1", + "P2SH,STRICTENC", + "OP_COUNT", + ">201 opcodes including non-executed IF branch. 0x61 is NOP" + ], + [ + "1 2 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "1 2 3 4 5 6 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "P2SH,STRICTENC", + "STACK_SIZE", + ">1,000 stack size (0x6f is 3DUP)" + ], + [ + "1 2 3 4 5 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "1 TOALTSTACK 2 TOALTSTACK 3 4 5 6 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f", + "P2SH,STRICTENC", + "STACK_SIZE", + ">1,000 stack+altstack size" + ], + [ + "NOP", + "0 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' 0x6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f 2DUP 0x616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161", + "P2SH,STRICTENC", + "SCRIPT_SIZE", + "10,001-byte scriptPubKey" + ], + [ + "NOP1", + "NOP10", + "P2SH,STRICTENC", + "EVAL_FALSE" + ], + [ + "1", + "VER", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_VER is reserved" + ], + [ + "1", + "VERIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_VERIF is reserved" + ], + [ + "1", + "VERNOTIF", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_VERNOTIF is reserved" + ], + [ + "1", + "RESERVED", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_RESERVED is reserved" + ], + [ + "1", + "RESERVED1", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_RESERVED1 is reserved" + ], + [ + "1", + "RESERVED2", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_RESERVED2 is reserved" + ], + [ + "1", + "0xba", + "P2SH,STRICTENC", + "BAD_OPCODE", + "0xba == OP_NOP10 + 1" + ], + [ + "2147483648", + "1ADD 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "We cannot do math on 5-byte integers" + ], + [ + "2147483648", + "NEGATE 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "We cannot do math on 5-byte integers" + ], + [ + "-2147483648", + "1ADD 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "Because we use a sign bit, -2147483648 is also 5 bytes" + ], + [ + "2147483647", + "1ADD 1SUB 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "We cannot do math on 5-byte integers, even if the result is 4-bytes" + ], + [ + "2147483648", + "1SUB 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "We cannot do math on 5-byte integers, even if the result is 4-bytes" + ], + [ + "2147483648 1", + "BOOLOR 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "We cannot do BOOLOR on 5-byte integers (but we can still do IF etc)" + ], + [ + "2147483648 1", + "BOOLAND 1", + "P2SH,STRICTENC", + "UNKNOWN_ERROR", + "We cannot do BOOLAND on 5-byte integers" + ], + [ + "1", + "1 ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "ENDIF without IF" + ], + [ + "1", + "IF 1", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "IF without ENDIF" + ], + [ + "1 IF 1", + "ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "IFs don't carry over" + ], + [ + "NOP", + "IF 1 ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "The following tests check the if(stack.size() < N) tests in each opcode" + ], + [ + "NOP", + "NOTIF 1 ENDIF", + "P2SH,STRICTENC", + "UNBALANCED_CONDITIONAL", + "They are here to catch copy-and-paste errors" + ], + [ + "NOP", + "VERIFY 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION", + "Most of them are duplicated elsewhere," + ], + [ + "NOP", + "TOALTSTACK 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION", + "but, hey, more is always better, right?" + ], + [ + "1", + "FROMALTSTACK", + "P2SH,STRICTENC", + "INVALID_ALTSTACK_OPERATION" + ], + [ + "1", + "2DROP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "2DUP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1", + "3DUP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1 1", + "2OVER", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1 1 1 1", + "2ROT", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1 1", + "2SWAP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "IFDUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "DROP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "DUP 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "NIP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "OVER", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1 1 3", + "PICK", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "0", + "PICK 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1 1 3", + "ROLL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "0", + "ROLL 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1", + "ROT", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "SWAP", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "TUCK", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "SIZE 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "EQUAL 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "EQUALVERIFY 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "1ADD 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "1SUB 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "NEGATE 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "ABS 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "NOT 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "0NOTEQUAL 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "ADD", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "SUB", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "BOOLAND", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "BOOLOR", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "NUMEQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "NUMEQUALVERIFY 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "NUMNOTEQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "LESSTHAN", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "GREATERTHAN", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "LESSTHANOREQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "GREATERTHANOREQUAL", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "MIN", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1", + "MAX", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "1 1", + "WITHIN", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "RIPEMD160 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "SHA1 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "SHA256 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "HASH160 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "NOP", + "HASH256 1", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION" + ], + [ + "Increase CHECKSIG and CHECKMULTISIG negative test coverage" + ], + [ + "", + "CHECKSIG NOT", + "STRICTENC", + "INVALID_STACK_OPERATION", + "CHECKSIG must error when there are no stack items" + ], + [ + "0", + "CHECKSIG NOT", + "STRICTENC", + "INVALID_STACK_OPERATION", + "CHECKSIG must error when there are not 2 stack items" + ], + [ + "", + "CHECKMULTISIG NOT", + "STRICTENC", + "INVALID_STACK_OPERATION", + "CHECKMULTISIG must error when there are no stack items" + ], + [ + "", + "-1 CHECKMULTISIG NOT", + "STRICTENC", + "PUBKEY_COUNT", + "CHECKMULTISIG must error when the specified number of pubkeys is negative" + ], + [ + "", + "1 CHECKMULTISIG NOT", + "STRICTENC", + "INVALID_STACK_OPERATION", + "CHECKMULTISIG must error when there are not enough pubkeys on the stack" + ], + [ + "", + "-1 0 CHECKMULTISIG NOT", + "STRICTENC", + "SIG_COUNT", + "CHECKMULTISIG must error when the specified number of signatures is negative" + ], + [ + "", + "1 'pk1' 1 CHECKMULTISIG NOT", + "STRICTENC", + "INVALID_STACK_OPERATION", + "CHECKMULTISIG must error when there are not enough signatures on the stack" + ], + [ + "", + "'dummy' 'sig1' 1 'pk1' 1 CHECKMULTISIG IF 1 ENDIF", + "", + "EVAL_FALSE", + "CHECKMULTISIG must push false to stack when signature is invalid when NOT in strict enc mode" + ], + [ + "", + "0 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG 0 0 CHECKMULTISIG", + "P2SH,STRICTENC", + "OP_COUNT", + "202 CHECKMULTISIGS, fails due to 201 op limit" + ], + [ + "1", + "0 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY 0 0 CHECKMULTISIGVERIFY", + "P2SH,STRICTENC", + "INVALID_STACK_OPERATION", + "" + ], + [ + "", + "NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIG", + "P2SH,STRICTENC", + "OP_COUNT", + "Fails due to 201 script operation limit" + ], + [ + "1", + "NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY 0 0 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 20 CHECKMULTISIGVERIFY", + "P2SH,STRICTENC", + "OP_COUNT", + "" + ], + [ + "0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21", + "21 CHECKMULTISIG 1", + "P2SH,STRICTENC", + "PUBKEY_COUNT", + "nPubKeys > 20" + ], + [ + "0 'sig' 1 0", + "CHECKMULTISIG 1", + "P2SH,STRICTENC", + "SIG_COUNT", + "nSigs > nPubKeys" + ], + [ + "NOP 0x01 1", + "HASH160 0x14 0xda1745e9b549bd0bfa1a569971c77eba30cd5a4b EQUAL", + "P2SH,STRICTENC", + "SIG_PUSHONLY", + "Tests for Script.IsPushOnly()" + ], + [ + "NOP1 0x01 1", + "HASH160 0x14 0xda1745e9b549bd0bfa1a569971c77eba30cd5a4b EQUAL", + "P2SH,STRICTENC", + "SIG_PUSHONLY" + ], + [ + "0 0x01 0x50", + "HASH160 0x14 0xece424a6bb6ddf4db592c0faed60685047a361b1 EQUAL", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_RESERVED in P2SH should fail" + ], + [ + "0 0x01 VER", + "HASH160 0x14 0x0f4d7845db968f2a81b530b6f3c1d6246d4c7e01 EQUAL", + "P2SH,STRICTENC", + "BAD_OPCODE", + "OP_VER in P2SH should fail" + ], + [ + "0x00", + "'00' EQUAL", + "P2SH,STRICTENC", + "EVAL_FALSE", + "Basic OP_0 execution" + ], + [ + "MINIMALDATA enforcement for PUSHDATAs" + ], + [ + "0x4c 0x00", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA", + "Empty vector minimally represented by OP_0" + ], + [ + "0x01 0x81", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA", + "-1 minimally represented by OP_1NEGATE" + ], + [ + "0x01 0x01", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA", + "1 to 16 minimally represented by OP_1 to OP_16" + ], + [ + "0x01 0x02", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x03", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x04", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x05", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x06", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x07", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x08", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x09", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x0a", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x0b", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x0c", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x0d", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x0e", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x0f", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x01 0x10", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA" + ], + [ + "0x4c 0x48 0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA", + "PUSHDATA1 of 72 bytes minimally represented by direct push" + ], + [ + "0x4d 0xFF00 0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA", + "PUSHDATA2 of 255 bytes minimally represented by PUSHDATA1" + ], + [ + "0x4e 0x00010000 0x11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + "DROP 1", + "MINIMALDATA", + "MINIMALDATA", + "PUSHDATA4 of 256 bytes minimally represented by PUSHDATA2" + ], + [ + "MINIMALDATA enforcement for numeric arguments" + ], + [ + "0x01 0x00", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals 0" + ], + [ + "0x02 0x0000", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals 0" + ], + [ + "0x01 0x80", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "0x80 (negative zero) numequals 0" + ], + [ + "0x02 0x0080", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals 0" + ], + [ + "0x02 0x0500", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals 5" + ], + [ + "0x03 0x050000", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals 5" + ], + [ + "0x02 0x0580", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals -5" + ], + [ + "0x03 0x050080", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "numequals -5" + ], + [ + "0x03 0xff7f80", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "Minimal encoding is 0xffff" + ], + [ + "0x03 0xff7f00", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "Minimal encoding is 0xff7f" + ], + [ + "0x04 0xffff7f80", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "Minimal encoding is 0xffffff" + ], + [ + "0x04 0xffff7f00", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR", + "Minimal encoding is 0xffff7f" + ], + [ + "Test every numeric-accepting opcode for correct handling of the numeric minimal encoding rule" + ], + [ + "1 0x02 0x0000", + "PICK DROP", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "1 0x02 0x0000", + "ROLL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000", + "1ADD DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000", + "1SUB DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000", + "NEGATE DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000", + "ABS DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000", + "NOT DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000", + "0NOTEQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "ADD DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "ADD DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "SUB DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "SUB DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "BOOLAND DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "BOOLAND DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "BOOLOR DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "BOOLOR DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "NUMEQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 1", + "NUMEQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "NUMEQUALVERIFY 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "NUMEQUALVERIFY 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "NUMNOTEQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "NUMNOTEQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "LESSTHAN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "LESSTHAN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "GREATERTHAN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "GREATERTHAN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "LESSTHANOREQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "LESSTHANOREQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "GREATERTHANOREQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "GREATERTHANOREQUAL DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "MIN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "MIN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000", + "MAX DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0", + "MAX DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0x02 0x0000 0 0", + "WITHIN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000 0", + "WITHIN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0 0x02 0x0000", + "WITHIN DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0 0x02 0x0000", + "CHECKMULTISIG DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000 0", + "CHECKMULTISIG DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000 0 1", + "CHECKMULTISIG DROP 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0 0x02 0x0000", + "CHECKMULTISIGVERIFY 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "0 0x02 0x0000 0", + "CHECKMULTISIGVERIFY 1", + "MINIMALDATA", + "UNKNOWN_ERROR" + ], + [ + "Order of CHECKMULTISIG evaluation tests, inverted by swapping the order of" + ], + [ + "pubkeys/signatures so they fail due to the STRICTENC rules on validly encoded" + ], + [ + "signatures and pubkeys." + ], + [ + "0 0x47 0x3044022044dc17b0887c161bb67ba9635bf758735bdde503e4b0a0987f587f14a4e1143d022009a215772d49a85dae40d8ca03955af26ad3978a0ff965faa12915e9586249a501 0x47 0x3044022044dc17b0887c161bb67ba9635bf758735bdde503e4b0a0987f587f14a4e1143d022009a215772d49a85dae40d8ca03955af26ad3978a0ff965faa12915e9586249a501", + "2 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 0 2 CHECKMULTISIG NOT", + "STRICTENC", + "PUBKEYTYPE", + "2-of-2 CHECKMULTISIG NOT with the first pubkey invalid, and both signatures validly encoded." + ], + [ + "0 0x47 0x3044022044dc17b0887c161bb67ba9635bf758735bdde503e4b0a0987f587f14a4e1143d022009a215772d49a85dae40d8ca03955af26ad3978a0ff965faa12915e9586249a501 1", + "2 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 0x21 0x02865c40293a680cb9c020e7b1e106d8c1916d3cef99aa431a56d253e69256dac0 2 CHECKMULTISIG NOT", + "STRICTENC", + "SIG_DER", + "2-of-2 CHECKMULTISIG NOT with both pubkeys valid, but first signature invalid." + ], + [ + "0 0x47 0x304402205451ce65ad844dbb978b8bdedf5082e33b43cae8279c30f2c74d9e9ee49a94f802203fe95a7ccf74da7a232ee523ef4a53cb4d14bdd16289680cdb97a63819b8f42f01 0x46 0x304402205451ce65ad844dbb978b8bdedf5082e33b43cae8279c30f2c74d9e9ee49a94f802203fe95a7ccf74da7a232ee523ef4a53cb4d14bdd16289680cdb97a63819b8f42f", + "2 0x21 0x02a673638cb9587cb68ea08dbef685c6f2d2a751a8b3c6f2a7e9a4999e6e4bfaf5 0x21 0x02a673638cb9587cb68ea08dbef685c6f2d2a751a8b3c6f2a7e9a4999e6e4bfaf5 0x21 0x02a673638cb9587cb68ea08dbef685c6f2d2a751a8b3c6f2a7e9a4999e6e4bfaf5 3 CHECKMULTISIG", + "P2SH,STRICTENC", + "SIG_DER", + "2-of-3 with one valid and one invalid signature due to parse error, nSigs > validSigs" + ], + [ + "Increase DERSIG test coverage" + ], + [ + "0x4a 0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Overly long signature is incorrectly encoded for DERSIG" + ], + [ + "0x25 0x30220220000000000000000000000000000000000000000000000000000000000000000000", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Missing S is incorrectly encoded for DERSIG" + ], + [ + "0x27 0x3024021077777777777777777777777777777777020a7777777777777777777777777777777701", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "S with invalid S length is incorrectly encoded for DERSIG" + ], + [ + "0x27 0x302403107777777777777777777777777777777702107777777777777777777777777777777701", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Non-integer R is incorrectly encoded for DERSIG" + ], + [ + "0x27 0x302402107777777777777777777777777777777703107777777777777777777777777777777701", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Non-integer S is incorrectly encoded for DERSIG" + ], + [ + "0x17 0x3014020002107777777777777777777777777777777701", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Zero-length R is incorrectly encoded for DERSIG" + ], + [ + "0x17 0x3014021077777777777777777777777777777777020001", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Zero-length S is incorrectly encoded for DERSIG" + ], + [ + "0x27 0x302402107777777777777777777777777777777702108777777777777777777777777777777701", + "0 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "Negative S is incorrectly encoded for DERSIG" + ], + [ + "Some basic segwit checks" + ], + [ + [ + "00", + 0.00000000 + ], + "", + "0 0x206e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + "P2SH,WITNESS", + "EVAL_FALSE", + "Invalid witness script" + ], + [ + [ + "51", + 0.00000000 + ], + "", + "0 0x206e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + "P2SH,WITNESS", + "WITNESS_PROGRAM_MISMATCH", + "Witness script hash mismatch" + ], + [ + [ + "00", + 0.00000000 + ], + "", + "0 0x206e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + "", + "OK", + "Invalid witness script without WITNESS" + ], + [ + [ + "51", + 0.00000000 + ], + "", + "0 0x206e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d", + "", + "OK", + "Witness script hash mismatch without WITNESS" + ], + [ + "Automatically generated test cases" + ], + [ + "0x47 0x304402200a5c6163f07b8d3b013c4d1d6dba25e780b39658d79ba37af7057a3b7f15ffa102201fd9b4eaa9943f734928b99a83592c2e7bf342ea2680f6a2bb705167966b742001", + "0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "", + "OK", + "P2PK" + ], + [ + "0x47 0x304402200a5c6163f07b8c3b013c4d1d6dba25e780b39658d79ba37af7057a3b7f15ffa102201fd9b4eaa9943f734928b99a83592c2e7bf342ea2680f6a2bb705167966b742001", + "0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "", + "EVAL_FALSE", + "P2PK, bad sig" + ], + [ + "0x47 0x304402206e05a6fe23c59196ffe176c9ddc31e73a9885638f9d1328d47c0c703863b8876022076feb53811aa5b04e0e79f938eb19906cc5e67548bc555a8e8b8b0fc603d840c01 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508", + "DUP HASH160 0x14 0x1018853670f9f3b0582c5b9ee8ce93764ac32b93 EQUALVERIFY CHECKSIG", + "", + "OK", + "P2PKH" + ], + [ + "0x47 0x3044022034bb0494b50b8ef130e2185bb220265b9284ef5b4b8a8da4d8415df489c83b5102206259a26d9cc0a125ac26af6153b17c02956855ebe1467412f066e402f5f05d1201 0x21 0x03363d90d446b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640", + "DUP HASH160 0x14 0xc0834c0c158f53be706d234c38fd52de7eece656 EQUALVERIFY CHECKSIG", + "", + "EQUALVERIFY", + "P2PKH, bad pubkey" + ], + [ + "0x47 0x304402204710a85181663b32d25c70ec2bbd14adff5ddfff6cb50d09e155ef5f541fc86c0220056b0cc949be9386ecc5f6c2ac0493269031dbb185781db90171b54ac127790281", + "0x41 0x048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf CHECKSIG", + "", + "OK", + "P2PK anyonecanpay" + ], + [ + "0x47 0x304402204710a85181663b32d25c70ec2bbd14adff5ddfff6cb50d09e155ef5f541fc86c0220056b0cc949be9386ecc5f6c2ac0493269031dbb185781db90171b54ac127790201", + "0x41 0x048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf CHECKSIG", + "", + "EVAL_FALSE", + "P2PK anyonecanpay marked with normal hashtype" + ], + [ + "0x47 0x3044022003fef42ed6c7be8917441218f525a60e2431be978e28b7aca4d7a532cc413ae8022067a1f82c74e8d69291b90d148778405c6257bbcfc2353cc38a3e1f22bf44254601 0x23 0x210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac", + "HASH160 0x14 0x23b0ad3477f2178bc0b3eed26e4e6316f4e83aa1 EQUAL", + "P2SH", + "OK", + "P2SH(P2PK)" + ], + [ + "0x47 0x3044022003fef42ed6c7be8917441218f525a60e2431be978e28b7aca4d7a532cc413ae8022067a1f82c74e8d69291b90d148778405c6257bbcfc2353cc38a3e1f22bf44254601 0x23 0x210279be667ef9dcbbac54a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac", + "HASH160 0x14 0x23b0ad3477f2178bc0b3eed26e4e6316f4e83aa1 EQUAL", + "P2SH", + "EVAL_FALSE", + "P2SH(P2PK), bad redeemscript" + ], + [ + "0x47 0x30440220781ba4f59a7b207a10db87628bc2168df4d59b844b397d2dbc9a5835fb2f2b7602206ed8fbcc1072fe2dfc5bb25909269e5dc42ffcae7ec2bc81d59692210ff30c2b01 0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 0x19 0x76a91491b24bf9f5288532960ac687abb035127b1d28a588ac", + "HASH160 0x14 0x7f67f0521934a57d3039f77f9f32cf313f3ac74b EQUAL", + "P2SH", + "OK", + "P2SH(P2PKH)" + ], + [ + "0x47 0x304402204e2eb034be7b089534ac9e798cf6a2c79f38bcb34d1b179efd6f2de0841735db022071461beb056b5a7be1819da6a3e3ce3662831ecc298419ca101eb6887b5dd6a401 0x19 0x76a9147cf9c846cd4882efec4bf07e44ebdad495c94f4b88ac", + "HASH160 0x14 0x2df519943d5acc0ef5222091f9dfe3543f489a82 EQUAL", + "", + "OK", + "P2SH(P2PKH), bad sig but no VERIFY_P2SH" + ], + [ + "0x47 0x304402204e2eb034be7b089534ac9e798cf6a2c79f38bcb34d1b179efd6f2de0841735db022071461beb056b5a7be1819da6a3e3ce3662831ecc298419ca101eb6887b5dd6a401 0x19 0x76a9147cf9c846cd4882efec4bf07e44ebdad495c94f4b88ac", + "HASH160 0x14 0x2df519943d5acc0ef5222091f9dfe3543f489a82 EQUAL", + "P2SH", + "EQUALVERIFY", + "P2SH(P2PKH), bad sig" + ], + [ + "0 0x47 0x3044022051254b9fb476a52d85530792b578f86fea70ec1ffb4393e661bcccb23d8d63d3022076505f94a403c86097841944e044c70c2045ce90e36de51f7e9d3828db98a07501 0x47 0x304402200a358f750934b3feb822f1966bfcd8bbec9eeaa3a8ca941e11ee5960e181fa01022050bf6b5a8e7750f70354ae041cb68a7bade67ec6c3ab19eb359638974410626e01 0x47 0x304402200955d031fff71d8653221e85e36c3c85533d2312fc3045314b19650b7ae2f81002202a6bb8505e36201909d0921f01abff390ae6b7ff97bbf959f98aedeb0a56730901", + "3 0x21 0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 3 CHECKMULTISIG", + "", + "OK", + "3-of-3" + ], + [ + "0 0x47 0x3044022051254b9fb476a52d85530792b578f86fea70ec1ffb4393e661bcccb23d8d63d3022076505f94a403c86097841944e044c70c2045ce90e36de51f7e9d3828db98a07501 0x47 0x304402200a358f750934b3feb822f1966bfcd8bbec9eeaa3a8ca941e11ee5960e181fa01022050bf6b5a8e7750f70354ae041cb68a7bade67ec6c3ab19eb359638974410626e01 0", + "3 0x21 0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 3 CHECKMULTISIG", + "", + "EVAL_FALSE", + "3-of-3, 2 sigs" + ], + [ + "0 0x47 0x304402205b7d2c2f177ae76cfbbf14d589c113b0b35db753d305d5562dd0b61cbf366cfb02202e56f93c4f08a27f986cd424ffc48a462c3202c4902104d4d0ff98ed28f4bf8001 0x47 0x30440220563e5b3b1fc11662a84bc5ea2a32cc3819703254060ba30d639a1aaf2d5068ad0220601c1f47ddc76d93284dd9ed68f7c9974c4a0ea7cbe8a247d6bc3878567a5fca01 0x4c69 0x52210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179821038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f515082103363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff464053ae", + "HASH160 0x14 0xc9e4a896d149702d0d1695434feddd52e24ad78d EQUAL", + "P2SH", + "OK", + "P2SH(2-of-3)" + ], + [ + "0 0x47 0x304402205b7d2c2f177ae76cfbbf14d589c113b0b35db753d305d5562dd0b61cbf366cfb02202e56f93c4f08a27f986cd424ffc48a462c3202c4902104d4d0ff98ed28f4bf8001 0 0x4c69 0x52210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179821038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f515082103363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff464053ae", + "HASH160 0x14 0xc9e4a896d149702d0d1695434feddd52e24ad78d EQUAL", + "P2SH", + "EVAL_FALSE", + "P2SH(2-of-3), 1 sig" + ], + [ + "0x47 0x304402200060558477337b9022e70534f1fea71a318caf836812465a2509931c5e7c4987022078ec32bd50ac9e03a349ba953dfd9fe1c8d2dd8bdb1d38ddca844d3d5c78c11801", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "", + "OK", + "P2PK with too much R padding but no DERSIG" + ], + [ + "0x47 0x304402200060558477337b9022e70534f1fea71a318caf836812465a2509931c5e7c4987022078ec32bd50ac9e03a349ba953dfd9fe1c8d2dd8bdb1d38ddca844d3d5c78c11801", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "DERSIG", + "SIG_DER", + "P2PK with too much R padding" + ], + [ + "0x48 0x304502202de8c03fc525285c9c535631019a5f2af7c6454fa9eb392a3756a4917c420edd02210046130bf2baf7cfc065067c8b9e33a066d9c15edcea9feb0ca2d233e3597925b401", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "", + "OK", + "P2PK with too much S padding but no DERSIG" + ], + [ + "0x48 0x304502202de8c03fc525285c9c535631019a5f2af7c6454fa9eb392a3756a4917c420edd02210046130bf2baf7cfc065067c8b9e33a066d9c15edcea9feb0ca2d233e3597925b401", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "DERSIG", + "SIG_DER", + "P2PK with too much S padding" + ], + [ + "0x47 0x30440220d7a0417c3f6d1a15094d1cf2a3378ca0503eb8a57630953a9e2987e21ddd0a6502207a6266d686c99090920249991d3d42065b6d43eb70187b219c0db82e4f94d1a201", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "", + "OK", + "P2PK with too little R padding but no DERSIG" + ], + [ + "0x47 0x30440220d7a0417c3f6d1a15094d1cf2a3378ca0503eb8a57630953a9e2987e21ddd0a6502207a6266d686c99090920249991d3d42065b6d43eb70187b219c0db82e4f94d1a201", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "DERSIG", + "SIG_DER", + "P2PK with too little R padding" + ], + [ + "0x47 0x30440220005ece1335e7f757a1a1f476a7fb5bd90964e8a022489f890614a04acfb734c002206c12b8294a6513c7710e8c82d3c23d75cdbfe83200eb7efb495701958501a5d601", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG NOT", + "", + "OK", + "P2PK NOT with bad sig with too much R padding but no DERSIG" + ], + [ + "0x47 0x30440220005ece1335e7f757a1a1f476a7fb5bd90964e8a022489f890614a04acfb734c002206c12b8294a6513c7710e8c82d3c23d75cdbfe83200eb7efb495701958501a5d601", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "P2PK NOT with bad sig with too much R padding" + ], + [ + "0x47 0x30440220005ece1335e7f657a1a1f476a7fb5bd90964e8a022489f890614a04acfb734c002206c12b8294a6513c7710e8c82d3c23d75cdbfe83200eb7efb495701958501a5d601", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG NOT", + "", + "EVAL_FALSE", + "P2PK NOT with too much R padding but no DERSIG" + ], + [ + "0x47 0x30440220005ece1335e7f657a1a1f476a7fb5bd90964e8a022489f890614a04acfb734c002206c12b8294a6513c7710e8c82d3c23d75cdbfe83200eb7efb495701958501a5d601", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "P2PK NOT with too much R padding" + ], + [ + "0x47 0x30440220d7a0417c3f6d1a15094d1cf2a3378ca0503eb8a57630953a9e2987e21ddd0a6502207a6266d686c99090920249991d3d42065b6d43eb70187b219c0db82e4f94d1a201", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "", + "OK", + "BIP66 example 1, without DERSIG" + ], + [ + "0x47 0x30440220d7a0417c3f6d1a15094d1cf2a3378ca0503eb8a57630953a9e2987e21ddd0a6502207a6266d686c99090920249991d3d42065b6d43eb70187b219c0db82e4f94d1a201", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "DERSIG", + "SIG_DER", + "BIP66 example 1, with DERSIG" + ], + [ + "0x47 0x304402208e43c0b91f7c1e5bc58e41c8185f8a6086e111b0090187968a86f2822462d3c902200a58f4076b1133b18ff1dc83ee51676e44c60cc608d9534e0df5ace0424fc0be01", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "", + "EVAL_FALSE", + "BIP66 example 2, without DERSIG" + ], + [ + "0x47 0x304402208e43c0b91f7c1e5bc58e41c8185f8a6086e111b0090187968a86f2822462d3c902200a58f4076b1133b18ff1dc83ee51676e44c60cc608d9534e0df5ace0424fc0be01", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "BIP66 example 2, with DERSIG" + ], + [ + "0", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "", + "EVAL_FALSE", + "BIP66 example 3, without DERSIG" + ], + [ + "0", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "DERSIG", + "EVAL_FALSE", + "BIP66 example 3, with DERSIG" + ], + [ + "0", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "", + "OK", + "BIP66 example 4, without DERSIG" + ], + [ + "0", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "DERSIG", + "OK", + "BIP66 example 4, with DERSIG" + ], + [ + "0x09 0x300602010102010101", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "DERSIG", + "OK", + "BIP66 example 4, with DERSIG, non-null DER-compliant signature" + ], + [ + "0", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "DERSIG,NULLFAIL", + "OK", + "BIP66 example 4, with DERSIG and NULLFAIL" + ], + [ + "0x09 0x300602010102010101", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "DERSIG,NULLFAIL", + "NULLFAIL", + "BIP66 example 4, with DERSIG and NULLFAIL, non-null DER-compliant signature" + ], + [ + "1", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "", + "EVAL_FALSE", + "BIP66 example 5, without DERSIG" + ], + [ + "1", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG", + "DERSIG", + "SIG_DER", + "BIP66 example 5, with DERSIG" + ], + [ + "1", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "", + "OK", + "BIP66 example 6, without DERSIG" + ], + [ + "1", + "0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 CHECKSIG NOT", + "DERSIG", + "SIG_DER", + "BIP66 example 6, with DERSIG" + ], + [ + "0 0x47 0x30440220cae00b1444babfbf6071b0ba8707f6bd373da3df494d6e74119b0430c5db810502205d5231b8c5939c8ff0c82242656d6e06edb073d42af336c99fe8837c36ea39d501 0x47 0x3044022027c2714269ca5aeecc4d70edc88ba5ee0e3da4986e9216028f489ab4f1b8efce022022bd545b4951215267e4c5ceabd4c5350331b2e4a0b6494c56f361fa5a57a1a201", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", + "", + "OK", + "BIP66 example 7, without DERSIG" + ], + [ + "0 0x47 0x30440220cae00b1444babfbf6071b0ba8707f6bd373da3df494d6e74119b0430c5db810502205d5231b8c5939c8ff0c82242656d6e06edb073d42af336c99fe8837c36ea39d501 0x47 0x3044022027c2714269ca5aeecc4d70edc88ba5ee0e3da4986e9216028f489ab4f1b8efce022022bd545b4951215267e4c5ceabd4c5350331b2e4a0b6494c56f361fa5a57a1a201", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", + "DERSIG", + "SIG_DER", + "BIP66 example 7, with DERSIG" + ], + [ + "0 0x47 0x30440220b119d67d389315308d1745f734a51ff3ec72e06081e84e236fdf9dc2f5d2a64802204b04e3bc38674c4422ea317231d642b56dc09d214a1ecbbf16ecca01ed996e2201 0x47 0x3044022079ea80afd538d9ada421b5101febeb6bc874e01dde5bca108c1d0479aec339a4022004576db8f66130d1df686ccf00935703689d69cf539438da1edab208b0d63c4801", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", + "", + "EVAL_FALSE", + "BIP66 example 8, without DERSIG" + ], + [ + "0 0x47 0x30440220b119d67d389315308d1745f734a51ff3ec72e06081e84e236fdf9dc2f5d2a64802204b04e3bc38674c4422ea317231d642b56dc09d214a1ecbbf16ecca01ed996e2201 0x47 0x3044022079ea80afd538d9ada421b5101febeb6bc874e01dde5bca108c1d0479aec339a4022004576db8f66130d1df686ccf00935703689d69cf539438da1edab208b0d63c4801", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", + "DERSIG", + "SIG_DER", + "BIP66 example 8, with DERSIG" + ], + [ + "0 0 0x47 0x3044022081aa9d436f2154e8b6d600516db03d78de71df685b585a9807ead4210bd883490220534bb6bdf318a419ac0749660b60e78d17d515558ef369bf872eff405b676b2e01", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", + "", + "EVAL_FALSE", + "BIP66 example 9, without DERSIG" + ], + [ + "0 0 0x47 0x3044022081aa9d436f2154e8b6d600516db03d78de71df685b585a9807ead4210bd883490220534bb6bdf318a419ac0749660b60e78d17d515558ef369bf872eff405b676b2e01", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", + "DERSIG", + "SIG_DER", + "BIP66 example 9, with DERSIG" + ], + [ + "0 0 0x47 0x30440220da6f441dc3b4b2c84cfa8db0cd5b34ed92c9e01686de5a800d40498b70c0dcac02207c2cf91b0c32b860c4cd4994be36cfb84caf8bb7c3a8e4d96a31b2022c5299c501", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", + "", + "OK", + "BIP66 example 10, without DERSIG" + ], + [ + "0 0 0x47 0x30440220da6f441dc3b4b2c84cfa8db0cd5b34ed92c9e01686de5a800d40498b70c0dcac02207c2cf91b0c32b860c4cd4994be36cfb84caf8bb7c3a8e4d96a31b2022c5299c501", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", + "DERSIG", + "SIG_DER", + "BIP66 example 10, with DERSIG" + ], + [ + "0 0x47 0x30440220cae00b1444babfbf6071b0ba8707f6bd373da3df494d6e74119b0430c5db810502205d5231b8c5939c8ff0c82242656d6e06edb073d42af336c99fe8837c36ea39d501 0", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", + "", + "EVAL_FALSE", + "BIP66 example 11, without DERSIG" + ], + [ + "0 0x47 0x30440220cae00b1444babfbf6071b0ba8707f6bd373da3df494d6e74119b0430c5db810502205d5231b8c5939c8ff0c82242656d6e06edb073d42af336c99fe8837c36ea39d501 0", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG", + "DERSIG", + "EVAL_FALSE", + "BIP66 example 11, with DERSIG" + ], + [ + "0 0x47 0x30440220b119d67d389315308d1745f734a51ff3ec72e06081e84e236fdf9dc2f5d2a64802204b04e3bc38674c4422ea317231d642b56dc09d214a1ecbbf16ecca01ed996e2201 0", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", + "", + "OK", + "BIP66 example 12, without DERSIG" + ], + [ + "0 0x47 0x30440220b119d67d389315308d1745f734a51ff3ec72e06081e84e236fdf9dc2f5d2a64802204b04e3bc38674c4422ea317231d642b56dc09d214a1ecbbf16ecca01ed996e2201 0", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 2 CHECKMULTISIG NOT", + "DERSIG", + "OK", + "BIP66 example 12, with DERSIG" + ], + [ + "0x48 0x304402203e4516da7253cf068effec6b95c41221c0cf3a8e6ccb8cbf1725b562e9afde2c022054e1c258c2981cdfba5df1f46661fb6541c44f77ca0092f3600331abfffb12510101", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG", + "", + "OK", + "P2PK with multi-byte hashtype, without DERSIG" + ], + [ + "0x48 0x304402203e4516da7253cf068effec6b95c41221c0cf3a8e6ccb8cbf1725b562e9afde2c022054e1c258c2981cdfba5df1f46661fb6541c44f77ca0092f3600331abfffb12510101", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG", + "DERSIG", + "SIG_DER", + "P2PK with multi-byte hashtype, with DERSIG" + ], + [ + "0x48 0x304502203e4516da7253cf068effec6b95c41221c0cf3a8e6ccb8cbf1725b562e9afde2c022100ab1e3da73d67e32045a20e0b999e049978ea8d6ee5480d485fcf2ce0d03b2ef001", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG", + "", + "OK", + "P2PK with high S but no LOW_S" + ], + [ + "0x48 0x304502203e4516da7253cf068effec6b95c41221c0cf3a8e6ccb8cbf1725b562e9afde2c022100ab1e3da73d67e32045a20e0b999e049978ea8d6ee5480d485fcf2ce0d03b2ef001", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG", + "LOW_S", + "SIG_HIGH_S", + "P2PK with high S" + ], + [ + "0x47 0x3044022057292e2d4dfe775becdd0a9e6547997c728cdf35390f6a017da56d654d374e4902206b643be2fc53763b4e284845bfea2c597d2dc7759941dce937636c9d341b71ed01", + "0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "", + "OK", + "P2PK with hybrid pubkey but no STRICTENC" + ], + [ + "0x47 0x3044022057292e2d4dfe775becdd0a9e6547997c728cdf35390f6a017da56d654d374e4902206b643be2fc53763b4e284845bfea2c597d2dc7759941dce937636c9d341b71ed01", + "0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "STRICTENC", + "PUBKEYTYPE", + "P2PK with hybrid pubkey" + ], + [ + "0x47 0x30440220035d554e3153c14950c9993f41c496607a8e24093db0595be7bf875cf64fcf1f02204731c8c4e5daf15e706cec19cdd8f2c5b1d05490e11dab8465ed426569b6e92101", + "0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG NOT", + "", + "EVAL_FALSE", + "P2PK NOT with hybrid pubkey but no STRICTENC" + ], + [ + "0x47 0x30440220035d554e3153c14950c9993f41c496607a8e24093db0595be7bf875cf64fcf1f02204731c8c4e5daf15e706cec19cdd8f2c5b1d05490e11dab8465ed426569b6e92101", + "0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG NOT", + "STRICTENC", + "PUBKEYTYPE", + "P2PK NOT with hybrid pubkey" + ], + [ + "0x47 0x30440220035d554e3153c04950c9993f41c496607a8e24093db0595be7bf875cf64fcf1f02204731c8c4e5daf15e706cec19cdd8f2c5b1d05490e11dab8465ed426569b6e92101", + "0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG NOT", + "", + "OK", + "P2PK NOT with invalid hybrid pubkey but no STRICTENC" + ], + [ + "0x47 0x30440220035d554e3153c04950c9993f41c496607a8e24093db0595be7bf875cf64fcf1f02204731c8c4e5daf15e706cec19cdd8f2c5b1d05490e11dab8465ed426569b6e92101", + "0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG NOT", + "STRICTENC", + "PUBKEYTYPE", + "P2PK NOT with invalid hybrid pubkey" + ], + [ + "0 0x47 0x304402202e79441ad1baf5a07fb86bae3753184f6717d9692680947ea8b6e8b777c69af1022079a262e13d868bb5a0964fefe3ba26942e1b0669af1afb55ef3344bc9d4fc4c401", + "1 0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 2 CHECKMULTISIG", + "", + "OK", + "1-of-2 with the second 1 hybrid pubkey and no STRICTENC" + ], + [ + "0 0x47 0x304402202e79441ad1baf5a07fb86bae3753184f6717d9692680947ea8b6e8b777c69af1022079a262e13d868bb5a0964fefe3ba26942e1b0669af1afb55ef3344bc9d4fc4c401", + "1 0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 2 CHECKMULTISIG", + "STRICTENC", + "OK", + "1-of-2 with the second 1 hybrid pubkey" + ], + [ + "0 0x47 0x3044022079c7824d6c868e0e1a273484e28c2654a27d043c8a27f49f52cb72efed0759090220452bbbf7089574fa082095a4fc1b3a16bafcf97a3a34d745fafc922cce66b27201", + "1 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x41 0x0679be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 2 CHECKMULTISIG", + "STRICTENC", + "PUBKEYTYPE", + "1-of-2 with the first 1 hybrid pubkey" + ], + [ + "0x47 0x304402206177d513ec2cda444c021a1f4f656fc4c72ba108ae063e157eb86dc3575784940220666fc66702815d0e5413bb9b1df22aed44f5f1efb8b99d41dd5dc9a5be6d205205", + "0x41 0x048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf CHECKSIG", + "", + "OK", + "P2PK with undefined hashtype but no STRICTENC" + ], + [ + "0x47 0x304402206177d513ec2cda444c021a1f4f656fc4c72ba108ae063e157eb86dc3575784940220666fc66702815d0e5413bb9b1df22aed44f5f1efb8b99d41dd5dc9a5be6d205205", + "0x41 0x048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf CHECKSIG", + "STRICTENC", + "SIG_HASHTYPE", + "P2PK with undefined hashtype" + ], + [ + "0x47 0x304402207409b5b320296e5e2136a7b281a7f803028ca4ca44e2b83eebd46932677725de02202d4eea1c8d3c98e6f42614f54764e6e5e6542e213eb4d079737e9a8b6e9812ec05", + "0x41 0x048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf CHECKSIG NOT", + "", + "OK", + "P2PK NOT with invalid sig and undefined hashtype but no STRICTENC" + ], + [ + "0x47 0x304402207409b5b320296e5e2136a7b281a7f803028ca4ca44e2b83eebd46932677725de02202d4eea1c8d3c98e6f42614f54764e6e5e6542e213eb4d079737e9a8b6e9812ec05", + "0x41 0x048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf CHECKSIG NOT", + "STRICTENC", + "SIG_HASHTYPE", + "P2PK NOT with invalid sig and undefined hashtype" + ], + [ + "1 0x47 0x3044022051254b9fb476a52d85530792b578f86fea70ec1ffb4393e661bcccb23d8d63d3022076505f94a403c86097841944e044c70c2045ce90e36de51f7e9d3828db98a07501 0x47 0x304402200a358f750934b3feb822f1966bfcd8bbec9eeaa3a8ca941e11ee5960e181fa01022050bf6b5a8e7750f70354ae041cb68a7bade67ec6c3ab19eb359638974410626e01 0x47 0x304402200955d031fff71d8653221e85e36c3c85533d2312fc3045314b19650b7ae2f81002202a6bb8505e36201909d0921f01abff390ae6b7ff97bbf959f98aedeb0a56730901", + "3 0x21 0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 3 CHECKMULTISIG", + "", + "OK", + "3-of-3 with nonzero dummy but no NULLDUMMY" + ], + [ + "1 0x47 0x3044022051254b9fb476a52d85530792b578f86fea70ec1ffb4393e661bcccb23d8d63d3022076505f94a403c86097841944e044c70c2045ce90e36de51f7e9d3828db98a07501 0x47 0x304402200a358f750934b3feb822f1966bfcd8bbec9eeaa3a8ca941e11ee5960e181fa01022050bf6b5a8e7750f70354ae041cb68a7bade67ec6c3ab19eb359638974410626e01 0x47 0x304402200955d031fff71d8653221e85e36c3c85533d2312fc3045314b19650b7ae2f81002202a6bb8505e36201909d0921f01abff390ae6b7ff97bbf959f98aedeb0a56730901", + "3 0x21 0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 3 CHECKMULTISIG", + "NULLDUMMY", + "SIG_NULLDUMMY", + "3-of-3 with nonzero dummy" + ], + [ + "1 0x47 0x304402201bb2edab700a5d020236df174fefed78087697143731f659bea59642c759c16d022061f42cdbae5bcd3e8790f20bf76687443436e94a634321c16a72aa54cbc7c2ea01 0x47 0x304402204bb4a64f2a6e5c7fb2f07fef85ee56fde5e6da234c6a984262307a20e99842d702206f8303aaba5e625d223897e2ffd3f88ef1bcffef55f38dc3768e5f2e94c923f901 0x47 0x3044022040c2809b71fffb155ec8b82fe7a27f666bd97f941207be4e14ade85a1249dd4d02204d56c85ec525dd18e29a0533d5ddf61b6b1bb32980c2f63edf951aebf7a27bfe01", + "3 0x21 0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 3 CHECKMULTISIG NOT", + "", + "OK", + "3-of-3 NOT with invalid sig and nonzero dummy but no NULLDUMMY" + ], + [ + "1 0x47 0x304402201bb2edab700a5d020236df174fefed78087697143731f659bea59642c759c16d022061f42cdbae5bcd3e8790f20bf76687443436e94a634321c16a72aa54cbc7c2ea01 0x47 0x304402204bb4a64f2a6e5c7fb2f07fef85ee56fde5e6da234c6a984262307a20e99842d702206f8303aaba5e625d223897e2ffd3f88ef1bcffef55f38dc3768e5f2e94c923f901 0x47 0x3044022040c2809b71fffb155ec8b82fe7a27f666bd97f941207be4e14ade85a1249dd4d02204d56c85ec525dd18e29a0533d5ddf61b6b1bb32980c2f63edf951aebf7a27bfe01", + "3 0x21 0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 3 CHECKMULTISIG NOT", + "NULLDUMMY", + "SIG_NULLDUMMY", + "3-of-3 NOT with invalid sig with nonzero dummy" + ], + [ + "0 0x47 0x304402200abeb4bd07f84222f474aed558cfbdfc0b4e96cde3c2935ba7098b1ff0bd74c302204a04c1ca67b2a20abee210cf9a21023edccbbf8024b988812634233115c6b73901 DUP", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 2 CHECKMULTISIG", + "", + "OK", + "2-of-2 with two identical keys and sigs pushed using OP_DUP but no SIGPUSHONLY" + ], + [ + "0 0x47 0x304402200abeb4bd07f84222f474aed558cfbdfc0b4e96cde3c2935ba7098b1ff0bd74c302204a04c1ca67b2a20abee210cf9a21023edccbbf8024b988812634233115c6b73901 DUP", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 2 CHECKMULTISIG", + "SIGPUSHONLY", + "SIG_PUSHONLY", + "2-of-2 with two identical keys and sigs pushed using OP_DUP" + ], + [ + "0x47 0x3044022018a2a81a93add5cb5f5da76305718e4ea66045ec4888b28d84cb22fae7f4645b02201e6daa5ed5d2e4b2b2027cf7ffd43d8d9844dd49f74ef86899ec8e669dfd39aa01 NOP8 0x23 0x2103363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640ac", + "HASH160 0x14 0x215640c2f72f0d16b4eced26762035a42ffed39a EQUAL", + "", + "OK", + "P2SH(P2PK) with non-push scriptSig but no P2SH or SIGPUSHONLY" + ], + [ + "0x47 0x304402203e4516da7253cf068effec6b95c41221c0cf3a8e6ccb8cbf1725b562e9afde2c022054e1c258c2981cdfba5df1f46661fb6541c44f77ca0092f3600331abfffb125101 NOP8", + "0x21 0x03363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640 CHECKSIG", + "", + "OK", + "P2PK with non-push scriptSig but with P2SH validation" + ], + [ + "0x47 0x3044022018a2a81a93add5cb5f5da76305718e4ea66045ec4888b28d84cb22fae7f4645b02201e6daa5ed5d2e4b2b2027cf7ffd43d8d9844dd49f74ef86899ec8e669dfd39aa01 NOP8 0x23 0x2103363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640ac", + "HASH160 0x14 0x215640c2f72f0d16b4eced26762035a42ffed39a EQUAL", + "P2SH", + "SIG_PUSHONLY", + "P2SH(P2PK) with non-push scriptSig but no SIGPUSHONLY" + ], + [ + "0x47 0x3044022018a2a81a93add5cb5f5da76305718e4ea66045ec4888b28d84cb22fae7f4645b02201e6daa5ed5d2e4b2b2027cf7ffd43d8d9844dd49f74ef86899ec8e669dfd39aa01 NOP8 0x23 0x2103363d90d447b00c9c99ceac05b6262ee053441c7e55552ffe526bad8f83ff4640ac", + "HASH160 0x14 0x215640c2f72f0d16b4eced26762035a42ffed39a EQUAL", + "SIGPUSHONLY", + "SIG_PUSHONLY", + "P2SH(P2PK) with non-push scriptSig but not P2SH" + ], + [ + "0 0x47 0x304402200abeb4bd07f84222f474aed558cfbdfc0b4e96cde3c2935ba7098b1ff0bd74c302204a04c1ca67b2a20abee210cf9a21023edccbbf8024b988812634233115c6b73901 0x47 0x304402200abeb4bd07f84222f474aed558cfbdfc0b4e96cde3c2935ba7098b1ff0bd74c302204a04c1ca67b2a20abee210cf9a21023edccbbf8024b988812634233115c6b73901", + "2 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 0x21 0x038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508 2 CHECKMULTISIG", + "SIGPUSHONLY", + "OK", + "2-of-2 with two identical keys and sigs pushed" + ], + [ + "11 0x47 0x304402200a5c6163f07b8d3b013c4d1d6dba25e780b39658d79ba37af7057a3b7f15ffa102201fd9b4eaa9943f734928b99a83592c2e7bf342ea2680f6a2bb705167966b742001", + "0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "P2SH", + "OK", + "P2PK with unnecessary input but no CLEANSTACK" + ], + [ + "11 0x47 0x304402200a5c6163f07b8d3b013c4d1d6dba25e780b39658d79ba37af7057a3b7f15ffa102201fd9b4eaa9943f734928b99a83592c2e7bf342ea2680f6a2bb705167966b742001", + "0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "CLEANSTACK,P2SH", + "CLEANSTACK", + "P2PK with unnecessary input" + ], + [ + "11 0x47 0x304402202f7505132be14872581f35d74b759212d9da40482653f1ffa3116c3294a4a51702206adbf347a2240ca41c66522b1a22a41693610b76a8e7770645dc721d1635854f01 0x43 0x410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + "HASH160 0x14 0x31edc23bdafda4639e669f89ad6b2318dd79d032 EQUAL", + "P2SH", + "OK", + "P2SH with unnecessary input but no CLEANSTACK" + ], + [ + "11 0x47 0x304402202f7505132be14872581f35d74b759212d9da40482653f1ffa3116c3294a4a51702206adbf347a2240ca41c66522b1a22a41693610b76a8e7770645dc721d1635854f01 0x43 0x410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + "HASH160 0x14 0x31edc23bdafda4639e669f89ad6b2318dd79d032 EQUAL", + "CLEANSTACK,P2SH", + "CLEANSTACK", + "P2SH with unnecessary input" + ], + [ + "0x47 0x304402202f7505132be14872581f35d74b759212d9da40482653f1ffa3116c3294a4a51702206adbf347a2240ca41c66522b1a22a41693610b76a8e7770645dc721d1635854f01 0x43 0x410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + "HASH160 0x14 0x31edc23bdafda4639e669f89ad6b2318dd79d032 EQUAL", + "CLEANSTACK,P2SH", + "OK", + "P2SH with CLEANSTACK" + ], + [ + "Testing with uncompressed keys in witness v0 without WITNESS_PUBKEYTYPE" + ], + [ + [ + "304402200d461c140cfdfcf36b94961db57ae8c18d1cb80e9d95a9e47ac22470c1bf125502201c8dc1cbfef6a3ef90acbbb992ca22fe9466ee6f9d4898eda277a7ac3ab4b25101", + "410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000001 + ], + "", + "0 0x20 0xb95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "P2SH,WITNESS", + "OK", + "Basic P2WSH" + ], + [ + [ + "304402201e7216e5ccb3b61d46946ec6cc7e8c4e0117d13ac2fd4b152197e4805191c74202203e9903e33e84d9ee1dd13fb057afb7ccfb47006c23f6a067185efbc9dd780fc501", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000001 + ], + "", + "0 0x14 0x91b24bf9f5288532960ac687abb035127b1d28a5", + "P2SH,WITNESS", + "OK", + "Basic P2WPKH" + ], + [ + [ + "3044022066e02c19a513049d49349cf5311a1b012b7c4fae023795a18ab1d91c23496c22022025e216342c8e07ce8ef51e8daee88f84306a9de66236cab230bb63067ded1ad301", + "410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000001 + ], + "0x22 0x0020b95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "HASH160 0x14 0xf386c2ba255cc56d20cfa6ea8b062f8b59945518 EQUAL", + "P2SH,WITNESS", + "OK", + "Basic P2SH(P2WSH)" + ], + [ + [ + "304402200929d11561cd958460371200f82e9cae64c727a495715a31828e27a7ad57b36d0220361732ced04a6f97351ecca21a56d0b8cd4932c1da1f8f569a2b68e5e48aed7801", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000001 + ], + "0x16 0x001491b24bf9f5288532960ac687abb035127b1d28a5", + "HASH160 0x14 0x17743beb429c55c942d2ec703b98c4d57c2df5c6 EQUAL", + "P2SH,WITNESS", + "OK", + "Basic P2SH(P2WPKH)" + ], + [ + [ + "304402202589f0512cb2408fb08ed9bd24f85eb3059744d9e4f2262d0b7f1338cff6e8b902206c0978f449693e0578c71bc543b11079fd0baae700ee5e9a6bee94db490af9fc01", + "41048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26cafac", + 0.00000000 + ], + "", + "0 0x20 0xac8ebd9e52c17619a381fa4f71aebb696087c6ef17c960fd0587addad99c0610", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2WSH with the wrong key" + ], + [ + [ + "304402206ef7fdb2986325d37c6eb1a8bb24aeb46dede112ed8fc76c7d7500b9b83c0d3d02201edc2322c794fe2d6b0bd73ed319e714aa9b86d8891961530d5c9b7156b60d4e01", + "048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf", + 0.00000000 + ], + "", + "0 0x14 0x7cf9c846cd4882efec4bf07e44ebdad495c94f4b", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2WPKH with the wrong key" + ], + [ + [ + "30440220069ea3581afaf8187f63feee1fd2bd1f9c0dc71ea7d6e8a8b07ee2ebcf824bf402201a4fdef4c532eae59223be1eda6a397fc835142d4ddc6c74f4aa85b766a5c16f01", + "41048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26cafac", + 0.00000000 + ], + "0x22 0x0020ac8ebd9e52c17619a381fa4f71aebb696087c6ef17c960fd0587addad99c0610", + "HASH160 0x14 0x61039a003883787c0d6ebc66d97fdabe8e31449d EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2SH(P2WSH) with the wrong key" + ], + [ + [ + "304402204209e49457c2358f80d0256bc24535b8754c14d08840fc4be762d6f5a0aed80b02202eaf7d8fc8d62f60c67adcd99295528d0e491ae93c195cec5a67e7a09532a88001", + "048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf", + 0.00000000 + ], + "0x16 0x00147cf9c846cd4882efec4bf07e44ebdad495c94f4b", + "HASH160 0x14 0x4e0c2aed91315303fc6a1dc4c7bc21c88f75402e EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2SH(P2WPKH) with the wrong key" + ], + [ + [ + "304402202589f0512cb2408fb08ed9bd24f85eb3059744d9e4f2262d0b7f1338cff6e8b902206c0978f449693e0578c71bc543b11079fd0baae700ee5e9a6bee94db490af9fc01", + "41048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26cafac", + 0.00000000 + ], + "", + "0 0x20 0xac8ebd9e52c17619a381fa4f71aebb696087c6ef17c960fd0587addad99c0610", + "P2SH", + "OK", + "Basic P2WSH with the wrong key but no WITNESS" + ], + [ + [ + "304402206ef7fdb2986325d37c6eb1a8bb24aeb46dede112ed8fc76c7d7500b9b83c0d3d02201edc2322c794fe2d6b0bd73ed319e714aa9b86d8891961530d5c9b7156b60d4e01", + "048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf", + 0.00000000 + ], + "", + "0 0x14 0x7cf9c846cd4882efec4bf07e44ebdad495c94f4b", + "P2SH", + "OK", + "Basic P2WPKH with the wrong key but no WITNESS" + ], + [ + [ + "30440220069ea3581afaf8187f63feee1fd2bd1f9c0dc71ea7d6e8a8b07ee2ebcf824bf402201a4fdef4c532eae59223be1eda6a397fc835142d4ddc6c74f4aa85b766a5c16f01", + "41048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26cafac", + 0.00000000 + ], + "0x22 0x0020ac8ebd9e52c17619a381fa4f71aebb696087c6ef17c960fd0587addad99c0610", + "HASH160 0x14 0x61039a003883787c0d6ebc66d97fdabe8e31449d EQUAL", + "P2SH", + "OK", + "Basic P2SH(P2WSH) with the wrong key but no WITNESS" + ], + [ + [ + "304402204209e49457c2358f80d0256bc24535b8754c14d08840fc4be762d6f5a0aed80b02202eaf7d8fc8d62f60c67adcd99295528d0e491ae93c195cec5a67e7a09532a88001", + "048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf", + 0.00000000 + ], + "0x16 0x00147cf9c846cd4882efec4bf07e44ebdad495c94f4b", + "HASH160 0x14 0x4e0c2aed91315303fc6a1dc4c7bc21c88f75402e EQUAL", + "P2SH", + "OK", + "Basic P2SH(P2WPKH) with the wrong key but no WITNESS" + ], + [ + [ + "3044022066faa86e74e8b30e82691b985b373de4f9e26dc144ec399c4f066aa59308e7c202204712b86f28c32503faa051dbeabff2c238ece861abc36c5e0b40b1139ca222f001", + "410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000000 + ], + "", + "0 0x20 0xb95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2WSH with wrong value" + ], + [ + [ + "304402203b3389b87448d7dfdb5e82fb854fcf92d7925f9938ea5444e36abef02c3d6a9602202410bc3265049abb07fd2e252c65ab7034d95c9d5acccabe9fadbdc63a52712601", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000000 + ], + "", + "0 0x14 0x91b24bf9f5288532960ac687abb035127b1d28a5", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2WPKH with wrong value" + ], + [ + [ + "3044022000a30c4cfc10e4387be528613575434826ad3c15587475e0df8ce3b1746aa210022008149265e4f8e9dafe1f3ea50d90cb425e9e40ea7ebdd383069a7cfa2b77004701", + "410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000000 + ], + "0x22 0x0020b95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "HASH160 0x14 0xf386c2ba255cc56d20cfa6ea8b062f8b59945518 EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2SH(P2WSH) with wrong value" + ], + [ + [ + "304402204fc3a2cd61a47913f2a5f9107d0ad4a504c7b31ee2d6b3b2f38c2b10ee031e940220055d58b7c3c281aaa381d8f486ac0f3e361939acfd568046cb6a311cdfa974cf01", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000000 + ], + "0x16 0x001491b24bf9f5288532960ac687abb035127b1d28a5", + "HASH160 0x14 0x17743beb429c55c942d2ec703b98c4d57c2df5c6 EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE", + "Basic P2SH(P2WPKH) with wrong value" + ], + [ + [ + "304402205ae57ae0534c05ca9981c8a6cdf353b505eaacb7375f96681a2d1a4ba6f02f84022056248e68643b7d8ce7c7d128c9f1f348bcab8be15d094ad5cadd24251a28df8001", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000000 + ], + "", + "1 0x14 0x91b24bf9f5288532960ac687abb035127b1d28a5", + "DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM,P2SH,WITNESS", + "DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM", + "P2WPKH with future witness version" + ], + [ + [ + "3044022064100ca0e2a33332136775a86cd83d0230e58b9aebb889c5ac952abff79a46ef02205f1bf900e022039ad3091bdaf27ac2aef3eae9ed9f190d821d3e508405b9513101", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000000 + ], + "", + "0 0x1f 0xb34b78da162751647974d5cb7410aa428ad339dbf7d1e16e833f68a0cbf1c3", + "P2SH,WITNESS", + "WITNESS_PROGRAM_WRONG_LENGTH", + "P2WPKH with wrong witness program length" + ], + [ + "", + "0 0x20 0xb95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "P2SH,WITNESS", + "WITNESS_PROGRAM_WITNESS_EMPTY", + "P2WSH with empty witness" + ], + [ + [ + "3044022039105b995a5f448639a997a5c90fda06f50b49df30c3bdb6663217bf79323db002206fecd54269dec569fcc517178880eb58bb40f381a282bb75766ff3637d5f4b4301", + "400479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000000 + ], + "", + "0 0x20 0xb95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "P2SH,WITNESS", + "WITNESS_PROGRAM_MISMATCH", + "P2WSH with witness program mismatch" + ], + [ + [ + "304402201a96950593cb0af32d080b0f193517f4559241a8ebd1e95e414533ad64a3f423022047f4f6d3095c23235bdff3aeff480d0529c027a3f093cb265b7cbf148553b85101", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + "", + 0.00000000 + ], + "", + "0 0x14 0x91b24bf9f5288532960ac687abb035127b1d28a5", + "P2SH,WITNESS", + "WITNESS_PROGRAM_MISMATCH", + "P2WPKH with witness program mismatch" + ], + [ + [ + "304402201a96950593cb0af32d080b0f193517f4559241a8ebd1e95e414533ad64a3f423022047f4f6d3095c23235bdff3aeff480d0529c027a3f093cb265b7cbf148553b85101", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000000 + ], + "11", + "0 0x14 0x91b24bf9f5288532960ac687abb035127b1d28a5", + "P2SH,WITNESS", + "WITNESS_MALLEATED", + "P2WPKH with non-empty scriptSig" + ], + [ + [ + "304402204209e49457c2358f80d0256bc24535b8754c14d08840fc4be762d6f5a0aed80b02202eaf7d8fc8d62f60c67adcd99295528d0e491ae93c195cec5a67e7a09532a88001", + "048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf", + 0.00000000 + ], + "11 0x16 0x00147cf9c846cd4882efec4bf07e44ebdad495c94f4b", + "HASH160 0x14 0x4e0c2aed91315303fc6a1dc4c7bc21c88f75402e EQUAL", + "P2SH,WITNESS", + "WITNESS_MALLEATED_P2SH", + "P2SH(P2WPKH) with superfluous push in scriptSig" + ], + [ + [ + "", + 0.00000000 + ], + "0x47 0x304402200a5c6163f07b8d3b013c4d1d6dba25e780b39658d79ba37af7057a3b7f15ffa102201fd9b4eaa9943f734928b99a83592c2e7bf342ea2680f6a2bb705167966b742001", + "0x41 0x0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 CHECKSIG", + "P2SH,WITNESS", + "WITNESS_UNEXPECTED", + "P2PK with witness" + ], + [ + "Testing with compressed keys in witness v0 with WITNESS_PUBKEYTYPE" + ], + [ + [ + "304402204256146fcf8e73b0fd817ffa2a4e408ff0418ff987dd08a4f485b62546f6c43c02203f3c8c3e2febc051e1222867f5f9d0eaf039d6792911c10940aa3cc74123378e01", + "210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac", + 0.00000001 + ], + "", + "0 0x20 0x1863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "Basic P2WSH with compressed key" + ], + [ + [ + "304402204edf27486f11432466b744df533e1acac727e0c83e5f912eb289a3df5bf8035f022075809fdd876ede40ad21667eba8b7e96394938f9c9c50f11b6a1280cce2cea8601", + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + 0.00000001 + ], + "", + "0 0x14 0x751e76e8199196d454941c45d1b3a323f1433bd6", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "Basic P2WPKH with compressed key" + ], + [ + [ + "304402203a549090cc46bce1e5e95c4922ea2c12747988e0207b04c42f81cdbe87bb1539022050f57a245b875fd5119c419aaf050bcdf41384f0765f04b809e5bced1fe7093d01", + "210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac", + 0.00000001 + ], + "0x22 0x00201863143c14c5166804bd19203356da136c985678cd4d27a1b8c6329604903262", + "HASH160 0x14 0xe4300531190587e3880d4c3004f5355d88ff928d EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "Basic P2SH(P2WSH) with compressed key" + ], + [ + [ + "304402201bc0d53046827f4a35a3166e33e3b3366c4085540dc383b95d21ed2ab11e368a0220333e78c6231214f5f8e59621e15d7eeab0d4e4d0796437e00bfbd2680c5f9c1701", + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + 0.00000001 + ], + "0x16 0x0014751e76e8199196d454941c45d1b3a323f1433bd6", + "HASH160 0x14 0xbcfeb728b584253d5f3f70bcb780e9ef218a68f4 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "Basic P2SH(P2WPKH) with compressed key" + ], + [ + "Testing with uncompressed keys in witness v0 with WITNESS_PUBKEYTYPE" + ], + [ + [ + "304402200d461c140cfdfcf36b94961db57ae8c18d1cb80e9d95a9e47ac22470c1bf125502201c8dc1cbfef6a3ef90acbbb992ca22fe9466ee6f9d4898eda277a7ac3ab4b25101", + "410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000001 + ], + "", + "0 0x20 0xb95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "Basic P2WSH" + ], + [ + [ + "304402201e7216e5ccb3b61d46946ec6cc7e8c4e0117d13ac2fd4b152197e4805191c74202203e9903e33e84d9ee1dd13fb057afb7ccfb47006c23f6a067185efbc9dd780fc501", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000001 + ], + "", + "0 0x14 0x91b24bf9f5288532960ac687abb035127b1d28a5", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "Basic P2WPKH" + ], + [ + [ + "3044022066e02c19a513049d49349cf5311a1b012b7c4fae023795a18ab1d91c23496c22022025e216342c8e07ce8ef51e8daee88f84306a9de66236cab230bb63067ded1ad301", + "410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8ac", + 0.00000001 + ], + "0x22 0x0020b95237b48faaa69eb078e1170be3b5cbb3fddf16d0a991e14ad274f7b33a4f64", + "HASH160 0x14 0xf386c2ba255cc56d20cfa6ea8b062f8b59945518 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "Basic P2SH(P2WSH)" + ], + [ + [ + "304402200929d11561cd958460371200f82e9cae64c727a495715a31828e27a7ad57b36d0220361732ced04a6f97351ecca21a56d0b8cd4932c1da1f8f569a2b68e5e48aed7801", + "0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + 0.00000001 + ], + "0x16 0x001491b24bf9f5288532960ac687abb035127b1d28a5", + "HASH160 0x14 0x17743beb429c55c942d2ec703b98c4d57c2df5c6 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "Basic P2SH(P2WPKH)" + ], + [ + "Testing P2WSH multisig with compressed keys" + ], + [ + [ + "", + "304402207eb8a59b5c65fc3f6aeef77066556ed5c541948a53a3ba7f7c375b8eed76ee7502201e036a7a9a98ff919ff94dc905d67a1ec006f79ef7cff0708485c8bb79dce38e01", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "", + "0 0x20 0x06c24420938f0fa3c1cb2707d867154220dca365cdbfa0dd2a83854730221460", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "P2WSH CHECKMULTISIG with compressed keys" + ], + [ + [ + "", + "3044022033706aed33b8155d5486df3b9bca8cdd3bd4bdb5436dce46d72cdaba51d22b4002203626e94fe53a178af46624f17315c6931f20a30b103f5e044e1eda0c3fe185c601", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "0x22 0x002006c24420938f0fa3c1cb2707d867154220dca365cdbfa0dd2a83854730221460", + "HASH160 0x14 0x26282aad7c29369d15fed062a778b6100d31a340 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "P2SH(P2WSH) CHECKMULTISIG with compressed keys" + ], + [ + [ + "", + "304402204048b7371ab1c544362efb89af0c80154747d665aa4fcfb2edfd2d161e57b42e02207e043748e96637080ffc3acbd4dcc6fee1e58d30f6d1269535f32188e5ddae7301", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "", + "0 0x20 0x06c24420938f0fa3c1cb2707d867154220dca365cdbfa0dd2a83854730221460", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "P2WSH CHECKMULTISIG with compressed keys" + ], + [ + [ + "", + "3044022073902ef0b8a554c36c44cc03c1b64df96ce2914ebcf946f5bb36078fd5245cdf02205b148f1ba127065fb8c83a5a9576f2dcd111739788ed4bb3ee08b2bd3860c91c01", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "0x22 0x002006c24420938f0fa3c1cb2707d867154220dca365cdbfa0dd2a83854730221460", + "HASH160 0x14 0x26282aad7c29369d15fed062a778b6100d31a340 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "P2SH(P2WSH) CHECKMULTISIG with compressed keys" + ], + [ + "Testing P2WSH multisig with compressed and uncompressed keys (first key being the key closer to the top of stack)" + ], + [ + [ + "", + "304402202d092ededd1f060609dbf8cb76950634ff42b3e62cf4adb69ab92397b07d742302204ff886f8d0817491a96d1daccdcc820f6feb122ee6230143303100db37dfa79f01", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "", + "0 0x20 0x08a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "P2SH,WITNESS", + "OK", + "P2WSH CHECKMULTISIG with first key uncompressed and signing with the first key" + ], + [ + [ + "", + "304402202dd7e91243f2235481ffb626c3b7baf2c859ae3a5a77fb750ef97b99a8125dc002204960de3d3c3ab9496e218ec57e5240e0e10a6f9546316fe240c216d45116d29301", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "0x22 0x002008a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "HASH160 0x14 0x6f5ecd4b83b77f3c438f5214eff96454934fc5d1 EQUAL", + "P2SH,WITNESS", + "OK", + "P2SH(P2WSH) CHECKMULTISIG first key uncompressed and signing with the first key" + ], + [ + [ + "", + "304402202d092ededd1f060609dbf8cb76950634ff42b3e62cf4adb69ab92397b07d742302204ff886f8d0817491a96d1daccdcc820f6feb122ee6230143303100db37dfa79f01", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "", + "0 0x20 0x08a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "P2WSH CHECKMULTISIG with first key uncompressed and signing with the first key" + ], + [ + [ + "", + "304402202dd7e91243f2235481ffb626c3b7baf2c859ae3a5a77fb750ef97b99a8125dc002204960de3d3c3ab9496e218ec57e5240e0e10a6f9546316fe240c216d45116d29301", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "0x22 0x002008a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "HASH160 0x14 0x6f5ecd4b83b77f3c438f5214eff96454934fc5d1 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "P2SH(P2WSH) CHECKMULTISIG with first key uncompressed and signing with the first key" + ], + [ + [ + "", + "304402201e9e6f7deef5b2f21d8223c5189b7d5e82d237c10e97165dd08f547c4e5ce6ed02206796372eb1cc6acb52e13ee2d7f45807780bf96b132cb6697f69434be74b1af901", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "", + "0 0x20 0x08a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "P2SH,WITNESS", + "OK", + "P2WSH CHECKMULTISIG with first key uncompressed and signing with the second key" + ], + [ + [ + "", + "3044022045e667f3f0f3147b95597a24babe9afecea1f649fd23637dfa7ed7e9f3ac18440220295748e81005231135289fe3a88338dabba55afa1bdb4478691337009d82b68d01", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "0x22 0x002008a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "HASH160 0x14 0x6f5ecd4b83b77f3c438f5214eff96454934fc5d1 EQUAL", + "P2SH,WITNESS", + "OK", + "P2SH(P2WSH) CHECKMULTISIG with first key uncompressed and signing with the second key" + ], + [ + [ + "", + "304402201e9e6f7deef5b2f21d8223c5189b7d5e82d237c10e97165dd08f547c4e5ce6ed02206796372eb1cc6acb52e13ee2d7f45807780bf96b132cb6697f69434be74b1af901", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "", + "0 0x20 0x08a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "P2WSH CHECKMULTISIG with first key uncompressed and signing with the second key" + ], + [ + [ + "", + "3044022045e667f3f0f3147b95597a24babe9afecea1f649fd23637dfa7ed7e9f3ac18440220295748e81005231135289fe3a88338dabba55afa1bdb4478691337009d82b68d01", + "5121038282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f51508410479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b852ae", + 0.00000001 + ], + "0x22 0x002008a6665ebfd43b02323423e764e185d98d1587f903b81507dbb69bfc41005efa", + "HASH160 0x14 0x6f5ecd4b83b77f3c438f5214eff96454934fc5d1 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "P2SH(P2WSH) CHECKMULTISIG with first key uncompressed and signing with the second key" + ], + [ + [ + "", + "3044022046f5367a261fd8f8d7de6eb390491344f8ec2501638fb9a1095a0599a21d3f4c02205c1b3b51d20091c5f1020841bbca87b44ebe25405c64e4acf758f2eae8665f8401", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "", + "0 0x20 0x230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "P2SH,WITNESS", + "OK", + "P2WSH CHECKMULTISIG with second key uncompressed and signing with the first key" + ], + [ + [ + "", + "3044022053e210e4fb1881e6092fd75c3efc5163105599e246ded661c0ee2b5682cc2d6c02203a26b7ada8682a095b84c6d1b881637000b47d761fc837c4cee33555296d63f101", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "0x22 0x0020230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "HASH160 0x14 0x3478e7019ce61a68148f87549579b704cbe4c393 EQUAL", + "P2SH,WITNESS", + "OK", + "P2SH(P2WSH) CHECKMULTISIG second key uncompressed and signing with the first key" + ], + [ + [ + "", + "3044022046f5367a261fd8f8d7de6eb390491344f8ec2501638fb9a1095a0599a21d3f4c02205c1b3b51d20091c5f1020841bbca87b44ebe25405c64e4acf758f2eae8665f8401", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "", + "0 0x20 0x230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "P2WSH CHECKMULTISIG with second key uncompressed and signing with the first key should pass as the uncompressed key is not used" + ], + [ + [ + "", + "3044022053e210e4fb1881e6092fd75c3efc5163105599e246ded661c0ee2b5682cc2d6c02203a26b7ada8682a095b84c6d1b881637000b47d761fc837c4cee33555296d63f101", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "0x22 0x0020230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "HASH160 0x14 0x3478e7019ce61a68148f87549579b704cbe4c393 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "OK", + "P2SH(P2WSH) CHECKMULTISIG with second key uncompressed and signing with the first key should pass as the uncompressed key is not used" + ], + [ + [ + "", + "304402206c6d9f5daf85b54af2a93ec38b15ab27f205dbf5c735365ff12451e43613d1f40220736a44be63423ed5ebf53491618b7cc3d8a5093861908da853739c73717938b701", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "", + "0 0x20 0x230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "P2SH,WITNESS", + "OK", + "P2WSH CHECKMULTISIG with second key uncompressed and signing with the second key" + ], + [ + [ + "", + "30440220687871bc6144012d75baf585bb26ce13997f7d8c626f4d8825b069c3b2d064470220108936fe1c57327764782253e99090b09c203ec400ed35ce9e026ce2ecf842a001", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "0x22 0x0020230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "HASH160 0x14 0x3478e7019ce61a68148f87549579b704cbe4c393 EQUAL", + "P2SH,WITNESS", + "OK", + "P2SH(P2WSH) CHECKMULTISIG with second key uncompressed and signing with the second key" + ], + [ + [ + "", + "304402206c6d9f5daf85b54af2a93ec38b15ab27f205dbf5c735365ff12451e43613d1f40220736a44be63423ed5ebf53491618b7cc3d8a5093861908da853739c73717938b701", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "", + "0 0x20 0x230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "P2WSH CHECKMULTISIG with second key uncompressed and signing with the second key" + ], + [ + [ + "", + "30440220687871bc6144012d75baf585bb26ce13997f7d8c626f4d8825b069c3b2d064470220108936fe1c57327764782253e99090b09c203ec400ed35ce9e026ce2ecf842a001", + "5141048282263212c609d9ea2a6e3e172de238d8c39cabd5ac1ca10646e23fd5f5150811f8a8098557dfe45e8256e830b60ace62d613ac2f7b17bed31b6eaff6e26caf210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179852ae", + 0.00000001 + ], + "0x22 0x0020230828ed48871f0f362ce9432aa52f620f442cc8d9ce7a8b5e798365595a38bb", + "HASH160 0x14 0x3478e7019ce61a68148f87549579b704cbe4c393 EQUAL", + "P2SH,WITNESS,WITNESS_PUBKEYTYPE", + "WITNESS_PUBKEYTYPE", + "P2SH(P2WSH) CHECKMULTISIG with second key uncompressed and signing with the second key" + ], + [ + "CHECKSEQUENCEVERIFY tests" + ], + [ + "", + "CHECKSEQUENCEVERIFY", + "CHECKSEQUENCEVERIFY", + "INVALID_STACK_OPERATION", + "CSV automatically fails on a empty stack" + ], + [ + "-1", + "CHECKSEQUENCEVERIFY", + "CHECKSEQUENCEVERIFY", + "NEGATIVE_LOCKTIME", + "CSV automatically fails if stack top is negative" + ], + [ + "0x0100", + "CHECKSEQUENCEVERIFY", + "CHECKSEQUENCEVERIFY,MINIMALDATA", + "UNKNOWN_ERROR", + "CSV fails if stack top is not minimally encoded" + ], + [ + "0", + "CHECKSEQUENCEVERIFY", + "CHECKSEQUENCEVERIFY", + "UNSATISFIED_LOCKTIME", + "CSV fails if stack top bit 1 << 31 is set and the tx version < 2" + ], + [ + "4294967296", + "CHECKSEQUENCEVERIFY", + "CHECKSEQUENCEVERIFY", + "UNSATISFIED_LOCKTIME", + "CSV fails if stack top bit 1 << 31 is not set, and tx version < 2" + ], + [ + "MINIMALIF tests" + ], + [ + "MINIMALIF is not applied to non-segwit scripts" + ], + [ + "1", + "IF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "2", + "IF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0x02 0x0100", + "IF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0", + "IF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0x01 0x00", + "IF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "1", + "NOTIF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "2", + "NOTIF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0x02 0x0100", + "NOTIF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0", + "NOTIF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0x01 0x00", + "NOTIF 1 ENDIF", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "Normal P2SH IF 1 ENDIF" + ], + [ + "1 0x03 0x635168", + "HASH160 0x14 0xe7309652a8e3f600f06f5d8d52d6df03d2176cc3 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "2 0x03 0x635168", + "HASH160 0x14 0xe7309652a8e3f600f06f5d8d52d6df03d2176cc3 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0x02 0x0100 0x03 0x635168", + "HASH160 0x14 0xe7309652a8e3f600f06f5d8d52d6df03d2176cc3 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0 0x03 0x635168", + "HASH160 0x14 0xe7309652a8e3f600f06f5d8d52d6df03d2176cc3 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0x01 0x00 0x03 0x635168", + "HASH160 0x14 0xe7309652a8e3f600f06f5d8d52d6df03d2176cc3 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0x03 0x635168", + "HASH160 0x14 0xe7309652a8e3f600f06f5d8d52d6df03d2176cc3 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "UNBALANCED_CONDITIONAL" + ], + [ + "Normal P2SH NOTIF 1 ENDIF" + ], + [ + "1 0x03 0x645168", + "HASH160 0x14 0x0c3f8fe3d6ca266e76311ecda544c67d15fdd5b0 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "2 0x03 0x645168", + "HASH160 0x14 0x0c3f8fe3d6ca266e76311ecda544c67d15fdd5b0 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0x02 0x0100 0x03 0x645168", + "HASH160 0x14 0x0c3f8fe3d6ca266e76311ecda544c67d15fdd5b0 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + "0 0x03 0x645168", + "HASH160 0x14 0x0c3f8fe3d6ca266e76311ecda544c67d15fdd5b0 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0x01 0x00 0x03 0x645168", + "HASH160 0x14 0x0c3f8fe3d6ca266e76311ecda544c67d15fdd5b0 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + "0x03 0x645168", + "HASH160 0x14 0x0c3f8fe3d6ca266e76311ecda544c67d15fdd5b0 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "UNBALANCED_CONDITIONAL" + ], + [ + "P2WSH IF 1 ENDIF" + ], + [ + [ + "01", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "02", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "0100", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "00", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "01", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + [ + "02", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "0100", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + [ + "00", + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS", + "UNBALANCED_CONDITIONAL" + ], + [ + [ + "635168", + 0.00000001 + ], + "", + "0 0x20 0xc7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "P2SH,WITNESS,MINIMALIF", + "UNBALANCED_CONDITIONAL" + ], + [ + "P2WSH NOTIF 1 ENDIF" + ], + [ + [ + "01", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "02", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "0100", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "00", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "01", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + [ + "02", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "0100", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + [ + "00", + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS", + "UNBALANCED_CONDITIONAL" + ], + [ + [ + "645168", + 0.00000001 + ], + "", + "0 0x20 0xf913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "P2SH,WITNESS,MINIMALIF", + "UNBALANCED_CONDITIONAL" + ], + [ + "P2SH-P2WSH IF 1 ENDIF" + ], + [ + [ + "01", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "02", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "0100", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "00", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "01", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + [ + "02", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "0100", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + [ + "00", + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS", + "UNBALANCED_CONDITIONAL" + ], + [ + [ + "635168", + 0.00000001 + ], + "0x22 0x0020c7eaf06d5ae01a58e376e126eb1e6fab2036076922b96b2711ffbec1e590665d", + "HASH160 0x14 0x9b27ee6d9010c21bf837b334d043be5d150e7ba7 EQUAL", + "P2SH,WITNESS,MINIMALIF", + "UNBALANCED_CONDITIONAL" + ], + [ + "P2SH-P2WSH NOTIF 1 ENDIF" + ], + [ + [ + "01", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "02", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "0100", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS", + "EVAL_FALSE" + ], + [ + [ + "", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "00", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS", + "OK" + ], + [ + [ + "01", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS,MINIMALIF", + "EVAL_FALSE" + ], + [ + [ + "02", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "0100", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS,MINIMALIF", + "OK" + ], + [ + [ + "00", + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS,MINIMALIF", + "MINIMALIF" + ], + [ + [ + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS", + "UNBALANCED_CONDITIONAL" + ], + [ + [ + "645168", + 0.00000001 + ], + "0x22 0x0020f913eacf2e38a5d6fc3a8311d72ae704cb83866350a984dd3e5eb76d2a8c28e8", + "HASH160 0x14 0xdbb7d1c0a56b7a9c423300c8cca6e6e065baf1dc EQUAL", + "P2SH,WITNESS,MINIMALIF", + "UNBALANCED_CONDITIONAL" + ], + [ + "NULLFAIL should cover all signatures and signatures only" + ], + [ + "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG", + "OK", + "BIP66 and NULLFAIL-compliant" + ], + [ + "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG,NULLFAIL", + "OK", + "BIP66 and NULLFAIL-compliant" + ], + [ + "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG,NULLFAIL", + "OK", + "BIP66 and NULLFAIL-compliant, not NULLDUMMY-compliant" + ], + [ + "1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG,NULLFAIL,NULLDUMMY", + "SIG_NULLDUMMY", + "BIP66 and NULLFAIL-compliant, not NULLDUMMY-compliant" + ], + [ + "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0x09 0x300602010102010101", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG", + "OK", + "BIP66-compliant but not NULLFAIL-compliant" + ], + [ + "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0x09 0x300602010102010101", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG,NULLFAIL", + "NULLFAIL", + "BIP66-compliant but not NULLFAIL-compliant" + ], + [ + "0 0x09 0x300602010102010101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG", + "OK", + "BIP66-compliant but not NULLFAIL-compliant" + ], + [ + "0 0x09 0x300602010102010101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", + "0x01 0x14 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0x01 0x14 CHECKMULTISIG NOT", + "DERSIG,NULLFAIL", + "NULLFAIL", + "BIP66-compliant but not NULLFAIL-compliant" + ], + [ + "The End" + ] +] diff --git a/pkg/txscript/data/sighash.json b/pkg/txscript/data/sighash.json new file mode 100755 index 0000000..4428892 --- /dev/null +++ b/pkg/txscript/data/sighash.json @@ -0,0 +1,3505 @@ +[ + [ + "raw_transaction, script, input_index, hashType, signature_hash (result)" + ], + [ + "907c2bc503ade11cc3b04eb2918b6f547b0630ab569273824748c87ea14b0696526c66ba740200000004ab65ababfd1f9bdd4ef073c7afc4ae00da8a66f429c917a0081ad1e1dabce28d373eab81d8628de802000000096aab5253ab52000052ad042b5f25efb33beec9f3364e8a9139e8439d9d7e26529c3c30b6c3fd89f8684cfd68ea0200000009ab53526500636a52ab599ac2fe02a526ed040000000008535300516352515164370e010000000003006300ab2ec229", + "", + 2, + 1864164639, + "31af167a6cf3f9d5f6875caa4d31704ceb0eba078d132b78dab52c3b8997317e" + ], + [ + "a0aa3126041621a6dea5b800141aa696daf28408959dfb2df96095db9fa425ad3f427f2f6103000000015360290e9c6063fa26912c2e7fb6a0ad80f1c5fea1771d42f12976092e7a85a4229fdb6e890000000001abc109f6e47688ac0e4682988785744602b8c87228fcef0695085edf19088af1a9db126e93000000000665516aac536affffffff8fe53e0806e12dfd05d67ac68f4768fdbe23fc48ace22a5aa8ba04c96d58e2750300000009ac51abac63ab5153650524aa680455ce7b000000000000499e50030000000008636a00ac526563ac5051ee030000000003abacabd2b6fe000000000003516563910fb6b5", + "65", + 0, + -1391424484, + "48d6a1bd2cd9eec54eb866fc71209418a950402b5d7e52363bfb75c98e141175" + ], + [ + "6e7e9d4b04ce17afa1e8546b627bb8d89a6a7fefd9d892ec8a192d79c2ceafc01694a6a7e7030000000953ac6a51006353636a33bced1544f797f08ceed02f108da22cd24c9e7809a446c61eb3895914508ac91f07053a01000000055163ab516affffffff11dc54eee8f9e4ff0bcf6b1a1a35b1cd10d63389571375501af7444073bcec3c02000000046aab53514a821f0ce3956e235f71e4c69d91abe1e93fb703bd33039ac567249ed339bf0ba0883ef300000000090063ab65000065ac654bec3cc504bcf499020000000005ab6a52abac64eb060100000000076a6a5351650053bbbc130100000000056a6aab53abd6e1380100000000026a51c4e509b8", + "acab655151", + 0, + 479279909, + "2a3d95b09237b72034b23f2d2bb29fa32a58ab5c6aa72f6aafdfa178ab1dd01c" + ], + [ + "73107cbd025c22ebc8c3e0a47b2a760739216a528de8d4dab5d45cbeb3051cebae73b01ca10200000007ab6353656a636affffffffe26816dffc670841e6a6c8c61c586da401df1261a330a6c6b3dd9f9a0789bc9e000000000800ac6552ac6aac51ffffffff0174a8f0010000000004ac52515100000000", + "5163ac63635151ac", + 1, + 1190874345, + "06e328de263a87b09beabe222a21627a6ea5c7f560030da31610c4611f4a46bc" + ], + [ + "e93bbf6902be872933cb987fc26ba0f914fcfc2f6ce555258554dd9939d12032a8536c8802030000000453ac5353eabb6451e074e6fef9de211347d6a45900ea5aaf2636ef7967f565dce66fa451805c5cd10000000003525253ffffffff047dc3e6020000000007516565ac656aabec9eea010000000001633e46e600000000000015080a030000000001ab00000000", + "5300ac6a53ab6a", + 1, + -886562767, + "f03aa4fc5f97e826323d0daa03343ebf8a34ed67a1ce18631f8b88e5c992e798" + ], + [ + "50818f4c01b464538b1e7e7f5ae4ed96ad23c68c830e78da9a845bc19b5c3b0b20bb82e5e9030000000763526a63655352ffffffff023b3f9c040000000008630051516a6a5163a83caf01000000000553ab65510000000000", + "6aac", + 0, + 946795545, + "746306f322de2b4b58ffe7faae83f6a72433c22f88062cdde881d4dd8a5a4e2d" + ], + [ + "a93e93440250f97012d466a6cc24839f572def241c814fe6ae94442cf58ea33eb0fdd9bcc1030000000600636a0065acffffffff5dee3a6e7e5ad6310dea3e5b3ddda1a56bf8de7d3b75889fc024b5e233ec10f80300000007ac53635253ab53ffffffff0160468b04000000000800526a5300ac526a00000000", + "ac00636a53", + 1, + 1773442520, + "5c9d3a2ce9365bb72cfabbaa4579c843bb8abf200944612cf8ae4b56a908bcbd" + ], + [ + "ce7d371f0476dda8b811d4bf3b64d5f86204725deeaa3937861869d5b2766ea7d17c57e40b0100000003535265ffffffff7e7e9188f76c34a46d0bbe856bde5cb32f089a07a70ea96e15e92abb37e479a10100000006ab6552ab655225bcab06d1c2896709f364b1e372814d842c9c671356a1aa5ca4e060462c65ae55acc02d0000000006abac0063ac5281b33e332f96beebdbc6a379ebe6aea36af115c067461eb99d22ba1afbf59462b59ae0bd0200000004ab635365be15c23801724a1704000000000965006a65ac00000052ca555572", + "53ab530051ab", + 1, + 2030598449, + "c336b2f7d3702fbbdeffc014d106c69e3413c7c71e436ba7562d8a7a2871f181" + ], + [ + "d3b7421e011f4de0f1cea9ba7458bf3486bee722519efab711a963fa8c100970cf7488b7bb0200000003525352dcd61b300148be5d05000000000000000000", + "535251536aac536a", + 0, + -1960128125, + "29aa6d2d752d3310eba20442770ad345b7f6a35f96161ede5f07b33e92053e2a" + ], + [ + "04bac8c5033460235919a9c63c42b2db884c7c8f2ed8fcd69ff683a0a2cccd9796346a04050200000003655351fcad3a2c5a7cbadeb4ec7acc9836c3f5c3e776e5c566220f7f965cf194f8ef98efb5e3530200000007526a006552526526a2f55ba5f69699ece76692552b399ba908301907c5763d28a15b08581b23179cb01eac03000000075363ab6a516351073942c2025aa98a05000000000765006aabac65abd7ffa6030000000004516a655200000000", + "53ac6365ac526a", + 1, + 764174870, + "bf5fdc314ded2372a0ad078568d76c5064bf2affbde0764c335009e56634481b" + ], + [ + "c363a70c01ab174230bbe4afe0c3efa2d7f2feaf179431359adedccf30d1f69efe0c86ed390200000002ab51558648fe0231318b04000000000151662170000000000008ac5300006a63acac00000000", + "", + 0, + 2146479410, + "191ab180b0d753763671717d051f138d4866b7cb0d1d4811472e64de595d2c70" + ], + [ + "8d437a7304d8772210a923fd81187c425fc28c17a5052571501db05c7e89b11448b36618cd02000000026a6340fec14ad2c9298fde1477f1e8325e5747b61b7e2ff2a549f3d132689560ab6c45dd43c3010000000963ac00ac000051516a447ed907a7efffebeb103988bf5f947fc688aab2c6a7914f48238cf92c337fad4a79348102000000085352ac526a5152517436edf2d80e3ef06725227c970a816b25d0b58d2cd3c187a7af2cea66d6b27ba69bf33a0300000007000063ab526553f3f0d6140386815d030000000003ab6300de138f00000000000900525153515265abac1f87040300000000036aac6500000000", + "51", + 3, + -315779667, + "b6632ac53578a741ae8c36d8b69e79f39b89913a2c781cdf1bf47a8c29d997a5" + ], + [ + "fd878840031e82fdbe1ad1d745d1185622b0060ac56638290ec4f66b1beef4450817114a2c0000000009516a63ab53650051abffffffff37b7a10322b5418bfd64fb09cd8a27ddf57731aeb1f1f920ffde7cb2dfb6cdb70300000008536a5365ac53515369ecc034f1594690dbe189094dc816d6d57ea75917de764cbf8eccce4632cbabe7e116cd0100000003515352ffffffff035777fc000000000003515200abe9140300000000050063005165bed6d10200000000076300536363ab65195e9110", + "635265", + 0, + 1729787658, + "6e3735d37a4b28c45919543aabcb732e7a3e1874db5315abb7cc6b143d62ff10" + ], + [ + "f40a750702af06efff3ea68e5d56e42bc41cdb8b6065c98f1221fe04a325a898cb61f3d7ee030000000363acacffffffffb5788174aef79788716f96af779d7959147a0c2e0e5bfb6c2dba2df5b4b97894030000000965510065535163ac6affffffff0445e6fd0200000000096aac536365526a526aa6546b000000000008acab656a6552535141a0fd010000000000c897ea030000000008526500ab526a6a631b39dba3", + "00abab5163ac", + 1, + -1778064747, + "d76d0fc0abfa72d646df888bce08db957e627f72962647016eeae5a8412354cf" + ], + [ + "a63bc673049c75211aa2c09ecc38e360eaa571435fedd2af1116b5c1fa3d0629c269ecccbf0000000008ac65ab516352ac52ffffffffbf1a76fdda7f451a5f0baff0f9ccd0fe9136444c094bb8c544b1af0fa2774b06010000000463535253ffffffff13d6b7c3ddceef255d680d87181e100864eeb11a5bb6a3528cb0d70d7ee2bbbc02000000056a0052abab951241809623313b198bb520645c15ec96bfcc74a2b0f3db7ad61d455cc32db04afc5cc702000000016309c9ae25014d9473020000000004abab6aac3bb1e803", + "", + 3, + -232881718, + "6e48f3da3a4ac07eb4043a232df9f84e110485d7c7669dd114f679c27d15b97e" + ], + [ + "4c565efe04e7d32bac03ae358d63140c1cfe95de15e30c5b84f31bb0b65bb542d637f49e0f010000000551abab536348ae32b31c7d3132030a510a1b1aacf7b7c3f19ce8dc49944ef93e5fa5fe2d356b4a73a00100000009abac635163ac00ab514c8bc57b6b844e04555c0a4f4fb426df139475cd2396ae418bc7015820e852f711519bc202000000086a00510000abac52488ff4aec72cbcfcc98759c58e20a8d2d9725aa4a80f83964e69bc4e793a4ff25cd75dc701000000086a52ac6aac5351532ec6b10802463e0200000000000553005265523e08680100000000002f39a6b0", + "", + 3, + 70712784, + "c6076b6a45e6fcfba14d3df47a34f6aadbacfba107e95621d8d7c9c0e40518ed" + ], + [ + "1233d5e703403b3b8b4dae84510ddfc126b4838dcb47d3b23df815c0b3a07b55bf3098110e010000000163c5c55528041f480f40cf68a8762d6ed3efe2bd402795d5233e5d94bf5ddee71665144898030000000965525165655151656affffffff6381667e78bb74d0880625993bec0ea3bd41396f2bcccc3cc097b240e5e92d6a01000000096363acac6a63536365ffffffff04610ad60200000000065251ab65ab52e90d680200000000046351516ae30e98010000000008abab52520063656a671856010000000004ac6aac514c84e383", + "6aabab636300", + 1, + -114996813, + "aeb8c5a62e8a0b572c28f2029db32854c0b614dbecef0eaa726abebb42eebb8d" + ], + [ + "0c69702103b25ceaed43122cc2672de84a3b9aa49872f2a5bb458e19a52f8cc75973abb9f102000000055365656aacffffffff3ffb1cf0f76d9e3397de0942038c856b0ebbea355dc9d8f2b06036e19044b0450100000000ffffffff4b7793f4169617c54b734f2cd905ed65f1ce3d396ecd15b6c426a677186ca0620200000008655263526551006a181a25b703240cce0100000000046352ab53dee22903000000000865526a6a516a51005e121602000000000852ab52ababac655200000000", + "6a516aab63", + 1, + -2040012771, + "a6e6cb69f409ec14e10dd476f39167c29e586e99bfac93a37ed2c230fcc1dbbe" + ], + [ + "fd22692802db8ae6ab095aeae3867305a954278f7c076c542f0344b2591789e7e33e4d29f4020000000151ffffffffb9409129cfed9d3226f3b6bab7a2c83f99f48d039100eeb5796f00903b0e5e5e0100000006656552ac63abd226abac0403e649000000000007abab51ac5100ac8035f10000000000095165006a63526a52510d42db030000000007635365ac6a63ab24ef5901000000000453ab6a0000000000", + "536a52516aac6a", + 1, + 309309168, + "7ca0f75e6530ec9f80d031fc3513ca4ecd67f20cb38b4dacc6a1d825c3cdbfdb" + ], + [ + "a43f85f701ffa54a3cc57177510f3ea28ecb6db0d4431fc79171cad708a6054f6e5b4f89170000000008ac6a006a536551652bebeaa2013e779c05000000000665ac5363635100000000", + "ac", + 0, + 2028978692, + "58294f0d7f2e68fe1fd30c01764fe1619bcc7961d68968944a0e263af6550437" + ], + [ + "c2b0b99001acfecf7da736de0ffaef8134a9676811602a6299ba5a2563a23bb09e8cbedf9300000000026300ffffffff042997c50300000000045252536a272437030000000007655353ab6363ac663752030000000002ab6a6d5c900000000000066a6a5265abab00000000", + "52ac525163515251", + 0, + -894181723, + "8b300032a1915a4ac05cea2f7d44c26f2a08d109a71602636f15866563eaafdc" + ], + [ + "82f9f10304c17a9d954cf3380db817814a8c738d2c811f0412284b2c791ec75515f38c4f8c020000000265ab5729ca7db1b79abee66c8a757221f29280d0681355cb522149525f36da760548dbd7080a0100000001510b477bd9ce9ad5bb81c0306273a3a7d051e053f04ecf3a1dbeda543e20601a5755c0cfae030000000451ac656affffffff71141a04134f6c292c2e0d415e6705dfd8dcee892b0d0807828d5aeb7d11f5ef0300000001520b6c6dc802a6f3dd0000000000056aab515163bfb6800300000000015300000000", + "", + 3, + -635779440, + "d55ed1e6c53510f2608716c12132a11fb5e662ec67421a513c074537eeccc34b" + ], + [ + "8edcf5a1014b604e53f0d12fe143cf4284f86dc79a634a9f17d7e9f8725f7beb95e8ffcd2403000000046aabac52ffffffff01c402b5040000000005ab6a63525100000000", + "6351525251acabab6a", + 0, + 1520147826, + "2765bbdcd3ebb8b1a316c04656b28d637f80bffbe9b040661481d3dc83eea6d6" + ], + [ + "2074bad5011847f14df5ea7b4afd80cd56b02b99634893c6e3d5aaad41ca7c8ee8e5098df003000000026a6affffffff018ad59700000000000900ac656a526551635300000000", + "65635265", + 0, + -1804671183, + "663c999a52288c9999bff36c9da2f8b78d5c61b8347538f76c164ccba9868d0a" + ], + [ + "7100b11302e554d4ef249ee416e7510a485e43b2ba4b8812d8fe5529fe33ea75f36d392c4403000000020000ffffffff3d01a37e075e9a7715a657ae1bdf1e44b46e236ad16fd2f4c74eb9bf370368810000000007636553ac536365ffffffff01db696a0400000000065200ac656aac00000000", + "63005151", + 0, + -1210499507, + "b9c3aee8515a4a3b439de1ffc9c156824bda12cb75bfe5bc863164e8fd31bd7a" + ], + [ + "02c1017802091d1cb08fec512db7b012fe4220d57a5f15f9e7676358b012786e1209bcff950100000004acab6352ffffffff799bc282724a970a6fea1828984d0aeb0f16b67776fa213cbdc4838a2f1961a3010000000951516a536552ab6aabffffffff016c7b4b03000000000865abac5253ac5352b70195ad", + "65655200516a", + 0, + -241626954, + "be567cb47170b34ff81c66c1142cb9d27f9b6898a384d6dfc4fce16b75b6cb14" + ], + [ + "cb3178520136cd294568b83bb2520f78fecc507898f4a2db2674560d72fd69b9858f75b3b502000000066aac00515100ffffffff03ab005a01000000000563526363006e3836030000000001abfbda3200000000000665ab0065006500000000", + "ab516a0063006a5300", + 0, + 1182109299, + "2149e79c3f4513da4e4378608e497dcfdfc7f27c21a826868f728abd2b8a637a" + ], + [ + "18a4b0c004702cf0e39686ac98aab78ad788308f1d484b1ddfe70dc1997148ba0e28515c310300000000ffffffff05275a52a23c59da91129093364e275da5616c4070d8a05b96df5a2080ef259500000000096aac51656a6aac53ab66e64966b3b36a07dd2bb40242dd4a3743d3026e7e1e0d9e9e18f11d068464b989661321030000000265ac383339c4fae63379cafb63b0bab2eca70e1f5fc7d857eb5c88ccd6c0465093924bba8b2a000000000300636ab5e0545402bc2c4c010000000000cd41c002000000000000000000", + "abac635253656a00", + 3, + 2052372230, + "32db877b6b1ca556c9e859442329406f0f8246706522369839979a9f7a235a32" + ], + [ + "1d9c5df20139904c582285e1ea63dec934251c0f9cf5c47e86abfb2b394ebc57417a81f67c010000000353515222ba722504800d3402000000000353656a3c0b4a0200000000000fb8d20500000000076300ab005200516462f30400000000015200000000", + "ab65", + 0, + -210854112, + "edf73e2396694e58f6b619f68595b0c1cdcb56a9b3147845b6d6afdb5a80b736" + ], + [ + "4504cb1904c7a4acf375ddae431a74de72d5436efc73312cf8e9921f431267ea6852f9714a01000000066a656a656553a2fbd587c098b3a1c5bd1d6480f730a0d6d9b537966e20efc0e352d971576d0f87df0d6d01000000016321aeec3c4dcc819f1290edb463a737118f39ab5765800547522708c425306ebfca3f396603000000055300ac656a1d09281d05bfac57b5eb17eb3fa81ffcedfbcd3a917f1be0985c944d473d2c34d245eb350300000007656a51525152ac263078d9032f470f0500000000066aac00000052e12da60200000000003488410200000000076365006300ab539981e432", + "52536a52526a", + 1, + -31909119, + "f0a2deee7fd8a3a9fad6927e763ded11c940ee47e9e6d410f94fda5001f82e0c" + ], + [ + "14bc7c3e03322ec0f1311f4327e93059c996275302554473104f3f7b46ca179bfac9ef753503000000016affffffff9d405eaeffa1ca54d9a05441a296e5cc3a3e32bb8307afaf167f7b57190b07e00300000008abab51ab5263abab45533aa242c61bca90dd15d46079a0ab0841d85df67b29ba87f2393cd764a6997c372b55030000000452005263ffffffff0250f40e02000000000651516a0063630e95ab0000000000046a5151ac00000000", + "6a65005151", + 0, + -1460947095, + "aa418d096929394c9147be8818d8c9dafe6d105945ab9cd7ec682df537b5dd79" + ], + [ + "2b3bd0dd04a1832f893bf49a776cd567ec4b43945934f4786b615d6cb850dfc0349b33301a000000000565ac000051cf80c670f6ddafab63411adb4d91a69c11d9ac588898cbfb4cb16061821cc104325c895103000000025163ffffffffa9e2d7506d2d7d53b882bd377bbcc941f7a0f23fd15d2edbef3cd9df8a4c39d10200000009ac63006a52526a5265ffffffff44c099cdf10b10ce87d4b38658d002fd6ea17ae4a970053c05401d86d6e75f99000000000963ab53526a5252ab63ffffffff035af69c01000000000100ba9b8b0400000000004cead10500000000026a520b77d667", + "ab52abac526553", + 3, + -1955078165, + "eb9ceecc3b401224cb79a44d23aa8f428e29f1405daf69b4e01910b848ef1523" + ], + [ + "35df11f004a48ba439aba878fe9df20cc935b4a761c262b1b707e6f2b33e2bb7565cd68b130000000000ffffffffb2a2f99abf64163bb57ca900500b863f40c02632dfd9ea2590854c5fb4811da90200000006ac006363636affffffffaf9d89b2a8d2670ca37c8f7c140600b81259f2e037cb4590578ec6e37af8bf200000000005abac6a655270a4751eb551f058a93301ffeda2e252b6614a1fdd0e283e1d9fe53c96c5bbaafaac57b8030000000153ffffffff020d9f3b02000000000100ed7008030000000004abac000000000000", + "abac", + 3, + 593793071, + "88fdee1c2d4aeead71d62396e28dc4d00e5a23498eea66844b9f5d26d1f21042" + ], + [ + "a08ff466049fb7619e25502ec22fedfb229eaa1fe275aa0b5a23154b318441bf547989d0510000000005ab5363636affffffff2b0e335cb5383886751cdbd993dc0720817745a6b1c9b8ab3d15547fc9aafd03000000000965656a536a52656a532b53d10584c290d3ac1ab74ab0a19201a4a039cb59dc58719821c024f6bf2eb26322b33f010000000965ac6aac0053ab6353ffffffff048decba6ebbd2db81e416e39dde1f821ba69329725e702bcdea20c5cc0ecc6402000000086363ab5351ac6551466e377b0468c0fa00000000000651ab53ac6a513461c6010000000008636a636365535100eeb3dc010000000006526a52ac516a43f362010000000005000063536500000000", + "0063516a", + 1, + -1158911348, + "f6a1ecb50bd7c2594ebecea5a1aa23c905087553e40486dade793c2f127fdfae" + ], + [ + "5ac2f17d03bc902e2bac2469907ec7d01a62b5729340bc58c343b7145b66e6b97d434b30fa000000000163ffffffff44028aa674192caa0d0b4ebfeb969c284cb16b80c312d096efd80c6c6b094cca000000000763acabac516a52ffffffff10c809106e04b10f9b43085855521270fb48ab579266e7474657c6c625062d2d030000000351636595a0a97004a1b69603000000000465ab005352ad68010000000008636a5263acac5100da7105010000000002acab90325200000000000000000000", + "6a6aab516a63526353", + 2, + 1518400956, + "f7efb74b1dcc49d316b49c632301bc46f98d333c427e55338be60c7ef0d953be" + ], + [ + "aeb2e11902dc3770c218b97f0b1960d6ee70459ecb6a95eff3f05295dc1ef4a0884f10ba460300000005516352526393e9b1b3e6ae834102d699ddd3845a1e159aa7cf7635edb5c02003f7830fee3788b795f20100000009ab006a526553ac006ad8809c570469290e0400000000050000abab00b10fd5040000000008ab655263abac53ab630b180300000000009d9993040000000002516300000000", + "5351ababac6a65", + 0, + 1084852870, + "f2286001af0b0170cbdad92693d0a5ebaa8262a4a9d66e002f6d79a8c94026d1" + ], + [ + "9860ca9a0294ff4812534def8c3a3e3db35b817e1a2ddb7f0bf673f70eab71bb79e90a2f3100000000086a636551acac5165ffffffffed4d6d3cd9ff9b2d490e0c089739121161a1445844c3e204296816ab06e0a83702000000035100ac88d0db5201c3b59a050000000005ac6a0051ab00000000", + "535263ab006a526aab", + 1, + -962088116, + "30df2473e1403e2b8e637e576825f785528d998af127d501556e5f7f5ed89a2a" + ], + [ + "4ddaa680026ec4d8060640304b86823f1ac760c260cef81d85bd847952863d629a3002b54b0200000008526365636a656aab65457861fc6c24bdc760c8b2e906b6656edaf9ed22b5f50e1fb29ec076ceadd9e8ebcb6b000000000152ffffffff033ff04f00000000000551526a00657a1d900300000000002153af040000000003006a6300000000", + "ab526a53acabab", + 0, + 1055317633, + "7f21b62267ed52462e371a917eb3542569a4049b9dfca2de3c75872b39510b26" + ], + [ + "01e76dcd02ad54cbc8c71d68eaf3fa7c883b65d74217b30ba81f1f5144ef80b706c0dc82ca000000000352ab6a078ec18bcd0514825feced2e8b8ea1ccb34429fae41c70cc0b73a2799e85603613c6870002000000086363ab6365536a53ffffffff043acea90000000000016ad20e1803000000000100fa00830200000000056352515351e864ee00000000000865535253ab6a6551d0c46672", + "6a6365abacab", + 0, + -1420559003, + "8af0b4cbdbc011be848edf4dbd2cde96f0578d662cfebc42252495387114224a" + ], + [ + "fa00b26402670b97906203434aa967ce1559d9bd097d56dbe760469e6032e7ab61accb54160100000006635163630052fffffffffe0d3f4f0f808fd9cfb162e9f0c004601acf725cd7ea5683bbdc9a9a433ef15a0200000005ab52536563d09c7bef049040f305000000000153a7c7b9020000000004ac63ab52847a2503000000000553ab00655390ed80010000000005006553ab52860671d4", + "536565ab52", + 0, + 799022412, + "40ed8e7bbbd893e15f3cce210ae02c97669818de5946ca37eefc7541116e2c78" + ], + [ + "cb5c06dc01b022ee6105ba410f0eb12b9ce5b5aa185b28532492d839a10cef33d06134b91b010000000153ffffffff02cec0530400000000005e1e4504000000000865656551acacac6a00000000", + "ab53", + 0, + -1514251329, + "136beb95459fe6b126cd6cefd54eb5d971524b0e883e41a292a78f78015cb8d5" + ], + [ + "f10a0356031cd569d652dbca8e7a4d36c8da33cdff428d003338602b7764fe2c96c505175b010000000465ac516affffffffbb54563c71136fa944ee20452d78dc87073ac2365ba07e638dce29a5d179da600000000003635152ffffffff9a411d8e2d421b1e6085540ee2809901e590940bbb41532fa38bd7a16b68cc350100000007535251635365636195df1603b61c45010000000002ab65bf6a310400000000026352fcbba10200000000016aa30b7ff0", + "5351", + 0, + 1552495929, + "9eb8adf2caecb4bf9ac59d7f46bd20e83258472db2f569ee91aba4cf5ee78e29" + ], + [ + "c3325c9b012f659466626ca8f3c61dfd36f34670abc054476b7516a1839ec43cd0870aa0c0000000000753525265005351e7e3f04b0112650500000000000363ac6300000000", + "acac", + 0, + -68961433, + "5ca70e727d91b1a42b78488af2ed551642c32d3de4712a51679f60f1456a8647" + ], + [ + "2333e54c044370a8af16b9750ac949b151522ea6029bacc9a34261599549581c7b4e5ece470000000007510052006563abffffffff80630fc0155c750ce20d0ca4a3d0c8e8d83b014a5b40f0b0be0dd4c63ac28126020000000465000000ffffffff1b5f1433d38cdc494093bb1d62d84b10abbdae57e3d04e82e600857ab3b1dc990300000003515100b76564be13e4890a908ea7508afdad92ec1b200a9a67939fadce6eb7a29eb4550a0a28cb0300000001acffffffff02926c930300000000016373800201000000000153d27ee740", + "ab6365ab516a53", + 3, + 598653797, + "2be27a686eb7940dd32c44ff3a97c1b28feb7ab9c5c0b1593b2d762361cfc2db" + ], + [ + "b500ca48011ec57c2e5252e5da6432089130603245ffbafb0e4c5ffe6090feb629207eeb0e010000000652ab6a636aab8302c9d2042b44f40500000000015278c05a050000000004ac5251524be080020000000007636aac63ac5252c93a9a04000000000965ab6553636aab5352d91f9ddb", + "52005100", + 0, + -2024394677, + "49c8a6940a461cc7225637f1e512cdd174c99f96ec05935a59637ededc77124c" + ], + [ + "f52ff64b02ee91adb01f3936cc42e41e1672778962b68cf013293d649536b519bc3271dd2c00000000020065afee11313784849a7c15f44a61cd5fd51ccfcdae707e5896d131b082dc9322a19e12858501000000036aac654e8ca882022deb7c020000000006006a515352abd3defc0000000000016300000000", + "63520063", + 0, + 1130989496, + "7f208df9a5507e98c62cebc5c1e2445eb632e95527594929b9577b53363e96f6" + ], + [ + "ab7d6f36027a7adc36a5cf7528fe4fb5d94b2c96803a4b38a83a675d7806dda62b380df86a0000000003000000ffffffff5bc00131e29e22057c04be854794b4877dda42e416a7a24706b802ff9da521b20000000007ac6a0065ac52ac957cf45501b9f06501000000000500ac6363ab25f1110b", + "00526500536a635253", + 0, + 911316637, + "5fa09d43c8aef6f6fa01c383a69a5a61a609cd06e37dce35a39dc9eae3ddfe6c" + ], + [ + "f940888f023dce6360263c850372eb145b864228fdbbb4c1186174fa83aab890ff38f8c9a90300000000ffffffff01e80ccdb081e7bbae1c776531adcbfb77f2e5a7d0e5d0d0e2e6c8758470e85f00000000020053ffffffff03b49088050000000004656a52ab428bd604000000000951630065ab63ac636a0cbacf0400000000070063ac5265ac53d6e16604", + "ac63", + 0, + 39900215, + "713ddeeefcfe04929e7b6593c792a4efbae88d2b5280d1f0835d2214eddcbad6" + ], + [ + "530ecd0b01ec302d97ef6f1b5a6420b9a239714013e20d39aa3789d191ef623fc215aa8b940200000005ac5351ab6a3823ab8202572eaa04000000000752ab6a51526563fd8a270100000000036a006581a798f0", + "525153656a0063", + 0, + 1784562684, + "fe42f73a8742676e640698222b1bd6b9c338ff1ccd766d3d88d7d3c6c6ac987e" + ], + [ + "5d781d9303acfcce964f50865ddfddab527ea971aee91234c88e184979985c00b4de15204b0100000003ab6352a009c8ab01f93c8ef2447386c434b4498538f061845862c3f9d5751ad0fce52af442b3a902000000045165ababb909c66b5a3e7c81b3c45396b944be13b8aacfc0204f3f3c105a66fa8fa6402f1b5efddb01000000096a65ac636aacab656ac3c677c402b79fa4050000000004006aab5133e35802000000000751ab635163ab0078c2e025", + "6aac51636a6a005265", + 0, + -882306874, + "551ce975d58647f10adefb3e529d9bf9cda34751627ec45e690f135ef0034b95" + ], + [ + "25ee54ef0187387564bb86e0af96baec54289ca8d15e81a507a2ed6668dc92683111dfb7a50100000004005263634cecf17d0429aa4d000000000007636a6aabab5263daa75601000000000251ab4df70a01000000000151980a890400000000065253ac6a006377fd24e3", + "65ab", + 0, + 797877378, + "069f38fd5d47abff46f04ee3ae27db03275e9aa4737fa0d2f5394779f9654845" + ], + [ + "a9c57b1a018551bcbc781b256642532bbc09967f1cbe30a227d352a19365d219d3f11649a3030000000451655352b140942203182894030000000006ab00ac6aab654add350400000000003d379505000000000553abacac00e1739d36", + "5363", + 0, + -1069721025, + "6da32416deb45a0d720a1dbe6d357886eabc44029dd5db74d50feaffbe763245" + ], + [ + "05c4fb94040f5119dc0b10aa9df054871ed23c98c890f1e931a98ffb0683dac45e98619fdc0200000007acab6a525263513e7495651c9794c4d60da835d303eb4ee6e871f8292f6ad0b32e85ef08c9dc7aa4e03c9c010000000500ab52acacfffffffffee953259cf14ced323fe8d567e4c57ba331021a1ef5ac2fa90f7789340d7c550100000007ac6aacac6a6a53ffffffff08d9dc820d00f18998af247319f9de5c0bbd52a475ea587f16101af3afab7c210100000003535363569bca7c0468e34f00000000000863536353ac51ac6584e319010000000006650052ab6a533debea030000000003ac0053ee7070020000000006ac52005253ac00000000", + "6351005253", + 2, + 1386916157, + "76c4013c40bfa1481badd9d342b6d4b8118de5ab497995fafbf73144469e5ff0" + ], + [ + "c95ab19104b63986d7303f4363ca8f5d2fa87c21e3c5d462b99f1ebcb7c402fc012f5034780000000009006aac63ac65655265ffffffffbe91afa68af40a8700fd579c86d4b706c24e47f7379dad6133de389f815ef7f501000000046aac00abffffffff1520db0d81be4c631878494668d258369f30b8f2b7a71e257764e9a27f24b48701000000076a515100535300b0a989e1164db9499845bac01d07a3a7d6d2c2a76e4c04abe68f808b6e2ef5068ce6540e0100000009ac53636a63ab65656affffffff0309aac6050000000005ab6563656a6067e8020000000003ac536aec91c8030000000009655251ab65ac6a53acc7a45bc5", + "63526a65abac", + 1, + 512079270, + "fb7eca81d816354b6aedec8cafc721d5b107336657acafd0d246049556f9e04b" + ], + [ + "ca66ae10049533c2b39f1449791bd6d3f039efe0a121ab7339d39ef05d6dcb200ec3fb2b3b020000000465006a53ffffffff534b8f97f15cc7fb4f4cea9bf798472dc93135cd5b809e4ca7fe4617a61895980100000000ddd83c1dc96f640929dd5e6f1151dab1aa669128591f153310d3993e562cc7725b6ae3d903000000046a52536582f8ccddb8086d8550f09128029e1782c3f2624419abdeaf74ecb24889cc45ac1a64492a0100000002516a4867b41502ee6ccf03000000000752acacab52ab6a4b7ba80000000000075151ab0052536300000000", + "6553", + 2, + -62969257, + "8085e904164ab9a8c20f58f0d387f6adb3df85532e11662c03b53c3df8c943cb" + ], + [ + "ba646d0b0453999f0c70cb0430d4cab0e2120457bb9128ed002b6e9500e9c7f8d7baa20abe0200000001652a4e42935b21db02b56bf6f08ef4be5adb13c38bc6a0c3187ed7f6197607ba6a2c47bc8a03000000040052516affffffffa55c3cbfc19b1667594ac8681ba5d159514b623d08ed4697f56ce8fcd9ca5b0b00000000096a6a5263ac655263ab66728c2720fdeabdfdf8d9fb2bfe88b295d3b87590e26a1e456bad5991964165f888c03a0200000006630051ac00acffffffff0176fafe0100000000070063acac65515200000000", + "63", + 1, + 2002322280, + "9db4e320208185ee70edb4764ee195deca00ba46412d5527d9700c1cf1c3d057" + ], + [ + "2ddb8f84039f983b45f64a7a79b74ff939e3b598b38f436def7edd57282d0803c7ef34968d02000000026a537eb00c4187de96e6e397c05f11915270bcc383959877868ba93bac417d9f6ed9f627a7930300000004516551abffffffffacc12f1bb67be3ae9f1d43e55fda8b885340a0df1175392a8bbd9f959ad3605003000000025163ffffffff02ff0f4700000000000070bd99040000000003ac53abf8440b42", + "", + 2, + -393923011, + "0133f1a161363b71dfb3a90065c7128c56bd0028b558b610142df79e055ab5c7" + ], + [ + "b21fc15403b4bdaa994204444b59323a7b8714dd471bd7f975a4e4b7b48787e720cbd1f5f00000000000ffffffff311533001cb85c98c1d58de0a5fbf27684a69af850d52e22197b0dc941bc6ca9030000000765ab6363ab5351a8ae2c2c7141ece9a4ff75c43b7ea9d94ec79b7e28f63e015ac584d984a526a73fe1e04e0100000007526352536a5365ffffffff02a0a9ea030000000002ab52cfc4f300000000000465525253e8e0f342", + "000000", + 1, + 1305253970, + "d1df1f4bba2484cff8a816012bb6ec91c693e8ca69fe85255e0031711081c46a" + ], + [ + "d1704d6601acf710b19fa753e307cfcee2735eada0d982b5df768573df690f460281aad12d0000000007656300005100acffffffff0232205505000000000351ab632ca1bc0300000000016300000000", + "ac65ab65ab51", + 0, + 165179664, + "40b4f03c68288bdc996011b0f0ddb4b48dc3be6762db7388bdc826113266cd6c" + ], + [ + "d2f6c096025cc909952c2400bd83ac3d532bfa8a1f8f3e73c69b1fd7b8913379793f3ce92202000000076a00ab6a53516ade5332d81d58b22ed47b2a249ab3a2cb3a6ce9a6b5a6810e18e3e1283c1a1b3bd73e3ab00300000002acabffffffff01a9b2d40500000000056352abab00dc4b7f69", + "ab0065", + 0, + -78019184, + "2ef025e907f0fa454a2b48a4f3b81346ba2b252769b5c35d742d0c8985e0bf5e" + ], + [ + "3e6db1a1019444dba461247224ad5933c997256d15c5d37ade3d700506a0ba0a57824930d7010000000852ab6500ab00ac00ffffffff03389242020000000001aba8465a0200000000086a6a636a5100ab52394e6003000000000953ac51526351000053d21d9800", + "abababacab53ab65", + 0, + 1643661850, + "1f8a3aca573a609f4aea0c69522a82fcb4e15835449da24a05886ddc601f4f6a" + ], + [ + "f821a042036ad43634d29913b77c0fc87b4af593ac86e9a816a9d83fd18dfcfc84e1e1d57102000000076a63ac52006351ffffffffbcdaf490fc75086109e2f832c8985716b3a624a422cf9412fe6227c10585d21203000000095252abab5352ac526affffffff2efed01a4b73ad46c7f7bc7fa3bc480f8e32d741252f389eaca889a2e9d2007e000000000353ac53ffffffff032ac8b3020000000009636300000063516300d3d9f2040000000006510065ac656aafa5de0000000000066352ab5300ac9042b57d", + "525365", + 1, + 667065611, + "0d17a92c8d5041ba09b506ddf9fd48993be389d000aad54f9cc2a44fcc70426b" + ], + [ + "58e3f0f704a186ef55d3919061459910df5406a9121f375e7502f3be872a449c3f2bb058380100000000f0e858da3ac57b6c973f889ad879ffb2bd645e91b774006dfa366c74e2794aafc8bbc871010000000751ac65516a515131a68f120fd88ca08687ceb4800e1e3fbfea7533d34c84fef70cc5a96b648d580369526d000000000600ac00515363f6191d5b3e460fa541a30a6e83345dedfa3ed31ad8574d46d7bbecd3c9074e6ba5287c24020000000151e3e19d6604162602010000000004005100ac71e17101000000000065b5e90300000000040053ab53f6b7d101000000000200ac00000000", + "6563ab", + 1, + -669018604, + "8221d5dfb75fc301a80e919e158e0b1d1e86ffb08870a326c89408d9bc17346b" + ], + [ + "efec1cce044a676c1a3d973f810edb5a9706eb4cf888a240f2b5fb08636bd2db482327cf500000000005ab51656a52ffffffff46ef019d7c03d9456e5134eb0a7b5408d274bd8e33e83df44fab94101f7c5b650200000009ac5100006353630051407aadf6f5aaffbd318fdbbc9cae4bd883e67d524df06bb006ce2f7c7e2725744afb76960100000005536aab53acec0d64eae09e2fa1a7c4960354230d51146cf6dc45ee8a51f489e20508a785cbe6ca86fc000000000651536a516300ffffffff014ef598020000000006636aac655265a6ae1b75", + "53516a5363526563ab", + 2, + -1823982010, + "13e8b5ab4e5b2ceeff0045c625e19898bda2d39fd7af682e2d1521303cfe1154" + ], + [ + "3c436c2501442a5b700cbc0622ee5143b34b1b8021ea7bbc29e4154ab1f5bdfb3dff9d640501000000086aab5251ac5252acffffffff0170b9a20300000000066aab6351525114b13791", + "63acabab52ab51ac65", + 0, + -2140612788, + "87ddf1f9acb6640448e955bd1968f738b4b3e073983af7b83394ab7557f5cd61" + ], + [ + "d62f183e037e0d52dcf73f9b31f70554bce4f693d36d17552d0e217041e01f15ad3840c838000000000963acac6a6a6a63ab63ffffffffabdfb395b6b4e63e02a763830f536fc09a35ff8a0cf604021c3c751fe4c88f4d0300000006ab63ab65ac53aa4d30de95a2327bccf9039fb1ad976f84e0b4a0936d82e67eafebc108993f1e57d8ae39000000000165ffffffff04364ad30500000000036a005179fd84010000000007ab636aac6363519b9023030000000008510065006563ac6acd2a4a02000000000000000000", + "52", + 1, + 595020383, + "da8405db28726dc4e0f82b61b2bfd82b1baa436b4e59300305cc3b090b157504" + ], + [ + "44c200a5021238de8de7d80e7cce905606001524e21c8d8627e279335554ca886454d692e6000000000500acac52abbb8d1dc876abb1f514e96b21c6e83f429c66accd961860dc3aed5071e153e556e6cf076d02000000056553526a51870a928d0360a580040000000004516a535290e1e302000000000851ab6a00510065acdd7fc5040000000007515363ab65636abb1ec182", + "6363", + 0, + -785766894, + "ed53cc766cf7cb8071cec9752460763b504b2183442328c5a9761eb005c69501" + ], + [ + "d682d52d034e9b062544e5f8c60f860c18f029df8b47716cabb6c1b4a4b310a0705e754556020000000400656a0016eeb88eef6924fed207fba7ddd321ff3d84f09902ff958c815a2bf2bb692eb52032c4d803000000076365ac516a520099788831f8c8eb2552389839cfb81a9dc55ecd25367acad4e03cfbb06530f8cccf82802701000000085253655300656a53ffffffff02d543200500000000056a510052ac03978b05000000000700ac51525363acfdc4f784", + "", + 2, + -696035135, + "e1a256854099907050cfee7778f2018082e735a1f1a3d91437584850a74c87bb" + ], + [ + "e8c0dec5026575ddf31343c20aeeca8770afb33d4e562aa8ee52eeda6b88806fdfd4fe0a97030000000953acabab65ab516552ffffffffdde122c2c3e9708874286465f8105f43019e837746686f442666629088a970e0010000000153ffffffff01f98eee0100000000025251fe87379a", + "63", + 1, + 633826334, + "abe441209165d25bc6d8368f2e7e7dc21019056719fef1ace45542aa2ef282e2" + ], + [ + "b288c331011c17569293c1e6448e33a64205fc9dc6e35bc756a1ac8b97d18e912ea88dc0770200000007635300ac6aacabfc3c890903a3ccf8040000000004656500ac9c65c9040000000009ab6a6aabab65abac63ac5f7702000000000365005200000000", + "526a63", + 0, + 1574937329, + "0dd1bd5c25533bf5f268aa316ce40f97452cca2061f0b126a59094ca5b65f7a0" + ], + [ + "fc0a092003cb275fa9a25a72cf85d69c19e4590bfde36c2b91cd2c9c56385f51cc545530210000000004ab530063ffffffff729b006eb6d14d6e5e32b1c376acf1c62830a5d9246da38dbdb4db9f51fd1c74020000000463636500ffffffff0ae695c6d12ab7dcb8d3d4b547b03f178c7268765d1de9af8523d244e3836b12030000000151ffffffff0115c1e20100000000066a6aabac6a6a1ff59aec", + "ab0053ac", + 0, + 931831026, + "73fe22099c826c34a74edf45591f5d7b3a888c8178cd08facdfd96a9a681261c" + ], + [ + "0fcae7e004a71a4a7c8f66e9450c0c1785268679f5f1a2ee0fb3e72413d70a9049ecff75de020000000452005251ffffffff99c8363c4b95e7ec13b8c017d7bb6e80f7c04b1187d6072961e1c2479b1dc0320200000000ffffffff7cf03b3d66ab53ed740a70c5c392b84f780fff5472aee82971ac3bfeeb09b2df0200000006ab5265636a0058e4fe9257d7c7c7e82ff187757c6eadc14cceb6664dba2de03a018095fd3006682a5b9600000000056353536a636de26b2303ff76de010000000001acdc0a2e020000000001ab0a53ed020000000007530063ab51510088417307", + "ac6aacab5165535253", + 2, + -902160694, + "eea96a48ee572aea33d75d0587ce954fcfb425531a7da39df26ef9a6635201be" + ], + [ + "612701500414271138e30a46b7a5d95c70c78cc45bf8e40491dac23a6a1b65a51af04e6b94020000000451655153ffffffffeb72dc0e49b2fad3075c19e1e6e4b387f1365dca43d510f6a02136318ddecb7f0200000003536352e115ffc4f9bae25ef5baf534a890d18106fb07055c4d7ec9553ba89ed1ac2101724e507303000000080063006563acabac2ff07f69a080cf61a9d19f868239e6a4817c0eeb6a4f33fe254045d8af2bca289a8695de0300000000430736c404d317840500000000086a00abac5351ab65306e0503000000000963ab0051536aabab6a6c8aca01000000000565516351ab5dcf960100000000016a00000000", + "ab", + 2, + -604581431, + "5ec805e74ee934aa815ca5f763425785ae390282d46b5f6ea076b6ad6255a842" + ], + [ + "6b68ba00023bb4f446365ea04d68d48539aae66f5b04e31e6b38b594d2723ab82d44512460000000000200acffffffff5dfc6febb484fff69c9eeb7c7eb972e91b6d949295571b8235b1da8955f3137b020000000851ac6352516a535325828c8a03365da801000000000800636aabac6551ab0f594d03000000000963ac536365ac63636a45329e010000000005abac53526a00000000", + "005151", + 0, + 1317038910, + "42f5ba6f5fe1e00e652a08c46715871dc4b40d89d9799fd7c0ea758f86eab6a7" + ], + [ + "aff5850c0168a67296cc790c1b04a9ed9ad1ba0469263a9432fcb53676d1bb4e0eea8ea1410100000005ac65526a537d5fcb1d01d9c26d0200000000065265ab5153acc0617ca1", + "51ab650063", + 0, + 1712981774, + "8449d5247071325e5f8edcc93cb9666c0fecabb130ce0e5bef050575488477eb" + ], + [ + "e6d6b9d8042c27aec99af8c12b6c1f7a80453e2252c02515e1f391da185df0874e133696b50300000006ac5165650065ffffffff6a4b60a5bfe7af72b198eaa3cde2e02aa5fa36bdf5f24ebce79f6ecb51f3b554000000000652656aababac2ec4c5a6cebf86866b1fcc4c5bd5f4b19785a8eea2cdfe58851febf87feacf6f355324a80100000001537100145149ac1e287cef62f6f5343579189fad849dd33f25c25bfca841cb696f10c5a34503000000046a636a63df9d7c4c018d96e20100000000015100000000", + "53ab", + 1, + -1924777542, + "f98f95d0c5ec3ac3e699d81f6c440d2e7843eab15393eb023bc5a62835d6dcea" + ], + [ + "046ac25e030a344116489cc48025659a363da60bc36b3a8784df137a93b9afeab91a04c1ed020000000951ab0000526a65ac51ffffffff6c094a03869fde55b9a8c4942a9906683f0a96e2d3e5a03c73614ea3223b2c29020000000500ab636a6affffffff3da7aa5ecef9071600866267674b54af1740c5aeb88a290c459caa257a2683cb0000000004ab6565ab7e2a1b900301b916030000000005abac63656308f4ed03000000000852ab53ac63ac51ac73d620020000000003ab00008deb1285", + "6a", + 2, + 1299505108, + "f79e6b776e2592bad45ca328c54abf14050c241d8f822d982c36ea890fd45757" + ], + [ + "bd515acd0130b0ac47c2d87f8d65953ec7d657af8d96af584fc13323d0c182a2e5f9a96573000000000652ac51acac65ffffffff0467aade000000000003655363dc577d050000000006515252ab5300137f60030000000007535163530065004cdc860500000000036a5265241bf53e", + "acab", + 0, + 621090621, + "771d4d87f1591a13d77e51858c16d78f1956712fe09a46ff1abcabbc1e7af711" + ], + [ + "ff1ae37103397245ac0fa1c115b079fa20930757f5b6623db3579cb7663313c2dc4a3ffdb300000000076353656a000053ffffffff83c59e38e5ad91216ee1a312d15b4267bae2dd2e57d1a3fd5c2f0f809eeb5d46010000000800abab6a6a53ab51ffffffff9d5e706c032c1e0ca75915f8c6686f64ec995ebcd2539508b7dd8abc3e4d7d2a01000000006b2bdcda02a8fe070500000000045253000019e31d04000000000700ab63acab526a00000000", + "53656aab6a525251", + 0, + 881938872, + "726bb88cdf3af2f7603a31f33d2612562306d08972a4412a55dbbc0e3363721c" + ], + [ + "ff5400dd02fec5beb9a396e1cbedc82bedae09ed44bae60ba9bef2ff375a6858212478844b03000000025253ffffffff01e46c203577a79d1172db715e9cc6316b9cfc59b5e5e4d9199fef201c6f9f0f000000000900ab6552656a5165acffffffff02e8ce62040000000002515312ce3e00000000000251513f119316", + "", + 0, + 1541581667, + "1e0da47eedbbb381b0e0debbb76e128d042e02e65b11125e17fd127305fc65cd" + ], + [ + "28e3daa603c03626ad91ffd0ff927a126e28d29db5012588b829a06a652ea4a8a5732407030200000004ab6552acffffffff8e643146d3d0568fc2ad854fd7864d43f6f16b84e395db82b739f6f5c84d97b40000000004515165526b01c2dc1469db0198bd884e95d8f29056c48d7e74ff9fd37a9dec53e44b8769a6c99c030200000009ab006a516a53630065eea8738901002398000000000007ac5363516a51abeaef12f5", + "52ab52515253ab", + 2, + 1687390463, + "55591346aec652980885a558cc5fc2e3f8d21cbd09f314a798e5a7ead5113ea6" + ], + [ + "b54bf5ac043b62e97817abb892892269231b9b220ba08bc8dbc570937cd1ea7cdc13d9676c010000000451ab5365a10adb7b35189e1e8c00b86250f769319668189b7993d6bdac012800f1749150415b2deb0200000003655300ffffffff60b9f4fb9a7e17069fd00416d421f804e2ef2f2c67de4ca04e0241b9f9c1cc5d0200000003ab6aacfffffffff048168461cce1d40601b42fbc5c4f904ace0d35654b7cc1937ccf53fe78505a0100000008526563525265abacffffffff01dbf4e6040000000007acac656553636500000000", + "63", + 2, + 882302077, + "f5b38b0f06e246e47ce622e5ee27d5512c509f8ac0e39651b3389815eff2ab93" + ], + [ + "ebf628b30360bab3fa4f47ce9e0dcbe9ceaf6675350e638baff0c2c197b2419f8e4fb17e16000000000452516365ac4d909a79be207c6e5fb44fbe348acc42fc7fe7ef1d0baa0e4771a3c4a6efdd7e2c118b0100000003acacacffffffffa6166e9101f03975721a3067f1636cc390d72617be72e5c3c4f73057004ee0ee010000000863636a6a516a5252c1b1e82102d8d54500000000000153324c900400000000015308384913", + "0063516a51", + 1, + -1658428367, + "eb2d8dea38e9175d4d33df41f4087c6fea038a71572e3bad1ea166353bf22184" + ], + [ + "d6a8500303f1507b1221a91adb6462fb62d741b3052e5e7684ea7cd061a5fc0b0e93549fa50100000004acab65acfffffffffdec79bf7e139c428c7cfd4b35435ae94336367c7b5e1f8e9826fcb0ebaaaea30300000000ffffffffd115fdc00713d52c35ea92805414bd57d1e59d0e6d3b79a77ee18a3228278ada020000000453005151ffffffff040231510300000000085100ac6a6a000063c6041c0400000000080000536a6563acac138a0b04000000000263abd25fbe03000000000900656a00656aac510000000000", + "ac526aac6a00", + 1, + -2007972591, + "13d12a51598b34851e7066cd93ab8c5212d60c6ed2dae09d91672c10ccd7f87c" + ], + [ + "658cb1c1049564e728291a56fa79987a4ed3146775fce078bd2e875d1a5ca83baf6166a82302000000056a656351ab2170e7d0826cbdb45fda0457ca7689745fd70541e2137bb4f52e7b432dcfe2112807bd720300000007006a0052536351ffffffff8715ca2977696abf86d433d5c920ef26974f50e9f4a20c584fecbb68e530af5101000000009e49d864155bf1d3c757186d29f3388fd89c7f55cc4d9158b4cf74ca27a35a1dd93f945502000000096a535353ac656351510d29fa870230b809040000000006ab6a6a526a633b41da050000000004ab6a6a65ed63bf62", + "52acabac", + 2, + -1774073281, + "53ab197fa7e27b8a3f99ff48305e67081eb90e95d89d7e92d80cee25a03a6689" + ], + [ + "e92492cc01aec4e62df67ea3bc645e2e3f603645b3c5b353e4ae967b562d23d6e043badecd0100000003acab65ffffffff02c7e5ea040000000002ab52e1e584010000000005536365515195d16047", + "6551", + 0, + -424930556, + "93c34627f526d73f4bea044392d1a99776b4409f7d3d835f23b03c358f5a61c2" + ], + [ + "02e242db04be2d8ced9179957e98cee395d4767966f71448dd084426844cbc6d15f2182e85030000000200650c8ffce3db9de9c3f9cdb9104c7cb26647a7531ad1ebf7591c259a9c9985503be50f8de30000000007ac6a51636a6353ffffffffa2e33e7ff06fd6469987ddf8a626853dbf30c01719efb259ae768f051f803cd30300000000fffffffffd69d8aead941683ca0b1ee235d09eade960e0b1df3cd99f850afc0af1b73e070300000001ab60bb602a011659670100000000076363526300acac00000000", + "6353ab515251", + 3, + 1451100552, + "bbc9069b8615f3a52ac8a77359098dcc6c1ba88c8372d5d5fe080b99eb781e55" + ], + [ + "b28d5f5e015a7f24d5f9e7b04a83cd07277d452e898f78b50aae45393dfb87f94a26ef57720200000008ababac630053ac52ffffffff046475ed040000000008ab5100526363ac65c9834a04000000000251abae26b30100000000040000ac65ceefb900000000000000000000", + "ac6551ac6a536553", + 0, + -1756558188, + "5848d93491044d7f21884eef7a244fe7d38886f8ae60df49ce0dfb2a342cd51a" + ], + [ + "efb8b09801f647553b91922a5874f8e4bb2ed8ddb3536ed2d2ed0698fac5e0e3a298012391030000000952ac005263ac52006affffffff04cdfa0f050000000007ac53ab51abac65b68d1b02000000000553ab65ac00d057d50000000000016a9e1fda010000000007ac63ac536552ac00000000", + "6aac", + 0, + 1947322973, + "603a9b61cd30fcea43ef0a5c18b88ca372690b971b379ee9e01909c336280511" + ], + [ + "68a59fb901c21946797e7d07a4a3ea86978ce43df0479860d7116ac514ba955460bae78fff0000000001abffffffff03979be80100000000036553639300bc040000000008006552006a656565cfa78d0000000000076552acab63ab5100000000", + "ab65ab", + 0, + 995583673, + "3b320dd47f2702452a49a1288bdc74a19a4b849b132b6cad9a1d945d87dfbb23" + ], + [ + "67761f2a014a16f3940dcb14a22ba5dc057fcffdcd2cf6150b01d516be00ef55ef7eb07a830100000004636a6a51ffffffff01af67bd050000000008526553526300510000000000", + "6a00", + 0, + 1570943676, + "079fa62e9d9d7654da8b74b065da3154f3e63c315f25751b4d896733a1d67807" + ], + [ + "e20fe96302496eb436eee98cd5a32e1c49f2a379ceb71ada8a48c5382df7c8cd88bdc47ced03000000016556aa0e180660925a841b457aed0aae47fca2a92fa1d7afeda647abf67198a3902a7c80dd00000000085152ac636a535265bd18335e01803c810100000000046500ac52f371025e", + "6363ab", + 1, + -651254218, + "2921a0e5e3ba83c57ba57c25569380c17986bf34c366ec216d4188d5ba8b0b47" + ], + [ + "4e1bd9fa011fe7aa14eee8e78f27c9fde5127f99f53d86bc67bdab23ca8901054ee8a8b6eb0300000009ac535153006a6a0063ffffffff044233670500000000000a667205000000000652ab636a51abe5bf35030000000003535351d579e505000000000700630065ab51ac3419ac30", + "52abac52", + 0, + -1807563680, + "4aae6648f856994bed252d319932d78db55da50d32b9008216d5366b44bfdf8a" + ], + [ + "ec02fbee03120d02fde12574649660c441b40d330439183430c6feb404064d4f507e704f3c0100000000ffffffffe108d99c7a4e5f75cc35c05debb615d52fac6e3240a6964a29c1704d98017fb60200000002ab63fffffffff726ec890038977adfc9dadbeaf5e486d5fcb65dc23acff0dd90b61b8e2773410000000002ac65e9dace55010f881b010000000005ac00ab650000000000", + "51ac525152ac6552", + 2, + -1564046020, + "3f988922d8cd11c7adff1a83ce9499019e5ab5f424752d8d361cf1762e04269b" + ], + [ + "23dbdcc1039c99bf11938d8e3ccec53b60c6c1d10c8eb6c31197d62c6c4e2af17f52115c3a0300000008636352000063ababffffffff17823880e1df93e63ad98c29bfac12e36efd60254346cac9d3f8ada020afc0620300000003ab63631c26f002ac66e86cd22a25e3ed3cb39d982f47c5118f03253054842daadc88a6c41a2e1500000000096a00ab636a53635163195314de015570fd0100000000096a5263acab5200005300000000", + "ababac6a6553", + 1, + 11586329, + "bd36a50e0e0a4ecbf2709e68daef41eddc1c0c9769efaee57910e99c0a1d1343" + ], + [ + "33b03bf00222c7ca35c2f8870bbdef2a543b70677e413ce50494ac9b22ea673287b6aa55c50000000005ab00006a52ee4d97b527eb0b427e4514ea4a76c81e68c34900a23838d3e57d0edb5410e62eeb8c92b6000000000553ac6aacac42e59e170326245c000000000009656553536aab516aabb1a10603000000000852ab52ab6a516500cc89c802000000000763ac6a63ac516300000000", + "", + 0, + 557416556, + "41bead1b073e1e9fee065dd612a617ca0689e8f9d3fed9d0acfa97398ebb404c" + ], + [ + "813eda1103ac8159850b4524ef65e4644e0fc30efe57a5db0c0365a30446d518d9b9aa8fdd0000000003656565c2f1e89448b374b8f12055557927d5b33339c52228f7108228149920e0b77ef0bcd69da60000000006abac00ab63ab82cdb7978d28630c5e1dc630f332c4245581f787936f0b1e84d38d33892141974c75b4750300000004ac53ab65ffffffff0137edfb02000000000000000000", + "0063", + 1, + -1948560575, + "71dfcd2eb7f2e6473aed47b16a6d5fcbd0af22813d892e9765023151e07771ec" + ], + [ + "9e45d9aa0248c16dbd7f435e8c54ae1ad086de50c7b25795a704f3d8e45e1886386c653fbf01000000025352fb4a1acefdd27747b60d1fb79b96d14fb88770c75e0da941b7803a513e6d4c908c6445c7010000000163ffffffff014069a8010000000001520a794fb3", + "51ac005363", + 1, + -719113284, + "0d31a221c69bd322ef7193dd7359ddfefec9e0a1521d4a8740326d46e44a5d6a" + ], + [ + "36e42018044652286b19a90e5dd4f8d9f361d0760d080c5c5add1970296ff0f1de630233c8010000000200ac39260c7606017d2246ee14ddb7611586178067e6a4be38e788e33f39a3a95a55a13a6775010000000352ac638bea784f7c2354ed02ea0b93f0240cdfb91796fa77649beee6f7027caa70778b091deee700000000066a65ac656363ffffffff4d9d77ab676d711267ef65363f2d192e1bd55d3cd37f2280a34c72e8b4c559d700000000056a006aab00001764e1020d30220100000000085252516aacab0053472097040000000009635353ab6a636a5100a56407a1", + "006a536551ab53ab", + 0, + 827296034, + "daec2af5622bbe220c762da77bab14dc75e7d28aa1ade9b7f100798f7f0fd97a" + ], + [ + "5e06159a02762b5f3a5edcdfc91fd88c3bff08b202e69eb5ba74743e9f4291c4059ab008200000000001ac348f5446bb069ef977f89dbe925795d59fb5d98562679bafd61f5f5f3150c3559582992d0000000008ab5165515353abac762fc67703847ec6010000000000e200cf040000000002abaca64b86010000000008520000515363acabb82b491b", + "ab53525352ab6a", + 0, + -61819505, + "75a7db0df41485a28bf6a77a37ca15fa8eccc95b5d6014a731fd8adb9ada0f12" + ], + [ + "a1948872013b543d6d902ccdeead231c585195214ccf5d39f136023855958436a43266911501000000086aac006a6a6a51514951c9b2038a538a04000000000452526563c0f345050000000007526a5252ac526af9be8e03000000000752acac51ab006306198db2", + "ab6353", + 0, + -326384076, + "ced7ef84aad4097e1eb96310e0d1c8e512cfcb392a01d9010713459b23bc0cf4" + ], + [ + "c3efabba03cb656f154d1e159aa4a1a4bf9423a50454ebcef07bc3c42a35fb8ad84014864d0000000000d1cc73d260980775650caa272e9103dc6408bdacaddada6b9c67c88ceba6abaa9caa2f7d020000000553536a5265ffffffff9f946e8176d9b11ff854b76efcca0a4c236d29b69fb645ba29d406480427438e01000000066a0065005300ffffffff040419c0010000000003ab6a63cdb5b6010000000009006300ab5352656a63f9fe5e050000000004acac5352611b980100000000086a00acac00006a512d7f0c40", + "0053", + 0, + -59089911, + "c503001c16fbff82a99a18d88fe18720af63656fccd8511bca1c3d0d69bd7fc0" + ], + [ + "efb55c2e04b21a0c25e0e29f6586be9ef09f2008389e5257ebf2f5251051cdc6a79fce2dac020000000351006affffffffaba73e5b6e6c62048ba5676d18c33ccbcb59866470bb7911ccafb2238cfd493802000000026563ffffffffe62d7cb8658a6eca8a8babeb0f1f4fa535b62f5fc0ec70eb0111174e72bbec5e0300000009abababac516365526affffffffbf568789e681032d3e3be761642f25e46c20322fa80346c1146cb47ac999cf1b0300000000b3dbd55902528828010000000001ab0aac7b0100000000015300000000", + "acac52", + 3, + 1638140535, + "e84444d91580da41c8a7dcf6d32229bb106f1be0c811b2292967ead5a96ce9d4" + ], + [ + "91d3b21903629209b877b3e1aef09cd59aca6a5a0db9b83e6b3472aceec3bc2109e64ab85a0200000003530065ffffffffca5f92de2f1b7d8478b8261eaf32e5656b9eabbc58dcb2345912e9079a33c4cd010000000700ab65ab00536ad530611da41bbd51a389788c46678a265fe85737b8d317a83a8ff7a839debd18892ae5c80300000007ab6aac65ab51008b86c501038b8a9a05000000000263525b3f7a040000000007ab535353ab00abd4e3ff04000000000665ac51ab65630b7b656f", + "6551525151516a00", + 2, + 499657927, + "ef4bd7622eb7b2bbbbdc48663c1bc90e01d5bde90ff4cb946596f781eb420a0c" + ], + [ + "5d5c41ad0317aa7e40a513f5141ad5fc6e17d3916eebee4ddb400ddab596175b41a111ead20100000005536a5265acffffffff900ecb5e355c5c9f278c2c6ea15ac1558b041738e4bffe5ae06a9346d66d5b2b00000000080000ab636a65ab6affffffff99f4e08305fa5bd8e38fb9ca18b73f7a33c61ff7b3c68e696b30a04fea87f3ca000000000163d3d1760d019fc13a00000000000000000000", + "ab53acabab6aac6a52", + 2, + 1007461922, + "4012f5ff2f1238a0eb84854074670b4703238ebc15bfcdcd47ffa8498105fcd9" + ], + [ + "ceecfa6c02b7e3345445b82226b15b7a097563fa7d15f3b0c979232b138124b62c0be007890200000009abac51536a63525253ffffffffbae481ccb4f15d94db5ec0d8854c24c1cc8642bd0c6300ede98a91ca13a4539a0200000001ac50b0813d023110f5020000000006acabac526563e2b0d0040000000009656aac0063516a536300000000", + "0063526500", + 0, + -1862053821, + "e1600e6df8a6160a79ac32aa40bb4644daa88b5f76c0d7d13bf003327223f70c" + ], + [ + "ae62d5fd0380c4083a26642159f51af24bf55dc69008e6b7769442b6a69a603edd980a33000000000005ab5100ab53ffffffff49d048324d899d4b8ed5e739d604f5806a1104fede4cb9f92cc825a7fa7b4bfe0200000005536a000053ffffffff42e5cea5673c650881d0b4005fa4550fd86de5f21509c4564a379a0b7252ac0e0000000007530000526a53525f26a68a03bfacc3010000000000e2496f000000000009ab5253acac52636563b11cc600000000000700510065526a6a00000000", + "abab", + 1, + -1600104856, + "05cf0ec9c61f1a15f651a0b3c5c221aa543553ce6c804593f43bb5c50bb91ffb" + ], + [ + "f06f64af04fdcb830464b5efdb3d5ee25869b0744005375481d7b9d7136a0eb8828ad1f0240200000003516563fffffffffd3ba192dabe9c4eb634a1e3079fca4f072ee5ceb4b57deb6ade5527053a92c5000000000165ffffffff39f43401a36ba13a5c6dd7f1190e793933ae32ee3bf3e7bfb967be51e681af760300000009650000536552636a528e34f50b21183952cad945a83d4d56294b55258183e1627d6e8fb3beb8457ec36cadb0630000000005abab530052334a7128014bbfd10100000000085352ab006a63656afc424a7c", + "53650051635253ac00", + 2, + 313255000, + "d309da5afd91b7afa257cfd62df3ca9df036b6a9f4b38f5697d1daa1f587312b" + ], + [ + "6dfd2f98046b08e7e2ef5fff153e00545faf7076699012993c7a30cb1a50ec528281a9022f030000000152ffffffff1f535e4851920b968e6c437d84d6ecf586984ebddb7d5db6ae035bd02ba222a8010000000651006a53ab51605072acb3e17939fa0737bc3ee43bc393b4acd58451fc4ffeeedc06df9fc649828822d5010000000253525a4955221715f27788d302382112cf60719be9ae159c51f394519bd5f7e70a4f9816c7020200000009526a6a51636aab656a36d3a5ff0445548e0100000000086a6a00516a52655167030b050000000004ac6a63525cfda8030000000000e158200000000000010000000000", + "535263ac6a65515153", + 3, + 585774166, + "72b7da10704c3ca7d1deb60c31b718ee12c70dc9dfb9ae3461edce50789fe2ba" + ], + [ + "187eafed01389a45e75e9dda526d3acbbd41e6414936b3356473d1f9793d161603efdb45670100000002ab00ffffffff04371c8202000000000563630063523b3bde02000000000753516563006300e9e765010000000005516aac656a373f9805000000000665525352acab08d46763", + "ab", + 0, + 122457992, + "393aa6c758e0eed15fa4af6d9e2d7c63f49057246dbb92b4268ec24fc87301ca" + ], + [ + "7d50b977035d50411d814d296da9f7965ddc56f3250961ca5ba805cadd0454e7c521e31b0300000000003d0416c2cf115a397bacf615339f0e54f6c35ffec95aa009284d38390bdde1595cc7aa7c0100000005ab52ac5365ffffffff4232c6e796544d5ac848c9dc8d25cfa74e32e847a5fc74c74d8f38ca51188562030000000653ac51006a51ffffffff016bd8bb00000000000465ab5253163526f3", + "51ab526a00005353", + 1, + -1311316785, + "60b7544319b42e4159976c35c32c2644f0adf42eff13be1dc2f726fc0b6bb492" + ], + [ + "2a45cd1001bf642a2315d4a427eddcc1e2b0209b1c6abd2db81a800c5f1af32812de42032702000000050051525200ffffffff032177db050000000005530051abac49186f000000000004ab6aab00645c0000000000000765655263acabac00000000", + "6a65", + 0, + -1774715722, + "6a9ac3f7da4c7735fbc91f728b52ecbd602233208f96ac5592656074a5db118a" + ], + [ + "479358c202427f3c8d19e2ea3def6d6d3ef2281b4a93cd76214f0c7d8f040aa042fe19f71f0300000001abffffffffa2709be556cf6ecaa5ef530df9e4d056d0ed57ce96de55a5b1f369fa40d4e74a020000000700006a51635365c426be3f02af578505000000000363ab63fd8f590500000000065153abac53632dfb14b3", + "520063ab51", + 1, + -763226778, + "cfe147982afacde044ce66008cbc5b1e9f0fd9b8ed52b59fc7c0fecf95a39b0e" + ], + [ + "76179a8e03bec40747ad65ab0f8a21bc0d125b5c3c17ad5565556d5cb03ade7c83b4f32d98030000000151ffffffff99b900504e0c02b97a65e24f3ad8435dfa54e3c368f4e654803b756d011d24150200000003ac5353617a04ac61bb6cf697cfa4726657ba35ed0031432da8c0ffb252a190278830f9bd54f0320100000006656551005153c8e8fc8803677c77020000000007ac6553535253ac70f442030000000001535be0f20200000000026300bf46cb3a", + "6aab52", + 1, + -58495673, + "35e94b3776a6729d20aa2f3ddeeb06d3aad1c14cc4cde52fd21a4efc212ea16c" + ], + [ + "75ae53c2042f7546223ce5d5f9e00a968ddc68d52e8932ef2013fa40ce4e8c6ed0b6195cde01000000056563ac630079da0452c20697382e3dba6f4fc300da5f52e95a9dca379bb792907db872ba751b8024ee0300000009655151536500005163ffffffffe091b6d43f51ff00eff0ccfbc99b72d3aff208e0f44b44dfa5e1c7322cfc0c5f01000000075200005363ab63ffffffff7e96c3b83443260ac5cfd18258574fbc4225c630d3950df812bf51dceaeb0f9103000000065365655165639a6bf70b01b3e14305000000000563530063ac00000000", + "6300ab00ac", + 2, + 982422189, + "ee4ea49d2aae0dbba05f0b9785172da54408eb1ec67d36759ff7ed25bfc28766" + ], + [ + "1cdfa01e01e1b8078e9c2b0ca5082249bd18fdb8b629ead659adedf9a0dd5a04031871ba120200000008525351536565ab6affffffff011e28430200000000076a5363636aac52b2febd4a", + "abacac63656300", + 0, + 387396350, + "299dcaac2bdaa627eba0dfd74767ee6c6f27c9200b49da8ff6270b1041669e7e" + ], + [ + "cc28c1810113dfa6f0fcd9c7d9c9a30fb6f1d774356abeb527a8651f24f4e6b25cf763c4e00300000003ab636affffffff02dfc6050000000000080053636351ab0052afd56903000000000453ab5265f6c90d99", + "006551abacacac", + 0, + 1299280838, + "a4c0773204ab418a939e23f493bd4b3e817375d133d307609e9782f2cc38dbcf" + ], + [ + "ca816e7802cd43d66b9374cd9bf99a8da09402d69c688d8dcc5283ace8f147e1672b757e020200000005516aabab5240fb06c95c922342279fcd88ba6cd915933e320d7becac03192e0941e0345b79223e89570300000004005151ac353ecb5d0264dfbd010000000005ac6aacababd5d70001000000000752ac53ac6a5151ec257f71", + "63ac", + 1, + 774695685, + "cc180c4f797c16a639962e7aec58ec4b209853d842010e4d090895b22e7a7863" + ], + [ + "b42b955303942fedd7dc77bbd9040aa0de858afa100f399d63c7f167b7986d6c2377f66a7403000000066aac00525100ffffffff0577d04b64880425a3174055f94191031ad6b4ca6f34f6da9be7c3411d8b51fc000000000300526a6391e1cf0f22e45ef1c44298523b516b3e1249df153590f592fcb5c5fc432dc66f3b57cb03000000046a6aac65ffffffff0393a6c9000000000004516a65aca674ac0400000000046a525352c82c370000000000030053538e577f89", + "", + 1, + -1237094944, + "566953eb806d40a9fb684d46c1bf8c69dea86273424d562bd407b9461c8509af" + ], + [ + "92c9fe210201e781b72554a0ed5e22507fb02434ddbaa69aff6e74ea8bad656071f1923f3f02000000056a63ac6a514470cef985ba83dcb8eee2044807bedbf0d983ae21286421506ae276142359c8c6a34d68020000000863ac63525265006aa796dd0102ca3f9d05000000000800abab52ab535353cd5c83010000000007ac00525252005322ac75ee", + "5165", + 0, + 97879971, + "6e6307cef4f3a9b386f751a6f40acebab12a0e7e17171d2989293cbec7fd45c2" + ], + [ + "ccca1d5b01e40fe2c6b3ee24c660252134601dab785b8f55bd6201ffaf2fddc7b3e2192325030000000365535100496d4703b4b66603000000000665535253ac633013240000000000015212d2a502000000000951abac636353636a5337b82426", + "0052", + 0, + -1691630172, + "577bf2b3520b40aef44899a20d37833f1cded6b167e4d648fc5abe203e43b649" + ], + [ + "bc1a7a3c01691e2d0c4266136f12e391422f93655c71831d90935fbda7e840e50770c61da20000000008635253abac516353ffffffff031f32aa020000000003636563786dbc0200000000003e950f00000000000563516a655184b8a1de", + "51536a", + 0, + -1627072905, + "730bc25699b46703d7718fd5f5c34c4b5f00f594a9968ddc247fa7d5175124ed" + ], + [ + "076d209e02d904a6c40713c7225d23e7c25d4133c3c3477828f98c7d6dbd68744023dbb66b030000000753ab00536565acffffffff10975f1b8db8861ca94c8cc7c7cff086ddcd83e10b5fffd4fc8f2bdb03f9463c0100000000ffffffff029dff76010000000006526365530051a3be6004000000000000000000", + "515253ac65acacac", + 1, + -1207502445, + "66c488603b2bc53f0d22994a1f0f66fb2958203102eba30fe1d37b27a55de7a5" + ], + [ + "690fd1f80476db1f9eebe91317f2f130a60cbc1f4feadd9d6474d438e9cb7f91e4994600af0300000004ab536a63a15ce9fa6622d0c4171d895b42bff884dc6e8a7452f827fdc68a29c3c88e6fdee364eaf50000000002ab52ffffffff022dc39d3c0956b24d7f410b1e387859e7a72955f45d6ffb1e884d77888d18fe0300000005ac6a63656afffffffff10b06bce1800f5c49153d24748fdefb0bf514c12863247d1042d56018c3e25c03000000086a63ac6365536a52ffffffff031f162f0500000000060000655265abffbcd40500000000045151ac001a9c8c05000000000652ac53656a6300000000", + "ac51ab63acac", + 0, + -67986012, + "051c0df7ac688c2c930808dabde1f50300aea115f2bb3334f4753d5169b51e46" + ], + [ + "49ac2af00216c0307a29e83aa5de19770e6b20845de329290bd69cf0e0db7aed61ae41b39002000000035163ac8b2558ef84635bfc59635150e90b61fc753d34acfd10d97531043053e229cd720133cd95000000000463516a51ffffffff02458471040000000008abab636a51ac0065545aa80000000000096a6553516a5263ac6a00000000", + "51526300ab5363", + 1, + 1449668540, + "ddfd902bba312a06197810da96a0ddccb595f96670b28ded7dba88d8cd0469b8" + ], + [ + "fa4d868b024b010bd5dce46576c2fb489aa60bb797dac3c72a4836f49812c5c564c258414f03000000007a9b3a585e05027bdd89edbadf3c85ac61f8c3a04c773fa746517ae600ff1a9d6b6c02fb0200000004515163abffffffff01b17d020500000000046a65520000000000", + "536565ab65635363", + 0, + -1718953372, + "96c2b32f0a00a5925db7ba72d0b5d39922f30ea0f7443b22bc1b734808513c47" + ], + [ + "cac6382d0462375e83b67c7a86c922b569a7473bfced67f17afd96c3cd2d896cf113febf9e0300000003006a53ffffffffaa4913b7eae6821487dd3ca43a514e94dcbbf350f8cc4cafff9c1a88720711b800000000096a6a525300acac6353ffffffff184fc4109c34ea27014cc2c1536ef7ed1821951797a7141ddacdd6e429fae6ff01000000055251655200ffffffff9e7b79b4e6836e290d7b489ead931cba65d1030ccc06f20bd4ca46a40195b33c030000000008f6bc8304a09a2704000000000563655353511dbc73050000000000cf34c500000000000091f76e0000000000085200ab00005100abd07208cb", + "0063656a", + 2, + -1488731031, + "bf078519fa87b79f40abc38f1831731422722c59f88d86775535f209cb41b9b1" + ], + [ + "1711146502c1a0b82eaa7893976fefe0fb758c3f0e560447cef6e1bde11e42de91a125f71c030000000015bd8c04703b4030496c7461482481f290c623be3e76ad23d57a955807c9e851aaaa20270300000000d04abaf20326dcb7030000000001632225350400000000075263ac00520063dddad9020000000000af23d148", + "52520053510063", + 0, + 1852122830, + "e33d5ee08c0f3c130a44d7ce29606450271b676f4a80c52ab9ffab00cecf67f8" + ], + [ + "8d5b124d0231fbfc640c706ddb1d57bb49a18ba8ca0e1101e32c7e6e65a0d4c7971d93ea360100000008acabac0000abac65ffffffff8fe0fd7696597b845c079c3e7b87d4a44110c445a330d70342a5501955e17dd70100000004ab525363ef22e8a90346629f030000000009516a00ac63acac51657bd57b05000000000200acfd4288050000000009acab5352ab00ab636300000000", + "53ac526553ab65", + 0, + 1253152975, + "8b57a7c3170c6c02dd14ae1d392ce3d828197b20e9145c89c1cfd5de050e1562" + ], + [ + "38146dc502c7430e92b6708e9e107b61cd38e5e773d9395e5c8ad8986e7e4c03ee1c1e1e760100000000c8962ce2ac1bb3b1285c0b9ba07f4d2e5ce87c738c42ac0548cd8cec1100e6928cd6b0b6010000000763ab636aab52527cccefbd04e5f6f8020000000006006aabacac65ab2c4a00000000000351635209a6f40100000000026aacce57dc040000000008ab5353ab516a516a00000000", + "ab", + 0, + -1205978252, + "3cb5b030e7da0b60ccce5b4a7f3793e6ca56f03e3799fe2d6c3cc22d6d841dcb" + ], + [ + "22d81c740469695a6a83a9a4824f77ecff8804d020df23713990afce2b72591ed7de98500502000000065352526a6a6affffffff90dc85e118379b1005d7bbc7d2b8b0bab104dad7eaa49ff5bead892f17d8c3ba010000000665656300ab51ffffffff965193879e1d5628b52005d8560a35a2ba57a7f19201a4045b7cbab85133311d0200000003ac005348af21a13f9b4e0ad90ed20bf84e4740c8a9d7129632590349afc03799414b76fd6e826200000000025353ffffffff04a0d40d04000000000060702700000000000652655151516ad31f1502000000000365ac0069a1ac0500000000095100655300ab53525100000000", + "51636a52ac", + 0, + -1644680765, + "add7f5da27262f13da6a1e2cc2feafdc809bd66a67fb8ae2a6f5e6be95373b6f" + ], + [ + "a27dcbc801e3475174a183586082e0914c314bc9d79d1570f29b54591e5e0dff07fbb45a7f0000000004ac53ab51ffffffff027347f5020000000005535351ab63d0e5c9030000000009ac65ab6a63515200ab7cd632ed", + "ac63636553", + 0, + -686435306, + "883a6ea3b2cc53fe8a803c229106366ca14d25ffbab9fef8367340f65b201da6" + ], + [ + "b123ed2204410d4e8aaaa8cdb95234ca86dad9ff77fb4ae0fd4c06ebed36794f0215ede0040100000002ac63ffffffff3b58b81b19b90d8f402701389b238c3a84ff9ba9aeea298bbf15b41a6766d27a01000000056a6553ab00151824d401786153b819831fb15926ff1944ea7b03d884935a8bde01ed069d5fd80220310200000000ffffffffa9c9d246f1eb8b7b382a9032b55567e9a93f86c77f4e32c092aa1738f7f756c30100000002ab65ffffffff011a2b48000000000000ed44d1fb", + "630051ab63", + 2, + -1118263883, + "b5dab912bcabedff5f63f6dd395fc2cf030d83eb4dd28214baba68a45b4bfff0" + ], + [ + "1339051503e196f730955c5a39acd6ed28dec89b4dadc3f7c79b203b344511270e5747fa9900000000045151636affffffff378c6090e08a3895cedf1d25453bbe955a274657172491fd2887ed5c9aceca7b0100000000ffffffffcf7cc3c36ddf9d4749edfa9cefed496d2f86e870deb814bfcd3b5637a5496461030000000451006300ffffffff04dcf3fa010000000008526a63005263acabb41d84040000000004abac5153800eff020000000005656a535365106c5e00000000000000000000", + "abac5300", + 2, + 2013719928, + "7fc74de39ce6ca46ca25d760d3cec7bb21fd14f7efe1c443b5aa294f2cb5f546" + ], + [ + "0728c606014c1fd6005ccf878196ba71a54e86cc8c53d6db500c3cc0ac369a26fac6fcbc210000000005ab53ac5365ba9668290182d7870100000000066a000053655100000000", + "65", + 0, + 1789961588, + "ab6baa6da3b2bc853868d166f8996ad31d63ef981179f9104f49968fd61c8427" + ], + [ + "a1134397034bf4067b6c81c581e2b73fb63835a08819ba24e4e92df73074bf773c94577df7000000000465525251ffffffff8b6608feaa3c1f35f49c6330a769716fa01c5c6f6e0cdc2eb10dfc99bbc21e77010000000952656aac005352655180a0bda4bc72002c2ea8262e26e03391536ec36867258cab968a6fd6ec7523b64fa1d8c001000000056a53ac6353ffffffff04dbeeed05000000000553650052abcd5d0e01000000000463abab51104b2e0500000000066aac53ac5165283ca7010000000004535252ab00000000", + "ab515151516552ab", + 1, + -324598676, + "91178482112f94d1c8e929de443e4b9c893e18682998d393ca9ca77950412586" + ], + [ + "bcdafbae04aa18eb75855aeb1f5124f30044741351b33794254a80070940cb10552fa4fa8e0300000001acd0423fe6e3f3f88ae606f2e8cfab7a5ef87caa2a8f0401765ff9a47d718afcfb40c0099b0000000008ac6565ab53ac6aac645308009d680202d600e492b31ee0ab77c7c5883ebad5065f1ce87e4dfe6453e54023a0010000000151ffffffffb9d818b14245899e1d440152827c95268a676f14c3389fc47f5a11a7b38b1bde03000000026300ffffffff03cda22102000000000751ac535263005100a4d20400000000045200536ac8bef405000000000700ab51ab6563ac00000000", + "6553516a526aab", + 1, + -2111409753, + "5e1849e7368cf4f042718586d9bd831d61479b775bab97aba9f450042bd9876a" + ], + [ + "ed3bb93802ddbd08cb030ef60a2247f715a0226de390c9c1a81d52e83f8674879065b5f87d0300000003ab6552ffffffff04d2c5e60a21fb6da8de20bf206db43b720e2a24ce26779bca25584c3f765d1e0200000008ab656a6aacab00ab6e946ded025a811d04000000000951abac6352ac00ab5143cfa3030000000005635200636a00000000", + "5352ac650065535300", + 1, + -668727133, + "e9995065e1fddef72a796eef5274de62012249660dc9d233a4f24e02a2979c87" + ], + [ + "59f4629d030fa5d115c33e8d55a79ea3cba8c209821f979ed0e285299a9c72a73c5bba00150200000002636affffffffd8aca2176df3f7a96d0dc4ee3d24e6cecde1582323eec2ebef9a11f8162f17ac0000000007ab6565acab6553ffffffffeebc10af4f99c7a21cbc1d1074bd9f0ee032482a71800f44f26ee67491208e0403000000065352ac656351ffffffff0434e955040000000004ab515152caf2b305000000000365ac007b1473030000000003ab530033da970500000000060051536a5253bb08ab51", + "", + 2, + 396340944, + "0e9c47973ef2c292b2252c623f465bbb92046fe0b893eebf4e1c9e02cb01c397" + ], + [ + "286e3eb7043902bae5173ac3b39b44c5950bc363f474386a50b98c7bdab26f98dc83449c4a020000000752ac6a00510051ffffffff4339cd6a07f5a5a2cb5815e5845da70300f5c7833788363bf7fe67595d3225520100000000fffffffff9c2dd8b06ad910365ffdee1a966f124378a2b8021065c8764f6138bb1e951380200000005ab5153ac6affffffff0370202aba7a68df85436ea7c945139513384ef391fa33d16020420b8ad40e9a000000000900ab5165526353abacffffffff020c1907000000000004abac526a1b490b040000000000df1528f7", + "5353ab", + 3, + -1407529517, + "32154c09174a9906183abf26538c39e78468344ca0848bbd0785e24a3565d932" + ], + [ + "2e245cf80179e2e95cd1b34995c2aff49fe4519cd7cee93ad7587f7f7e8105fc2dff206cd30200000009006a63516a6553ab52350435a201d5ed2d02000000000352ab6558552c89", + "00ab53", + 0, + -233917810, + "4605ae5fd3d50f9c45d37db7118a81a9ef6eb475d2333f59df5d3e216f150d49" + ], + [ + "33a98004029d262f951881b20a8d746c8c707ea802cd2c8b02a33b7e907c58699f97e42be80100000007ac53536552abacdee04cc01d205fd8a3687fdf265b064d42ab38046d76c736aad8865ca210824b7c622ecf02000000070065006a536a6affffffff01431c5d010000000000270d48ee", + "", + 1, + 921554116, + "ff9d7394002f3f196ea25472ea6c46f753bd879a7244795157bb7235c9322902" + ], + [ + "aac18f2b02b144ed481557c53f2146ae523f24fcde40f3445ab0193b6b276c315dc2894d2300000000075165650000636a233526947dbffc76aec7db1e1baa6868ad4799c76e14794dcbaaec9e713a83967f6a65170200000005abac6551ab27d518be01b652a30000000000015300000000", + "52ac5353", + 1, + 1559377136, + "59fc2959bb7bb24576cc8a237961ed95bbb900679d94da6567734c4390cb6ef5" + ], + [ + "5ab79881033555b65fe58c928883f70ce7057426fbdd5c67d7260da0fe8b1b9e6a2674cb850300000009ac516aac6aac006a6affffffffa5be9223b43c2b1a4d120b5c5b6ec0484f637952a3252181d0f8e813e76e11580200000000e4b5ceb8118cb77215bbeedc9a076a4d087bb9cd1473ea32368b71daeeeacc451ec209010000000005acac5153aced7dc34e02bc5d11030000000005ac5363006a54185803000000000552ab00636a00000000", + "5100", + 1, + 1927062711, + "e9f53d531c12cce1c50abed4ac521a372b4449b6a12f9327c80020df6bff66c0" + ], + [ + "6c2c8fac0124b0b7d4b610c3c5b91dee32b7c927ac71abdf2d008990ca1ac40de0dfd530660300000006ababac5253656bd7eada01d847ec000000000004ac52006af4232ec8", + "6a6a6a0051", + 0, + -340809707, + "fb51eb9d7e47d32ff2086205214f90c7c139e08c257a64829ae4d2b301071c6a" + ], + [ + "6e3880af031735a0059c0bb5180574a7dcc88e522c8b56746d130f8d45a52184045f96793e0100000008acabac6a526a6553fffffffffe05f14cdef7d12a9169ec0fd37524b5fcd3295f73f48ca35a36e671da4a2f560000000008006a526a6351ab63ffffffffdfbd869ac9e472640a84caf28bdd82e8c6797f42d03b99817a705a24fde2736600000000010090a090a503db956b04000000000952ac53ab6a536a63ab358390010000000009656a5200525153ac65353ee204000000000763530052526aaba6ad83fb", + "535151ab6300", + 2, + 222014018, + "57a34ddeb1bf36d28c7294dda0432e9228a9c9e5cc5c692db98b6ed2e218d825" + ], + [ + "8df1cd19027db4240718dcaf70cdee33b26ea3dece49ae6917331a028c85c5a1fb7ee3e475020000000865ab6a00510063636157988bc84d8d55a8ba93cdea001b9bf9d0fa65b5db42be6084b5b1e1556f3602f65d4d0100000005ac00ab0052206c852902b2fb54030000000008ac5252536aacac5378c4a5050000000007acabac535163532784439e", + "acab6a", + 0, + 1105620132, + "edb7c74223d1f10f9b3b9c1db8064bc487321ff7bb346f287c6bc2fad83682de" + ], + [ + "0e803682024f79337b25c98f276d412bc27e56a300aa422c42994004790cee213008ff1b8303000000080051ac65ac655165f421a331892b19a44c9f88413d057fea03c3c4a6c7de4911fe6fe79cf2e9b3b10184b1910200000005525163630096cb1c670398277204000000000253acf7d5d502000000000963536a6a636a5363ab381092020000000002ac6a911ccf32", + "6565", + 1, + -1492094009, + "f0672638a0e568a919e9d8a9cbd7c0189a3e132940beeb52f111a89dcc2daa2c" + ], + [ + "7d71669d03022f9dd90edac323cde9e56354c6804c6b8e687e9ae699f46805aafb8bcaa636000000000253abffffffff698a5fdd3d7f2b8b000c68333e4dd58fa8045b3e2f689b889beeb3156cecdb490300000009525353abab0051acabc53f0aa821cdd69b473ec6e6cf45cf9b38996e1c8f52c27878a01ec8bb02e8cb31ad24e500000000055353ab0052ffffffff0447a23401000000000565ab53ab5133aaa0030000000006515163656563057d110300000000056a6aacac52cf13b5000000000003526a5100000000", + "6a6a51", + 1, + -1349253507, + "722efdd69a7d51d3d77bed0ac5544502da67e475ea5857cd5af6bdf640a69945" + ], + [ + "9ff618e60136f8e6bb7eabaaac7d6e2535f5fba95854be6d2726f986eaa9537cb283c701ff02000000026a65ffffffff012d1c0905000000000865ab00ac6a516a652f9ad240", + "51515253635351ac", + 0, + 1571304387, + "659cd3203095d4a8672646add7d77831a1926fc5b66128801979939383695a79" + ], + [ + "9fbd43ac025e1462ecd10b1a9182a8e0c542f6d1089322a41822ab94361e214ed7e1dfdd8a020000000263519d0437581538e8e0b6aea765beff5b4f3a4a202fca6e5d19b34c141078c6688f71ba5b8e0100000003ac6552ffffffff02077774050000000009655153655263acab6a0ae4e10100000000035152524c97136b", + "635152ab", + 0, + 1969622955, + "d82d4ccd9b67810f26a378ad9592eb7a30935cbbd27e859b00981aefd0a72e08" + ], + [ + "0117c92004314b84ed228fc11e2999e657f953b6de3b233331b5f0d0cf40d5cc149b93c7b30300000005515263516a083e8af1bd540e54bf5b309d36ba80ed361d77bbf4a1805c7aa73667ad9df4f97e2da410020000000600ab6351ab524d04f2179455e794b2fcb3d214670001c885f0802e4b5e015ed13a917514a7618f5f332203000000086a536aab51000063ecf029e65a4a009a5d67796c9f1eb358b0d4bd2620c8ad7330fb98f5a802ab92d0038b1002000000036a6551a184a88804b04490000000000009ab6a5152535165526a33d1ab020000000001518e92320000000000002913df04000000000952abac6353525353ac8b19bfdf", + "000051ab0000", + 0, + 489433059, + "8eebac87e60da524bbccaf285a44043e2c9232868dda6c6271a53c153e7f3a55" + ], + [ + "e7f5482903f98f0299e0984b361efb2fddcd9979869102281e705d3001a9d283fe9f3f3a1e02000000025365ffffffffcc5c7fe82feebad32a22715fc30bc584efc9cd9cadd57e5bc4b6a265547e676e0000000001ab579d21235bc2281e08bf5e7f8f64d3afb552839b9aa5c77cf762ba2366fffd7ebb74e49400000000055263ab63633df82cf40100982e05000000000453ac535300000000", + "acacab", + 2, + -1362931214, + "046de666545330e50d53083eb78c9336416902f9b96c77cc8d8e543da6dfc7e4" + ], + [ + "09adb2e90175ca0e816326ae2dce7750c1b27941b16f6278023dbc294632ab97977852a09d030000000465ab006affffffff027739cf0100000000075151ab63ac65ab8a5bb601000000000653ac5151520011313cdc", + "ac", + 0, + -76831756, + "478ee06501b4965b40bdba6cbaad9b779b38555a970912bb791b86b7191c54bc" + ], + [ + "f973867602e30f857855cd0364b5bbb894c049f44abbfd661d7ae5dbfeaafca89fac8959c20100000005ab52536a51ffffffffbeceb68a4715f99ba50e131884d8d20f4a179313691150adf0ebf29d05f8770303000000066352ab00ac63ffffffff021fddb90000000000036a656322a177000000000008526500ac5100acac84839083", + "52acab53ac", + 0, + 1407879325, + "db0329439490efc64b7104d6d009b03fbc6fac597cf54fd786fbbb5fd73b92b4" + ], + [ + "fd22ebaa03bd588ad16795bea7d4aa7f7d48df163d75ea3afebe7017ce2f350f6a0c1cb0bb00000000086aabac5153526363ffffffff488e0bb22e26a565d77ba07178d17d8f85702630ee665ec35d152fa05af3bda10200000004515163abffffffffeb21035849e85ad84b2805e1069a91bb36c425dc9c212d9bae50a95b6bfde1200300000001ab5df262fd02b69848040000000008ab6363636a6363ace23bf2010000000007655263635253534348c1da", + "006353526563516a00", + 0, + -1491036196, + "92364ba3c7a85d4e88885b8cb9b520dd81fc29e9d2b750d0790690e9c1246673" + ], + [ + "130b462d01dd49fac019dc4442d0fb54eaa6b1c2d1ad0197590b7df26969a67abd7f3fbb4f0100000008ac65abac53ab6563ffffffff0345f825000000000004ac53acac9d5816020000000002ababeff8e90500000000086aab006552ac6a53a892dc55", + "ab0065ac530052", + 0, + 944483412, + "1f4209fd4ce7f13d175fdd522474ae9b34776fe11a5f17a27d0796c77a2a7a9d" + ], + [ + "f8e50c2604609be2a95f6d0f31553081f4e1a49a0a30777fe51eb1c596c1a9a92c053cf28c0300000009656a51ac5252630052fffffffff792ed0132ae2bd2f11d4a2aab9d0c4fbdf9a66d9ae2dc4108afccdc14d2b1700100000007ab6a6563ac636a7bfb2fa116122b539dd6a2ab089f88f3bc5923e5050c8262c112ff9ce0a3cd51c6e3e84f02000000066551ac5352650d5e687ddf4cc9a497087cabecf74d236aa4fc3081c3f67b6d323cba795e10e7a171b725000000000852635351ab635100ffffffff02df5409020000000008ac6a53acab5151004156990200000000045163655200000000", + "ac53abac65005300", + 0, + -173065000, + "b596f206d7eba22b7e2d1b7a4f4cf69c7c541b6c84dcc943f84e19a99a923310" + ], + [ + "18020dd1017f149eec65b2ec23300d8df0a7dd64fc8558b36907723c03cd1ba672bbb0f51d0300000005ab65ab6a63ffffffff037cd7ae000000000009ab516a65005352ac65f1e4360400000000056353530053f118f0040000000009536363ab006500abac00000000", + "63ab51acab52ac", + 0, + -550412404, + "e19b796c14a0373674968e342f2741d8b51092a5f8409e9bff7dcd52e56fcbcb" + ], + [ + "b04154610363fdade55ceb6942d5e5a723323863b48a0cb04fdcf56210717955763f56b08d0300000009ac526a525151635151ffffffff93a176e76151a9eabdd7af00ef2af72f9e7af5ecb0aa4d45d00618f394cdd03c030000000074d818b332ebe05dc24c44d776cf9d275c61f471cc01efce12fd5a16464157f1842c65cb00000000066a0000ac6352d3c4134f01d8a1c0030000000005520000005200000000", + "5200656a656351", + 2, + -9757957, + "6e3e5ba77f760b6b5b5557b13043f1262418f3dd2ce7f0298b012811fc8ad5bc" + ], + [ + "9794b3ce033df7b1e32db62d2f0906b589eacdacf5743963dc2255b6b9a6cba211fadd0d41020000000600ab00650065ffffffffaae00687a6a4131152bbcaafedfaed461c86754b0bde39e2bef720e6d1860a0302000000070065516aac6552ffffffff50e4ef784d6230df7486e972e8918d919f005025bc2d9aacba130f58bed7056703000000075265ab52656a52ffffffff02c6f1a9000000000006005251006363cf450c040000000008abab63510053abac00000000", + "ac0063ababab515353", + 1, + 2063905082, + "fad092fc98f17c2c20e10ba9a8eb44cc2bcc964b006f4da45cb9ceb249c69698" + ], + [ + "94533db7015e70e8df715066efa69dbb9c3a42ff733367c18c22ff070392f988f3b93920820000000006535363636300ce4dac3e03169af80300000000080065ac6a53ac65ac39c050020000000006abacab6aacac708a02050000000005ac5251520000000000", + "6553", + 0, + -360458507, + "5418cf059b5f15774836edd93571e0eed3855ba67b2b08c99dccab69dc87d3e9" + ], + [ + "c8597ada04f59836f06c224a2640b79f3a8a7b41ef3efa2602592ddda38e7597da6c639fee0300000009005251635351acabacffffffff4c518f347ee694884b9d4072c9e916b1a1f0a7fc74a1c90c63fdf8e5a185b6ae02000000007113af55afb41af7518ea6146786c7c726641c68c8829a52925e8d4afd07d8945f68e7230300000008ab00ab65ab650063ffffffffc28e46d7598312c420e11dfaae12add68b4d85adb182ae5b28f8340185394b63000000000165ffffffff04dbabb7010000000000ee2f6000000000000852ab6500ab6a51acb62a27000000000009ac53515300ac006a6345fb7505000000000752516a0051636a00000000", + "", + 3, + 15199787, + "0d66003aff5bf78cf492ecbc8fd40c92891acd58d0a271be9062e035897f317e" + ], + [ + "1a28c4f702c8efaad96d879b38ec65c5283b5c084b819ad7db1c086e85e32446c7818dc7a90300000008656351536a525165fa78cef86c982f1aac9c5eb8b707aee8366f74574c8f42ef240599c955ef4401cf578be30200000002ab518893292204c430eb0100000000016503138a0300000000040053abac60e0eb010000000005525200ab63567c2d030000000004abab52006cf81e85", + "ab51525152", + 1, + 2118315905, + "4e4c9a781f626b59b1d3ad8f2c488eb6dee8bb19b9bc138bf0dc33e7799210d4" + ], + [ + "c6c7a87003f772bcae9f3a0ac5e499000b68703e1804b9ddc3e73099663564d53ddc4e1c6e01000000076a536a6aac63636e3102122f4c30056ef8711a6bf11f641ddfa6984c25ac38c3b3e286e74e839198a80a34010000000165867195cd425821dfa2f279cb1390029834c06f018b1e6af73823c867bf3a0524d1d6923b0300000005acab53ab65ffffffff02fa4c49010000000008ab656a0052650053e001100400000000008836d972", + "ac526351acab", + 1, + 978122815, + "a869c18a0edf563d6e5eddd5d5ae8686f41d07f394f95c9feb8b7e52761531ca" + ], + [ + "0ea580ac04c9495ab6af3b8d59108bb4194fcb9af90b3511c83f7bb046d87aedbf8423218e02000000085152acac006363ab9063d7dc25704e0caa5edde1c6f2dd137ded379ff597e055b2977b9c559b07a7134fcef2000000000200aca89e50181f86e9854ae3b453f239e2847cf67300fff802707c8e3867ae421df69274449402000000056365abababffffffff47a4760c881a4d7e51c69b69977707bd2fb3bcdc300f0efc61f5840e1ac72cee0000000000ffffffff0460179a020000000004ab53ab52a5250c0500000000096565acac6365ab52ab6c281e02000000000952635100ac006563654e55070400000000046552526500000000", + "ab526563acac53ab", + 2, + 1426964167, + "b1c50d58b753e8f6c7513752158e9802cf0a729ebe432b99acc0fe5d9b4e9980" + ], + [ + "c33028b301d5093e1e8397270d75a0b009b2a6509a01861061ab022ca122a6ba935b8513320200000000ffffffff013bcf5a0500000000015200000000", + "", + 0, + -513413204, + "6b1459536f51482f5dbf42d7e561896557461e1e3b6bf67871e2b51faae2832c" + ], + [ + "43b2727901a7dd06dd2abf690a1ccedc0b0739cb551200796669d9a25f24f71d8d101379f50300000000ffffffff0418e031040000000000863d770000000000085352ac526563ac5174929e040000000004ac65ac00ec31ac0100000000066a51ababab5300000000", + "65", + 0, + -492874289, + "154ff7a9f0875edcfb9f8657a0b98dd9600fabee3c43eb88af37cf99286d516c" + ], + [ + "4763ed4401c3e6ab204bed280528e84d5288f9cac5fb8a2e7bd699c7b98d4df4ac0c40e55303000000066a6aacab5165ffffffff015b57f80400000000046a63535100000000", + "ac51abab53", + 0, + -592611747, + "849033a2321b5755e56ef4527ae6f51e30e3bca50149d5707368479723d744f8" + ], + [ + "d24f647b02f71708a880e6819a1dc929c1a50b16447e158f8ff62f9ccd644e0ca3c592593702000000050053536a00ffffffff67868cd5414b6ca792030b18d649de5450a456407242b296d936bcf3db79e07b02000000005af6319c016022f50100000000036a516300000000", + "6aab526353516a6a", + 0, + 1350782301, + "8556fe52d1d0782361dc28baaf8774b13f3ce5ed486ae0f124b665111e08e3e3" + ], + [ + "fe6ddf3a02657e42a7496ef170b4a8caf245b925b91c7840fd28e4a22c03cb459cb498b8d603000000065263656a650071ce6bf8d905106f9f1faf6488164f3decac65bf3c5afe1dcee20e6bc3cb6d052561985a030000000163295b117601343dbb0000000000026563dba521df", + "", + 1, + -1696179931, + "d9684685c99ce48f398fb467a91a1a59629a850c429046fb3071f1fa9a5fe816" + ], + [ + "c61523ef0129bb3952533cbf22ed797fa2088f307837dd0be1849f20decf709cf98c6f032f03000000026563c0f1d378044338310400000000066363516a5165a14fcb0400000000095163536a6a00ab53657271d60200000000001d953f0500000000010000000000", + "53516353005153", + 0, + 1141615707, + "7e975a72db5adaa3c48d525d9c28ac11cf116d0f8b16ce08f735ad75a80aec66" + ], + [ + "ba3dac6c0182562b0a26d475fe1e36315f0913b6869bdad0ecf21f1339a5fcbccd32056c840200000000ffffffff04300351050000000000220ed405000000000851abac636565ac53dbbd19020000000007636363ac6a52acbb005a0500000000016abd0c78a8", + "63006a635151005352", + 0, + 1359658828, + "47bc8ab070273e1f4a0789c37b45569a6e16f3f3092d1ce94dddc3c34a28f9f4" + ], + [ + "ac27e7f5025fc877d1d99f7fc18dd4cadbafa50e34e1676748cc89c202f93abf36ed46362101000000036300abffffffff958cd5381962b765e14d87fc9524d751e4752dd66471f973ed38b9d562e525620100000003006500ffffffff02b67120050000000004ac51516adc330c0300000000015200000000", + "656352", + 1, + 15049991, + "f3374253d64ac264055bdbcc32e27426416bd595b7c7915936c70f839e504010" + ], + [ + "edb30140029182b80c8c3255b888f7c7f061c4174d1db45879dca98c9aab8c8fed647a6ffc03000000086a53510052ab6300ffffffff82f65f261db62d517362c886c429c8fbbea250bcaad93356be6f86ba573e9d930100000000ffffffff04daaf150400000000016a86d1300100000000096a6353535252ac5165d4ddaf000000000002abab5f1c6201000000000000000000", + "ab6a6a00ac", + 0, + -2058017816, + "8d7794703dad18e2e40d83f3e65269834bb293e2d2b8525932d6921884b8f368" + ], + [ + "7e50207303146d1f7ad62843ae8017737a698498d4b9118c7a89bb02e8370307fa4fada41d000000000753006300005152b7afefc85674b1104ba33ef2bf37c6ed26316badbc0b4aa6cb8b00722da4f82ff3555a6c020000000900ac656363ac51ac52ffffffff93fab89973bd322c5d7ad7e2b929315453e5f7ada3072a36d8e33ca8bebee6e0020000000300acab930da52b04384b04000000000004650052ac435e380200000000076a6a515263ab6aa9494705000000000600ab6a525252af8ba90100000000096565acab526353536a279b17ad", + "acac005263536aac63", + 1, + -34754133, + "4e6357da0057fb7ff79da2cc0f20c5df27ff8b2f8af4c1709e6530459f7972b0" + ], + [ + "c05764f40244fb4ebe4c54f2c5298c7c798aa90e62c29709acca0b4c2c6ec08430b26167440100000008acab6a6565005253ffffffffc02c2418f398318e7f34a3cf669d034eef2111ea95b9f0978b01493293293a870100000000e563e2e00238ee8d040000000002acab03fb060200000000076500ac656a516aa37f5534", + "52ab6a0065", + 1, + -2033176648, + "83deef4a698b62a79d4877dd9afebc3011a5275dbe06e89567e9ef84e8a4ee19" + ], + [ + "5a59e0b9040654a3596d6dab8146462363cd6549898c26e2476b1f6ae42915f73fd9aedfda00000000036363abffffffff9ac9e9ca90be0187be2214251ff08ba118e6bf5e2fd1ba55229d24e50a510d53010000000165ffffffff41d42d799ac4104644969937522873c0834cc2fcdab7cdbecd84d213c0e96fd60000000000ffffffffd838db2c1a4f30e2eaa7876ef778470f8729fcf258ad228b388df2488709f8410300000000fdf2ace002ceb6d903000000000265654c1310040000000003ac00657e91c0ec", + "536a63ac", + 0, + 82144555, + "98ccde2dc14d14f5d8b1eeea5364bd18fc84560fec2fcea8de4d88b49c00695e" + ], + [ + "156ebc8202065d0b114984ee98c097600c75c859bfee13af75dc93f57c313a877efb09f230010000000463536a51ffffffff81114e8a697be3ead948b43b5005770dd87ffb1d5ccd4089fa6c8b33d3029e9c03000000066a5251656351ffffffff01a87f140000000000050000ac51ac00000000", + "00", + 0, + -362221092, + "a903c84d8c5e71134d1ab6dc1e21ac307c4c1a32c90c90f556f257b8a0ec1bf5" + ], + [ + "15e37793023c7cbf46e073428908fce0331e49550f2a42b92468827852693f0532a01c29f70200000007005353636351acffffffff38426d9cec036f00eb56ec1dcd193647e56a7577278417b8a86a78ac53199bc403000000056353006a53ffffffff04a25ce103000000000900ab5365656a526a63c8eff7030000000004526353537ab6db0200000000016a11a3fa02000000000651acacab526500000000", + "53ac6aab6a6551", + 0, + 1117532791, + "83c68b3c5a89260ce16ce8b4dbf02e1f573c532d9a72f5ea57ab419fa2630214" + ], + [ + "f7a09f10027250fc1b70398fb5c6bffd2be9718d3da727e841a73596fdd63810c9e4520a6a010000000963ac516a636a65acac1d2e2c57ab28d311edc4f858c1663972eebc3bbc93ed774801227fda65020a7ec1965f780200000005ac5252516a8299fddc01dcbf7200000000000463ac6551960fda03", + "65acab51", + 1, + 2017321737, + "9c5fa02abfd34d0f9dec32bf3edb1089fca70016debdb41f4f54affcb13a2a2a" + ], + [ + "6d97a9a5029220e04f4ccc342d8394c751282c328bf1c132167fc05551d4ca4da4795f6d4e02000000076a0052ab525165ffffffff9516a205e555fa2a16b73e6db6c223a9e759a7e09c9a149a8f376c0a7233fa1b0100000007acab51ab63ac6affffffff04868aed04000000000652ac65ac536a396edf01000000000044386c0000000000076aab5363655200894d48010000000001ab8ebefc23", + "6351526aac51", + 1, + 1943666485, + "f0bd4ca8e97203b9b4e86bc24bdc8a1a726db5e99b91000a14519dc83fc55c29" + ], + [ + "8e3fddfb028d9e566dfdda251cd874cd3ce72e9dde837f95343e90bd2a93fe21c5daeb5eed01000000045151525140517dc818181f1e7564b8b1013fd68a2f9a56bd89469686367a0e72c06be435cf99db750000000003635251ffffffff01c051780300000000096552ababac6a65acab099766eb", + "5163ab6a52ababab51", + 1, + 1296295812, + "5509eba029cc11d7dd2808b8c9eb47a19022b8d8b7778893459bbc19ab7ea820" + ], + [ + "a603f37b02a35e5f25aae73d0adc0b4b479e68a734cf722723fd4e0267a26644c36faefdab0200000000ffffffff43374ad26838bf733f8302585b0f9c22e5b8179888030de9bdda180160d770650200000001004c7309ce01379099040000000005526552536500000000", + "abababab005153", + 0, + 1409936559, + "4ca73da4fcd5f1b10da07998706ffe16408aa5dff7cec40b52081a6514e3827e" + ], + [ + "9eeedaa8034471a3a0e3165620d1743237986f060c4434f095c226114dcb4b4ec78274729f03000000086a5365510052ac6afb505af3736e347e3f299a58b1b968fce0d78f7457f4eab69240cbc40872fd61b5bf8b120200000002ac52df8247cf979b95a4c97ecb8edf26b3833f967020cd2fb25146a70e60f82c9ee4b14e88b103000000008459e2fa0125cbcd05000000000000000000", + "52ab5352006353516a", + 0, + -1832576682, + "fb018ae54206fdd20c83ae5873ec82b8e320a27ed0d0662db09cda8a071f9852" + ], + [ + "05921d7c048cf26f76c1219d0237c226454c2a713c18bf152acc83c8b0647a94b13477c07f0300000003ac526afffffffff2f494453afa0cabffd1ba0a626c56f90681087a5c1bd81d6adeb89184b27b7402000000036a6352ffffffff0ad10e2d3ce355481d1b215030820da411d3f571c3f15e8daf22fe15342fed04000000000095f29f7b93ff814a9836f54dc6852ec414e9c4e16a506636715f569151559100ccfec1d100000000055263656a53ffffffff04f4ffef010000000008ac6a6aabacabab6a0e6689040000000006ab536a5352abe364d005000000000965536363655251ab53807e00010000000004526aab63f18003e3", + "6363ac51", + 3, + -375891099, + "001b0b176f0451dfe2d9787b42097ceb62c70d324e925ead4c58b09eebdf7f67" + ], + [ + "b9b44d9f04b9f15e787d7704e6797d51bc46382190c36d8845ec68dfd63ee64cf7a467b21e00000000096aac00530052ab636aba1bcb110a80c5cbe073f12c739e3b20836aa217a4507648d133a8eedd3f02cb55c132b203000000076a000063526352b1c288e3a9ff1f2da603f230b32ef7c0d402bdcf652545e2322ac01d725d75f5024048ad0100000000ffffffffffd882d963be559569c94febc0ef241801d09dc69527c9490210f098ed8203c700000000056a006300ab9109298d01719d9a0300000000066a52ab006365d7894c5b", + "ac6351650063636a", + 3, + -622355349, + "ac87b1b93a6baab6b2c6624f10e8ebf6849b0378ef9660a3329073e8f5553c8d" + ], + [ + "ff60473b02574f46d3e49814c484081d1adb9b15367ba8487291fc6714fd6e3383d5b335f001000000026a6ae0b82da3dc77e5030db23d77b58c3c20fa0b70aa7d341a0f95f3f72912165d751afd57230300000008ac536563516a6363ffffffff04f86c0200000000000553acab636ab13111000000000003510065f0d3f305000000000951ab516a65516aabab730a3a010000000002515200000000", + "ac6a", + 1, + 1895032314, + "0767e09bba8cd66d55915677a1c781acd5054f530d5cf6de2d34320d6c467d80" + ], + [ + "f218026204f4f4fc3d3bd0eada07c57b88570d544a0436ae9f8b753792c0c239810bb30fbc0200000002536affffffff8a468928d6ec4cc10aa0f73047697970e99fa64ae8a3b4dca7551deb0b639149010000000851ab520052650051ffffffffa98dc5df357289c9f6873d0f5afcb5b030d629e8f23aa082cf06ec9a95f3b0cf0000000000ffffffffea2c2850c5107705fd380d6f29b03f533482fd036db88739122aac9eff04e0aa010000000365536a03bd37db034ac4c4020000000007515152655200ac33b27705000000000151efb71e0000000000007b65425b", + "515151", + 3, + -1772252043, + "de35c84a58f2458c33f564b9e58bc57c3e028d629f961ad1b3c10ee020166e5a" + ], + [ + "48e7d42103b260b27577b70530d1ac2fed2551e9dd607cbcf66dca34bb8c03862cf8f5fd5401000000075151526aacab00ffffffff1e3d3b841552f7c6a83ee379d9d66636836673ce0b0eda95af8f2d2523c91813030000000665acac006365ffffffff388b3c386cd8c9ef67c83f3eaddc79f1ff910342602c9152ffe8003bce51b28b0100000008636363006a636a52ffffffff04b8f67703000000000852005353ac6552520cef720200000000085151ab6352ab00ab5096d6030000000005516a005100662582020000000001ac6c137280", + "6a65", + 1, + 1513618429, + "e2fa3e1976aed82c0987ab30d4542da2cb1cffc2f73be13480132da8c8558d5c" + ], + [ + "91ebc4cf01bc1e068d958d72ee6e954b196f1d85b3faf75a521b88a78021c543a06e056279000000000265ab7c12df0503832121030000000000cc41a6010000000005ab5263516540a951050000000006ab63ab65acac00000000", + "526a0065636a6a6aac", + 0, + -614046478, + "7de4ba875b2e584a7b658818c112e51ee5e86226f5a80e5f6b15528c86400573" + ], + [ + "3cd4474201be7a6c25403bf00ca62e2aa8f8f4f700154e1bb4d18c66f7bb7f9b975649f0dc0100000006535151535153ffffffff01febbeb000000000006005151006aac00000000", + "", + 0, + -1674687131, + "6b77ca70cc452cc89acb83b69857cda98efbfc221688fe816ef4cb4faf152f86" + ], + [ + "92fc95f00307a6b3e2572e228011b9c9ed41e58ddbaefe3b139343dbfb3b34182e9fcdc3f50200000002acab847bf1935fde8bcfe41c7dd99683289292770e7f163ad09deff0e0665ed473cd2b56b0f40300000006516551ab6351294dab312dd87b9327ce2e95eb44b712cfae0e50fda15b07816c8282e8365b643390eaab01000000026aacffffffff016e0b6b040000000001ac00000000", + "650065acac005300", + 2, + -1885164012, + "bd7d26bb3a98fc8c90c972500618bf894cb1b4fe37bf5481ff60eef439d3b970" + ], + [ + "4db591ab018adcef5f4f3f2060e41f7829ce3a07ea41d681e8cb70a0e37685561e4767ac3b0000000005000052acabd280e63601ae6ef20000000000036a636326c908f7", + "ac6a51526300630052", + 0, + 862877446, + "355ccaf30697c9c5b966e619a554d3323d7494c3ea280a9b0dfb73f953f5c1cb" + ], + [ + "503fd5ef029e1beb7b242d10032ac2768f9a1aca0b0faffe51cec24770664ec707ef7ede4f01000000045253ac53375e350cc77741b8e96eb1ce2d3ca91858c052e5f5830a0193200ae2a45b413dda31541f0000000003516553ffffffff0175a5ba0500000000015200000000", + "6aab65510053ab65", + 1, + 1603081205, + "353ca9619ccb0210ae18b24d0e57efa7abf8e58fa6f7102738e51e8e72c9f0c4" + ], + [ + "c80abebd042cfec3f5c1958ee6970d2b4586e0abec8305e1d99eb9ee69ecc6c2cbd76374380000000007ac53006300ac510acee933b44817db79320df8094af039fd82111c7726da3b33269d3820123694d849ee5001000000056a65ab526562699bea8530dc916f5d61f0babea709dac578774e8a4dcd9c640ec3aceb6cb2443f24f302000000020063ea780e9e57d1e4245c1e5df19b4582f1bf704049c5654f426d783069bcc039f2d8fa659f030000000851ab53635200006a8d00de0b03654e8500000000000463ab635178ebbb0400000000055100636aab239f1d030000000006ab006300536500000000", + "6565ac515100", + 3, + 1460851377, + "b35bb1b72d02fab866ed6bbbea9726ab32d968d33a776686df3ac16aa445871e" + ], + [ + "0337b2d5043eb6949a76d6632b8bb393efc7fe26130d7409ef248576708e2d7f9d0ced9d3102000000075352636a5163007034384dfa200f52160690fea6ce6c82a475c0ef1caf5c9e5a39f8f9ddc1c8297a5aa0eb02000000026a51ffffffff38e536298799631550f793357795d432fb2d4231f4effa183c4e2f61a816bcf0030000000463ac5300706f1cd3454344e521fde05b59b96e875c8295294da5d81d6cc7efcfe8128f150aa54d6503000000008f4a98c704c1561600000000000072cfa6000000000000e43def01000000000100cf31cc0500000000066365526a6500cbaa8e2e", + "", + 3, + 2029506437, + "7615b4a7b3be865633a31e346bc3db0bcc410502c8358a65b8127089d81b01f8" + ], + [ + "59f6cffd034733f4616a20fe19ea6aaf6abddb30b408a3a6bd86cd343ab6fe90dc58300cc90200000000ffffffffc835430a04c3882066abe7deeb0fa1fdaef035d3233460c67d9eabdb05e95e5a02000000080065ac535353ab00ffffffff4b9a043e89ad1b4a129c8777b0e8d87a014a0ab6a3d03e131c27337bbdcb43b402000000066a5100abac6ad9e9bf62014bb118010000000001526cbe484f", + "ab526352ab65", + 0, + 2103515652, + "4f2ccf981598639bec57f885b4c3d8ea8db445ea6e61cfd45789c69374862e5e" + ], + [ + "cbc79b10020b15d605680a24ee11d8098ad94ae5203cb6b0589e432832e20c27b72a926af20300000006ab65516a53acbb854f3146e55c508ece25fa3d99dbfde641a58ed88c051a8a51f3dacdffb1afb827814b02000000026352c43e6ef30302410a020000000000ff4bd90100000000065100ab63000008aa8e0400000000095265526565ac5365abc52c8a77", + "53526aac0051", + 0, + 202662340, + "984efe0d8d12e43827b9e4b27e97b3777ece930fd1f589d616c6f9b71dab710e" + ], + [ + "7c07419202fa756d29288c57b5c2b83f3c847a807f4a9a651a3f6cd6c46034ae0aa3a7446b0200000004ab6a6365ffffffff9da83cf4219bb96c76f2d77d5df31c1411a421171d9b59ec02e5c1218f29935403000000008c13879002f8b1ac0400000000086a63536a636553653c584f02000000000000000000", + "abac53ab656363", + 1, + -1038419525, + "4a74f365a161bc6c9bddd249cbd70f5dadbe3de70ef4bd745dcb6ee1cd299fbd" + ], + [ + "351cbb57021346e076d2a2889d491e9bfa28c54388c91b46ee8695874ad9aa576f1241874d0200000008ab6563525300516affffffffe13e61b8880b8cd52be4a59e00f9723a4722ea58013ec579f5b3693b9e115b1100000000096363abac5252635351ffffffff027fee02040000000008ab6a5200ab006a65b85f130200000000086a52630053ab52ab00000000", + "ab6aab65", + 1, + 586415826, + "08bbb746a596991ab7f53a76e19acad087f19cf3e1db54054aab403c43682d09" + ], + [ + "a8252ea903f1e8ff953adb16c1d1455a5036222c6ea98207fc21818f0ece2e1fac310f9a0100000000095163ac635363ac0000be6619e9fffcde50a0413078821283ce3340b3993ad00b59950bae7a9f931a9b0a3a035f010000000463005300b8b0583fbd6049a1715e7adacf770162811989f2be20af33f5f60f26eba653dc26b024a00000000006525351636552ffffffff046d2acc030000000002636a9a2d430500000000080065005165ab53abecf63204000000000052b9ed050000000008acacac53ab65656500000000", + "65ab53635253636a51", + 2, + 1442639059, + "8ca11838775822f9a5beee57bdb352f4ee548f122de4a5ca61c21b01a1d50325" + ], + [ + "2f1a425c0471a5239068c4f38f9df135b1d24bf52d730d4461144b97ea637504495aec360801000000055300515365c71801dd1f49f376dd134a9f523e0b4ae611a4bb122d8b26de66d95203f181d09037974300000000025152ffffffff9bdcea7bc72b6e5262e242c94851e3a5bf8f314b3e5de0e389fc9e5b3eadac030000000009525265655151005153ffffffffdbb53ce99b5a2320a4e6e2d13b01e88ed885a0957d222e508e9ec8e4f83496cb0200000007635200abac63ac04c96237020cc5490100000000080000516a51ac6553074a360200000000025152225520ca", + "6551ab65ac65516a", + 1, + -489869549, + "9bc5bb772c553831fb40abe466074e59a469154679c7dee042b8ea3001c20393" + ], + [ + "ef3acfd4024defb48def411b8f8ba2dc408dc9ee97a4e8bde4d6cb8e10280f29c98a6e8e9103000000035100513d5389e3d67e075469dfd9f204a7d16175653a149bd7851619610d7ca6eece85a516b2df0300000005516aac6552ca678bdf02f477f003000000000057e45b0300000000055252525252af35c20a", + "5165ac53ab", + 1, + -1900839569, + "78eb6b24365ac1edc386aa4ffd15772f601059581c8776c34f92f8a7763c9ccf" + ], + [ + "ff4468dc0108475fc8d4959a9562879ce4ab4867a419664bf6e065f17ae25043e6016c70480100000000ffffffff02133c6f0400000000000bd0a8020000000004006a520035afa4f6", + "51ac65ab", + 0, + -537664660, + "f6da59b9deac63e83728850ac791de61f5dfcaeed384ebcbb20e44afcd8c8910" + ], + [ + "4e8594d803b1d0a26911a2bcdd46d7cbc987b7095a763885b1a97ca9cbb747d32c5ab9aa91030000000353ac53a0cc4b215e07f1d648b6eeb5cdbe9fa32b07400aa773b9696f582cebfd9930ade067b2b200000000060065abab6500fc99833216b8e27a02defd9be47fafae4e4a97f52a9d2a210d08148d2a4e5d02730bcd460100000004516351ac37ce3ae1033baa55040000000006006a636a63acc63c990400000000025265eb1919030000000005656a6a516a00000000", + "", + 1, + -75217178, + "04c5ee48514cd033b82a28e336c4d051074f477ef2675ce0ce4bafe565ee9049" + ], + [ + "a88830a7023f13ed19ab14fd757358eb6af10d6520f9a54923a6d613ac4f2c11e249cda8aa030000000851630065abababacffffffff8f5fe0bc04a33504c4b47e3991d25118947a0261a9fa520356731eeabd561dd3020000000363ababffffffff038404bd010000000008ab5153516aab6a63d33a5601000000000263004642dc020000000009655152acac636352004be6f3af", + "5253536565006aab6a", + 0, + 1174417836, + "2e42ead953c9f4f81b72c27557e6dc7d48c37ff2f5c46c1dbe9778fb0d79f5b2" + ], + [ + "44e1a2b4010762af23d2027864c784e34ef322b6e24c70308a28c8f2157d90d17b99cd94a401000000085163656565006300ffffffff0198233d020000000002000000000000", + "52525153656365", + 0, + 1119696980, + "d9096de94d70c6337da6202e6e588166f31bff5d51bb5adc9468594559d65695" + ], + [ + "44ca65b901259245abd50a745037b17eb51d9ce1f41aa7056b4888285f48c6f26cb97b7a25020000000552636363abffffffff047820350400000000040053acab14f3e603000000000652635100ab630ce66c03000000000001bdc704000000000765650065ac51ac3e886381", + "51", + 0, + -263340864, + "ed5622ac642d11f90e68c0feea6a2fe36d880ecae6b8c0d89c4ea4b3d162bd90" + ], + [ + "cfa147d2017fe84122122b4dda2f0d6318e59e60a7207a2d00737b5d89694d480a2c26324b0000000006006351526552ffffffff0456b5b804000000000800516aab525363ab166633000000000004655363ab254c0e02000000000952ab6a6a00ab525151097c1b020000000009656a52ac6300530065ad0d6e50", + "6a535165ac6a536500", + 0, + -574683184, + "f926d4036eac7f019a2b0b65356c4ee2fe50e089dd7a70f1843a9f7bc6997b35" + ], + [ + "91c5d5f6022fea6f230cc4ae446ce040d8313071c5ac1749c82982cc1988c94cb1738aa48503000000016a19e204f30cb45dd29e68ff4ae160da037e5fc93538e21a11b92d9dd51cf0b5efacba4dd70000000005656a6aac51ffffffff03db126905000000000953006a53ab6563636a36a273030000000006656a52656552b03ede00000000000352516500000000", + "530052526a00", + 1, + 1437328441, + "255c125b60ee85f4718b2972174c83588ee214958c3627f51f13b5fb56c8c317" + ], + [ + "03f20dc202c886907b607e278731ebc5d7373c348c8c66cac167560f19b341b782dfb634cb03000000076a51ac6aab63abea3e8de7adb9f599c9caba95aa3fa852e947fc88ed97ee50e0a0ec0d14d164f44c0115c10100000004ab5153516fdd679e0414edbd000000000005ac636a53512021f2040000000007006a0051536a52c73db2050000000005525265ac5369046e000000000003ab006a1ef7bd1e", + "52656a", + 0, + 1360223035, + "5a0a05e32ce4cd0558aabd5d79cd5fcbffa95c07137506e875a9afcba4bef5a2" + ], + [ + "d9611140036881b61e01627078512bc3378386e1d4761f959d480fdb9d9710bebddba2079d020000000763536aab5153ab819271b41e228f5b04daa1d4e72c8e1955230accd790640b81783cfc165116a9f535a74c000000000163ffffffffa2e7bb9a28e810624c251ff5ba6b0f07a356ac082048cf9f39ec036bba3d431a02000000076a000000ac65acffffffff01678a820000000000085363515153ac635100000000", + "535353", + 2, + -82213851, + "52b9e0778206af68998cbc4ebdaad5a9469e04d0a0a6cef251abfdbb74e2f031" + ], + [ + "98b3a0bf034233afdcf0df9d46ac65be84ef839e58ee9fa59f32daaa7d684b6bdac30081c60200000007636351acabababffffffffc71cf82ded4d1593e5825618dc1d5752ae30560ecfaa07f192731d68ea768d0f0100000006650052636563f3a2888deb5ddd161430177ce298242c1a86844619bc60ca2590d98243b5385bc52a5b8f00000000095365acacab520052ac50d4722801c3b8a60300000000035165517e563b65", + "51", + 1, + -168940690, + "b6b684e2d2ecec8a8dce4ed3fc1147f8b2e45732444222aa8f52d860c2a27a9d" + ], + [ + "97be4f7702dc20b087a1fdd533c7de762a3f2867a8f439bddf0dcec9a374dfd0276f9c55cc0300000000cdfb1dbe6582499569127bda6ca4aaff02c132dc73e15dcd91d73da77e92a32a13d1a0ba0200000002ab51ffffffff048cfbe202000000000900516351515363ac535128ce0100000000076aac5365ab6aabc84e8302000000000863536a53ab6a6552f051230500000000066aac535153510848d813", + "ac51", + 0, + 229541474, + "e5da9a416ea883be1f8b8b2d178463633f19de3fa82ae25d44ffb531e35bdbc8" + ], + [ + "085b6e04040b5bff81e29b646f0ed4a45e05890a8d32780c49d09643e69cdccb5bd81357670100000001abffffffffa5c981fe758307648e783217e3b4349e31a557602225e237f62b636ec26df1a80300000004650052ab4792e1da2930cc90822a8d2a0a91ea343317bce5356b6aa8aae6c3956076aa33a5351a9c0300000004abac5265e27ddbcd472a2f13325cc6be40049d53f3e266ac082172f17f6df817db1936d9ff48c02b000000000152ffffffff021aa7670500000000085353635163ab51ac14d584000000000001aca4d136cc", + "6a525300536352536a", + 0, + -1398925877, + "41ecca1e8152ec55074f4c39f8f2a7204dda48e9ec1e7f99d5e7e4044d159d43" + ], + [ + "eec32fff03c6a18b12cd7b60b7bdc2dd74a08977e53fdd756000af221228fe736bd9c42d870100000007005353ac515265ffffffff037929791a188e9980e8b9cc154ad1b0d05fb322932501698195ab5b219488fc02000000070063510065ab6a0bfc176aa7e84f771ea3d45a6b9c24887ceea715a0ff10ede63db8f089e97d927075b4f1000000000551abab63abffffffff02eb933c000000000000262c420000000000036563632549c2b6", + "6352", + 2, + 1480445874, + "ff8a4016dfdd918f53a45d3a1f62b12c407cd147d68ca5c92b7520e12c353ff5" + ], + [ + "98ea7eac0313d9fb03573fb2b8e718180c70ce647bebcf49b97a8403837a2556cb8c9377f30000000004ac53ac65ffffffff8caac77a5e52f0d8213ef6ce998bedbb50cfdf108954771031c0e0cd2a78423900000000010066e99a44937ebb37015be3693761078ad5c73aa73ec623ac7300b45375cc8eef36087eb80000000007515352acac5100ffffffff0114a51b02000000000000000000", + "6aacab", + 0, + 243527074, + "bad77967f98941af4dd52a8517d5ad1e32307c0d511e15461e86465e1b8b5273" + ], + [ + "3ab70f4604e8fc7f9de395ec3e4c3de0d560212e84a63f8d75333b604237aa52a10da17196000000000763526a6553ac63a25de6fd66563d71471716fe59087be0dde98e969e2b359282cf11f82f14b00f1c0ac70f02000000050052516aacdffed6bb6889a13e46956f4b8af20752f10185838fd4654e3191bf49579c961f5597c36c0100000005ac636363abc3a1785bae5b8a1b4be5d0cbfadc240b4f7acaa7dfed6a66e852835df5eb9ac3c553766801000000036a65630733b7530218569602000000000952006a6a6a51acab52777f06030000000007ac0063530052abc08267c9", + "000000536aac0000", + 1, + 1919096509, + "df1c87cf3ba70e754d19618a39fdbd2970def0c1bfc4576260cba5f025b87532" + ], + [ + "bdb6b4d704af0b7234ced671c04ba57421aba7ead0a117d925d7ebd6ca078ec6e7b93eea6600000000026565ffffffff3270f5ad8f46495d69b9d71d4ab0238cbf86cc4908927fbb70a71fa3043108e6010000000700516a65655152ffffffff6085a0fdc03ae8567d0562c584e8bfe13a1bd1094c518690ebcb2b7c6ce5f04502000000095251530052536a53aba576a37f2c516aad9911f687fe83d0ae7983686b6269b4dd54701cb5ce9ec91f0e6828390300000000ffffffff04cc76cc020000000002656a01ffb702000000000253ab534610040000000009acab006565516a00521f55f5040000000000389dfee9", + "6a525165", + 0, + 1336204763, + "71c294523c48fd7747eebefbf3ca06e25db7b36bff6d95b41c522fecb264a919" + ], + [ + "54258edd017d22b274fbf0317555aaf11318affef5a5f0ae45a43d9ca4aa652c6e85f8a040010000000953ac65ab5251656500ffffffff03321d450000000000085265526a51526a529ede8b030000000003635151ce6065020000000001534c56ec1b", + "acac", + 0, + 2094130012, + "110d90fea9470dfe6c5048f45c3af5e8cc0cb77dd58fd13d338268e1c24b1ccc" + ], + [ + "ce0d322e04f0ffc7774218b251530a7b64ebefca55c90db3d0624c0ff4b3f03f918e8cf6f60300000003656500ffffffff9cce943872da8d8af29022d0b6321af5fefc004a281d07b598b95f6dcc07b1830200000007abab515351acab8d926410e69d76b7e584aad1470a97b14b9c879c8b43f9a9238e52a2c2fefc2001c56af8010000000400ab5253cd2cd1fe192ce3a93b5478af82fa250c27064df82ba416dfb0debf4f0eb307a746b6928901000000096500abacac6a0063514214524502947efc0200000000035251652c40340100000000096a6aab52000052656a5231c54c", + "51", + 2, + -2090320538, + "0322ca570446869ec7ec6ad66d9838cff95405002d474c0d3c17708c7ee039c6" + ], + [ + "47ac54940313430712ebb32004679d3a512242c2b33d549bf5bbc8420ec1fd0850ed50eb6d0300000009536aac6a65acacab51ffffffffb843e44266ce2462f92e6bff54316661048c8c17ecb092cb493b39bfca9117850000000001519ab348c05e74ebc3f67423724a3371dd99e3bceb4f098f8860148f48ad70000313c4c223000000000653006565656512c2d8dc033f3c97010000000002636aa993aa010000000006526365ab526ab7cf560300000000076a0065ac6a526500000000", + "005352535300ab6a", + 2, + 59531991, + "8b5b3d00d9c658f062fe6c5298e54b1fe4ed3a3eab2a87af4f3119edc47b1691" + ], + [ + "233cd90b043916fc41eb870c64543f0111fb31f3c486dc72457689dea58f75c16ae59e9eb2000000000500536a6a6affffffff9ae30de76be7cd57fb81220fce78d74a13b2dbcad4d023f3cadb3c9a0e45a3ce000000000965ac6353ac5165515130834512dfb293f87cb1879d8d1b20ebad9d7d3d5c3e399a291ce86a3b4d30e4e32368a9020000000453005165ffffffff26d84ae93eb58c81158c9b3c3cbc24a84614d731094f38d0eea8686dec02824d0300000005636a65abacf02c784001a0bd5d03000000000900655351ab65ac516a416ef503", + "", + 1, + -295106477, + "b79f31c289e95d9dadec48ebf88e27c1d920661e50d090e422957f90ff94cb6e" + ], + [ + "9200e26b03ff36bc4bf908143de5f97d4d02358db642bd5a8541e6ff709c420d1482d471b70000000008abab65536a636553ffffffff61ba6d15f5453b5079fb494af4c48de713a0c3e7f6454d7450074a2a80cb6d880300000007ac6a00ab5165515dfb7574fbce822892c2acb5d978188b1d65f969e4fe874b08db4c791d176113272a5cc10100000000ffffffff0420958d000000000009ac63516a0063516353dd885505000000000465ac00007b79e901000000000066d8bf010000000005525252006a00000000", + "ac5152", + 0, + 2089531339, + "89ec7fab7cfe7d8d7d96956613c49dc48bf295269cfb4ea44f7333d88c170e62" + ], + [ + "45f335ba01ce2073a8b0273884eb5b48f56df474fc3dff310d9706a8ac7202cf5ac188272103000000025363ffffffff049d859502000000000365ab6a8e98b1030000000002ac51f3a80603000000000752535151ac00000306e30300000000020051b58b2b3a", + "", + 0, + 1899564574, + "78e01310a228f645c23a2ad0acbb8d91cedff4ecdf7ca997662c6031eb702b11" + ], + [ + "d8f652a6043b4faeada05e14b81756cd6920cfcf332e97f4086961d49232ad6ffb6bc6c097000000000453526563ffffffff1ea4d60e5e91193fbbc1a476c8785a79a4c11ec5e5d6c9950c668ceacfe07a15020000000352ab51fffffffffe029a374595c4edd382875a8dd3f20b9820abb3e93f877b622598d11d0b09e503000000095351000052ac515152ffffffff9d65fea491b979699ceb13caf2479cd42a354bd674ded3925e760758e85a756803000000046365acabffffffff0169001d00000000000651636a65656300000000", + "ab0063630000ac", + 3, + 1050965951, + "4cc85cbc2863ee7dbce15490d8ca2c5ded61998257b9eeaff968fe38e9f009ae" + ], + [ + "718662be026e1dcf672869ac658fd0c87d6835cfbb34bd854c44e577d5708a7faecda96e260300000004526a636a489493073353b678549adc7640281b9cbcb225037f84007c57e55b874366bb7b0fa03bdc00000000095165ababac65ac00008ab7f2a802eaa53d000000000007acac516aac526ae92f380100000000056aac00536500000000", + "ab00", + 1, + 43296088, + "2d642ceee910abff0af2116af75b2e117ffb7469b2f19ad8fef08f558416d8f7" + ], + [ + "94083c840288d40a6983faca876d452f7c52a07de9268ad892e70a81e150d602a773c175ad03000000007ec3637d7e1103e2e7e0c61896cbbf8d7e205b2ecc93dd0d6d7527d39cdbf6d335789f660300000000ffffffff019e1f7b03000000000800ac0051acac0053539cb363", + "", + 1, + -183614058, + "a17b66d6bb427f42653d08207a22b02353dd19ccf2c7de6a9a3a2bdb7c49c9e7" + ], + [ + "30e0d4d20493d0cd0e640b757c9c47a823120e012b3b64c9c1890f9a087ae4f2001ca22a61010000000152f8f05468303b8fcfaad1fb60534a08fe90daa79bff51675472528ebe1438b6f60e7f60c10100000009526aab6551ac510053ffffffffaaab73957ea2133e32329795221ed44548a0d3a54d1cf9c96827e7cffd1706df0200000009ab00526a005265526affffffffd19a6fe54352015bf170119742821696f64083b5f14fb5c7d1b5a721a3d7786801000000085265abababac53abffffffff020f39bd030000000004ab6aac52049f6c050000000004ab52516aba5b4c60", + "6a6365516a6a655253", + 0, + -624256405, + "8e221a6c4bf81ca0d8a0464562674dcd14a76a32a4b7baf99450dd9195d411e6" + ], + [ + "f9c69d940276ec00f65f9fe08120fc89385d7350388508fd80f4a6ba2b5d4597a9e21c884f010000000663ab63ababab15473ae6d82c744c07fc876ecd53bd0f3018b2dbedad77d757d5bdf3811b23d294e8c0170000000001abafababe00157ede2050000000006ac6a5263635300000000", + "ab53", + 1, + 606547088, + "714d8b14699835b26b2f94c58b6ea4c53da3f7adf0c62ea9966b1e1758272c47" + ], + [ + "5c0ac112032d6885b7a9071d3c5f493aa16c610a4a57228b2491258c38de8302014276e8be030000000300ab6a17468315215262ad5c7393bb5e0c5a6429fd1911f78f6f72dafbbbb78f3149a5073e24740300000003ac5100ffffffff33c7a14a062bdea1be3c9c8e973f54ade53fe4a69dcb5ab019df5f3345050be00100000008ac63655163526aab428defc0033ec36203000000000765516365536a00ae55b2000000000002ab53f4c0080400000000095265516a536563536a00000000", + "6a005151006a", + 2, + 272749594, + "91082410630337a5d89ff19145097090f25d4a20bdd657b4b953927b2f62c73b" + ], + [ + "e3683329026720010b08d4bec0faa244f159ae10aa582252dd0f3f80046a4e145207d54d31000000000852acac52656aacac3aaf2a5017438ad6adfa3f9d05f53ebed9ceb1b10d809d507bcf75e0604254a8259fc29c020000000653526552ab51f926e52c04b44918030000000000f7679c0100000000090000525152005365539e3f48050000000009516500ab635363ab008396c905000000000253650591024f", + "6a6365", + 0, + 908746924, + "458aec3b5089a585b6bad9f99fd37a2b443dc5a2eefac2b7e8c5b06705efc9db" + ], + [ + "48c4afb204204209e1df6805f0697edaa42c0450bbbd767941fe125b9bc40614d63d757e2203000000066a5363005152dc8b6a605a6d1088e631af3c94b8164e36e61445e2c60130292d81dabd30d15f54b355a802000000036a6353ffffffff1d05dcec4f3dedcfd02c042ce5d230587ee92cb22b52b1e59863f3717df2362f0300000005536552ac52ffffffffd4d71c4f0a7d53ba47bb0289ca79b1e33d4c569c1e951dd611fc9c9c1ca8bc6c030000000865536a65ab51abacffffffff042f9aa905000000000753655153656351ab93d8010000000002655337440e0300000000005d4c690000000000015278587acb", + "ab006565526a51", + 0, + 1502064227, + "bbed77ff0f808aa8abd946ba9e7ec1ddb003a969fa223dee0af779643cb841a9" + ], + [ + "00b20fd104dd59705b84d67441019fa26c4c3dec5fd3b50eca1aa549e750ef9ddb774dcabe000000000651ac656aac65ffffffff52d4246f2db568fc9eea143e4d260c698a319f0d0670f84c9c83341204fde48b0200000000ffffffffb8aeabb85d3bcbc67b132f1fd815b451ea12dcf7fc169c1bc2e2cf433eb6777a03000000086a51ac6aab6563acd510d209f413da2cf036a31b0def1e4dcd8115abf2e511afbcccb5ddf41d9702f28c52900100000006ac52ab6a0065ffffffff039c8276000000000008ab53655200656a52401561010000000003acab0082b7160100000000035100ab00000000", + "535265", + 1, + -947367579, + "3212c6d6dd8d9d3b2ac959dec11f4638ccde9be6ed5d36955769294e23343da0" + ], + [ + "455131860220abbaa72015519090a666faf137a0febce7edd49da1eada41feab1505a0028b02000000036365ab453ead4225724eb69beb590f2ec56a7693a608871e0ab0c34f5e96157f90e0a96148f3c502000000085251ab51535163acffffffff022d1249040000000009abac00acac6565630088b310040000000000e3920e59", + "5152ab6a52ac5152", + 0, + 294375737, + "c40fd7dfa72321ac79516502500478d09a35cc22cc264d652c7d18b14400b739" + ], + [ + "624d28cb02c8747915e9af2b13c79b417eb34d2fa2a73547897770ace08c6dd9de528848d3030000000651ab63abab533c69d3f9b75b6ef8ed2df50c2210fd0bf4e889c42477d58682f711cbaece1a626194bb85030000000765acab53ac5353ffffffff018cc280040000000009abacabac52636352ac6859409e", + "ac51ac", + 1, + 1005144875, + "919144aada50db8675b7f9a6849c9d263b86450570293a03c245bd1e3095e292" + ], + [ + "8f28471d02f7d41b2e70e9b4c804f2d90d23fb24d53426fa746bcdcfffea864925bdeabe3e0200000001acffffffff76d1d35d04db0e64d65810c808fe40168f8d1f2143902a1cc551034fd193be0e0000000001acffffffff048a5565000000000005005151516afafb610400000000045263ac53648bb30500000000086363516a6a5165513245de01000000000000000000", + "6a0053510053", + 1, + -1525137460, + "305fc8ff5dc04ebd9b6448b03c9a3d945a11567206c8d5214666b30ec6d0d6cc" + ], + [ + "10ec50d7046b8b40e4222a3c6449490ebe41513aad2eca7848284a08f3069f3352c2a9954f0000000009526aac656352acac53ffffffff0d979f236155aa972472d43ee6f8ce22a2d052c740f10b59211454ff22cb7fd00200000007acacacab63ab53ffffffffbbf97ebde8969b35725b2e240092a986a2cbfd58de48c4475fe077bdd493a20c010000000663ab5365ababffffffff4600722d33b8dba300d3ad037bcfc6038b1db8abfe8008a15a1de2da2264007302000000035351ac6dbdafaf020d0ccf04000000000663ab6a51ab6ae06e5e0200000000036aabab00000000", + "", + 0, + -1658960232, + "2420dd722e229eccafae8508e7b8d75c6920bfdb3b5bac7cb8e23419480637c2" + ], + [ + "fef98b7101bf99277b08a6eff17d08f3fcb862e20e13138a77d66fba55d54f26304143e5360100000006515365abab00ffffffff04265965030000000004655252ace2c775010000000001002b23b4040000000007516a5153ab53ac456a7a00000000000753ab525251acacba521291", + "526aacacab00abab53", + 0, + -1614097109, + "4370d05c07e231d6515c7e454a4e401000b99329d22ed7def323976fa1d2eeb5" + ], + [ + "34a2b8830253661b373b519546552a2c3bff7414ea0060df183b1052683d78d8f54e842442000000000152ffffffffd961a8e34cf374151058dfcddc86509b33832bc57267c63489f69ff01199697c0300000002abacba856cfb01b17c2f050000000008515365ac53ab000000000000", + "5263ab656a", + 1, + -2104480987, + "2f9993e0a84a6ca560d6d1cc2b63ffe7fd71236d9cfe7d809491cef62bbfad84" + ], + [ + "43559290038f32fda86580dd8a4bc4422db88dd22a626b8bd4f10f1c9dd325c8dc49bf479f01000000026351ffffffff401339530e1ed3ffe996578a17c3ec9d6fccb0723dd63e7b3f39e2c44b976b7b0300000006ab6a65656a51ffffffff6fb9ba041c96b886482009f56c09c22e7b0d33091f2ac5418d05708951816ce7000000000551ac525100ffffffff020921e40500000000035365533986f40500000000016a00000000", + "52ac51", + 0, + 1769771809, + "02040283ef2291d8e1f79bb71bdabe7c1546c40d7ed615c375643000a8b9600d" + ], + [ + "6878a6bd02e7e1c8082d5e3ee1b746cfebfac9e8b97e61caa9e0759d8a8ecb3743e36a30de0100000002ab532a911b0f12b73e0071f5d50b6bdaf783f4b9a6ce90ec0cad9eecca27d5abae188241ddec0200000001651c7758d803f7457b0500000000036551515f4e90000000000001007022080200000000035365acc86b6946", + "6351ab", + 0, + -1929374995, + "f24be499c58295f3a07f5f1c6e5084496ae160450bd61fdb2934e615289448f1" + ], + [ + "35b6fc06047ebad04783a5167ab5fc9878a00c4eb5e7d70ef297c33d5abd5137a2dea9912402000000036aacacffffffff21dc291763419a584bdb3ed4f6f8c60b218aaa5b99784e4ba8acfec04993e50c03000000046a00ac6affffffff69e04d77e4b662a82db71a68dd72ef0af48ca5bebdcb40f5edf0caf591bb41020200000000b5db78a16d93f5f24d7d932f93a29bb4b784febd0cbb1943f90216dc80bba15a0567684b000000000853ab52ab5100006a1be2208a02f6bdc103000000000265ab8550ea04000000000365636a00000000", + "", + 0, + -1114114836, + "1c8655969b241e717b841526f87e6bd68b2329905ba3fc9e9f72526c0b3ea20c" + ], + [ + "bebb90c302bf91fd4501d33555a5fc5f2e1be281d9b7743680979b65c3c919108cc2f517510100000003abab00ffffffff969c30053f1276550532d0aa33cfe80ca63758cd215b740448a9c08a84826f3303000000056565ab5153ffffffff04bf6f2a04000000000565ab5265ab903e760100000000026a6a7103fa020000000006526553525365b05b2c000000000006ab000000535300000000", + "51510053ab63635153", + 1, + 1081291172, + "94338cd47a4639be30a71e21a7103cee4c99ef7297e0edd56aaf57a068b004de" + ], + [ + "af48319f031b4eeb4319714a285f44244f283cbff30dcb9275b06f2348ccd0d7f015b54f8500000000066363ac65ac6affffffff2560a9817ebbc738ad01d0c9b9cf657b8f9179b1a7f073eb0b67517409d108180200000005ac6365ab52ffffffff0bdd67cd4ecae96249a2e2a96db1490ee645f042fd9d5579de945e22b799f4d003000000086552ab515153ab00cf187c8202e51abf0300000000066552006a00abadf37d000000000004ac6a535100000000", + "63ab65", + 1, + -1855554446, + "60caf46a7625f303c04706cec515a44b68ec319ee92273acb566cca4f66861c1" + ], + [ + "f35befbc03faf8c25cc4bc0b92f6239f477e663b44b83065c9cb7cf231243032cf367ce3130000000005ab65526a517c4c334149a9c9edc39e29276a4b3ffbbab337de7908ea6f88af331228bd90086a6900ba020000000151279d19950d2fe81979b72ce3a33c6d82ebb92f9a2e164b6471ac857f3bbd3c0ea213b542010000000953ab51635363520065052657c20300a9ba04000000000452636a6a0516ea020000000008535253656365ababcfdd3f01000000000865ac516aac00530000000000", + "", + 2, + -99793521, + "c834a5485e68dc13edb6c79948784712122440d7fa5bbaa5cd2fc3d4dac8185d" + ], + [ + "d3da18520216601acf885414538ce2fb4d910997eeb91582cac42eb6982c9381589587794f0300000000fffffffff1b1c9880356852e10cf41c02e928748dd8fae2e988be4e1c4cb32d0bfaea6f7000000000465ab6aabffffffff02fb0d69050000000002ababeda8580500000000085163526565ac52522b913c95", + "ac", + 1, + -1247973017, + "99b32b5679d91e0f9cdd6737afeb07459806e5acd7630c6a3b9ab5d550d0c003" + ], + [ + "8218eb740229c695c252e3630fc6257c42624f974bc856b7af8208df643a6c520ef681bfd00000000002510066f30f270a09b2b420e274c14d07430008e7886ec621ba45665057120afce58befca96010300000004525153ab84c380a9015d96100000000000076a5300acac526500000000", + "ac005263", + 0, + -1855679695, + "5071f8acf96aea41c7518bd1b5b6bbe16258b529df0c03f9e374b83c66b742c6" + ], + [ + "1123e7010240310013c74e5def60d8e14dd67aedff5a57d07a24abc84d933483431b8cf8ea0300000003530051fc6775ff1a23c627a2e605dd2560e84e27f4208300071e90f4589e762ad9c9fe8d0da95e020000000465655200ffffffff04251598030000000004ab65ab639d28d90400000000096563636aacac525153474df801000000000851525165ac51006a75e23b040000000000e5bd3a4a", + "6363636565", + 0, + -467124448, + "9cb0dd04e9fe287b112e94a1647590d27e8b164ca13c4fe70c610fd13f82c2fd" + ], + [ + "fd92fe1003083c5179f97e77bf7d71975788138147adbdb283306802e261c0aee080fa22630200000000860c643ba9a1816b9badf36077b4554d11720e284e395a1121bc45279e148b2064c65e49020000000651ab6a53636a2c713088d20f4bc4001264d972cce05b9fe004dc33376ad24d0d013e417b91a5f1b6734e000000000100ffffffff02e3064c0500000000066552006a5165b86e8705000000000665ab65ab53522052eadb", + "00ab53525265", + 0, + 776203277, + "47207b48777727532f62e09afcd4104ea6687e723c7657c30504fa2081331cc8" + ], + [ + "d1b6a703038f14d41fcc5cc45455faa135a5322be4bf0f5cbcd526578fc270a236cacb853f0200000001abffffffff135aeff902fa38f202ccf5bd34437ff89c9dc57a028b62447a0a38579383e8ef0000000000ffffffffadf398d2c818d0b90bc474f540c3618a4a643482eeab73d36101987e2ec0335900000000004bd3323504e69fc10000000000055151535251790ada02000000000563ab6aab521337a704000000000963ac63abacac52656a1e9862010000000007656500ac51ab6a8f4ee672", + "ab5251656565ac63", + 2, + 82008394, + "b8f3d255549909c07588ecba10a02e55a2d6f2206d831af9da1a7dae64cfbc8b" + ], + [ + "81dadaa7011556683db3fe95262f4fdb20391b7e75b7ffcee51b176af64d83c06f85545d620200000005ab5151ab52ffffffff044805ef0300000000065353516352639702c802000000000900516351515252ab5270db08040000000009ac516aab526553abac4aabc90500000000096365ab0052636a525100000000", + "6565ab6a5152", + 0, + -2126294159, + "ad01ec9d6dbae325ec3a8e1fd98e2d03b1188378210efef093dd8b0b0ef3f19d" + ], + [ + "3b937e05032b8895d2f4945cb7e3679be2fbd15311e2414f4184706dbfc0558cf7de7b4d000000000001638b91a12668a3c3ce349788c961c26aa893c862f1e630f18d80e7843686b6e1e6fc396310000000000852635353ab65ac51eeb09dd1c9605391258ee6f74b9ae17b5e8c2ef010dc721c5433dcdc6e93a1593e3b6d1700000000085365ac6553526351ffffffff0308b18e04000000000253acb6dd00040000000008536aac5153ac516ab0a88201000000000500ac006500804e3ff2", + "", + 0, + 416167343, + "595a3c02254564634e8085283ec4ea7c23808da97ce9c5da7aecd7b553e7fd7f" + ], + [ + "a48f27ca047997470da74c8ee086ddad82f36d9c22e790bd6f8603ee6e27ad4d3174ea875403000000095153ac636aab6aacabffffffffefc936294e468d2c9a99e09909ba599978a8c0891ad47dc00ba424761627cef202000000056a51630053ffffffff304cae7ed2d3dbb4f2fbd679da442aed06221ffda9aee460a28ceec5a9399f4e0200000000f5bddf82c9c25fc29c5729274c1ff0b43934303e5f595ce86316fc66ad263b96ca46ab8d0100000003536500d7cf226b0146b00c04000000000200ac5c2014ce", + "515100636563", + 0, + 1991799059, + "9c051a7092fe17fa62b1720bc2c4cb2ffc1527d9fb0b006d2e142bb8fe07bf3c" + ], + [ + "180cd53101c5074cf0b7f089d139e837fe49932791f73fa2342bd823c6df6a2f72fe6dba1303000000076a6a63ac53acabffffffff03853bc1020000000007ac526a6a6a6a003c4a8903000000000453515163a0fbbd030000000005ab656a5253253d64cf", + "ac65", + 0, + -1548453970, + "4d8efb3b99b9064d2f6be33b194a903ffabb9d0e7baa97a48fcec038072aac06" + ], + [ + "c21ec8b60376c47e057f2c71caa90269888d0ffd5c46a471649144a920d0b409e56f190b700000000008acac6a526a536365ffffffff5d315d9da8bf643a9ba11299450b1f87272e6030fdb0c8adc04e6c1bfc87de9a0000000000ea43a9a142e5830c96b0ce827663af36b23b0277244658f8f606e95384574b91750b8e940000000007516a63ac0063acffffffff023c61be0400000000055165ab5263313cc8020000000006006a53526551ed8c3d56", + "6a", + 1, + 1160627414, + "a638cc17fd91f4b1e77877e8d82448c84b2a4e100df1373f779de7ad32695112" + ], + [ + "128cd90f04b66a4cbc78bf48748f6eec0f08d5193ee8d0a6f2e8d3e5f138ed12c2c87d01a301000000085200ab6aac00ab00ffffffff09fc88bb1851e3dfb3d30179c38e15aeb1b39929c7c74f6acd071994ed4806490300000000e7fc5ea12ec56f56c0d758ecf4bb88aa95f3b08176b336db3b9bec2f6e27336dce28adbe030000000400530051fffffffffd6ff1adcf1fbe0d883451ee46904f1b7e8820243d395559b2d4ee8190a6e891000000000080fb1ae702f85b400000000000035200ab8d9651010000000006ab6a52536aab00000000", + "ab", + 1, + 1667598199, + "c10ccc9db8a92d7d4b133a2980782dab9d9d1d633d0dde9f9612ada57771fd89" + ], + [ + "da9695a403493d3511c10e1fe1286f954db0366b7667c91ef18ae4578056c1bf752114ac5901000000035351519788d91dd1f9c62dc005d80ea54eb13f7131ca5aace3d5d29f9b58ccc5fbc9a27e779950010000000453ac6a00ffffffffe2556ff29ebe83eb42a32c7a8d93bc598043578f491b5935805a33608538845a030000000252ab65d21b3b018f26c4030000000006acab51535352e1cbcb10", + "006565ab52", + 2, + -1550927794, + "0ca673a1ee66f9625ceb9ab278ebef772c113c188112b02824570c17fdf48194" + ], + [ + "b240517501334021240427adb0b413433641555424f6d24647211e3e6bfbb22a8045cbda2f000000000071bac8630112717802000000000000000000", + "6a5165abac52656551", + 0, + 1790414254, + "2c8be597620d95abd88f9c1cf4967c1ae3ca2309f3afec8928058c9598660e9e" + ], + [ + "96bac43903044a199b4b3efeeec5d196ee23fb05495541fa2cd6fb6405a9432d1723363660010000000151ffffffffe6ce2b66ce1488918a3e880bebb0e750123f007c7bcbac8fcd67ce75cb6fbae80300000000ffffffff9c0955aa07f506455834895c0c56be5a095398f47c62a3d431fe125b161d666a0200000005520000abac7ffdbc540216f2f004000000000165a26dce010000000001ab00000000", + "5151ab656a656a6a63", + 0, + -707123065, + "26b22e18d5d9081fde9631594a4f7c49069ed2e429f3d08caf9d834f685ccab2" + ], + [ + "b8fd394001ed255f49ad491fecc990b7f38688e9c837ccbc7714ddbbf5404f42524e68c18f0000000007ab6353535363ab081e15ee02706f7d050000000008515200535351526364c7ec040000000005636a53acac9206cbe1", + "655352ac", + 0, + -1251578838, + "8e0697d8cd8a9ccea837fd798cc6c5ed29f6fbd1892ee9bcb6c944772778af19" + ], + [ + "e42a76740264677829e30ed610864160c7f97232c16528fe5610fc08814b21c34eefcea69d010000000653006a6a0052ffffffff647046cf44f217d040e6a8ff3f295312ab4dd5a0df231c66968ad1c6d8f4428000000000025352ffffffff0199a7f900000000000000000000", + "655263006a005163", + 1, + 1122505713, + "7cda43f1ff9191c646c56a4e29b1a8c6cb3f7b331da6883ef2f0480a515d0861" + ], + [ + "0f034f32027a8e094119443aa9cfe11737c6d7dda9a52b839bc073dcc0235b847b28e0fab60200000006ac53ac536a63eee63447dfdad80476994b68706e916df1bd9d7cb4f3a4f6b14369de84564bea2e8688bd030000000565636a65acf8434663020b35fe01000000000800abab655163acabb3d6a103000000000353acab345eeda0", + "526a51ac63ab51", + 1, + 66020215, + "4435e62ff6531ac73529aac9cf878a7219e0b6e6cac79af8487c5355d1ad6d43" + ], + [ + "a2dfa4690214c1ab25331815a5128f143219de51a47abdc7ce2d367e683eeb93960a31af9f010000000363636affffffff8be0628abb1861b078fcc19c236bc4cc726fa49068b88ad170adb2a97862e7460200000004ac655363ffffffff0441f11103000000000153dbab0c000000000009ab53ac5365526aab63abbb95050000000004ab52516a29a029040000000003ac526a00000000", + "6a52ac63", + 1, + -1302210567, + "913060c7454e6c80f5ba3835454b54db2188e37dc4ce72a16b37d11a430b3d23" + ], + [ + "9dbc591f04521670af83fb3bb591c5d4da99206f5d38e020289f7db95414390dddbbeb56680100000004ac5100acffffffffb6a40b5e29d5e459f8e72d39f800089529f0889006cad3d734011991da8ef09d0100000009526a5100acab536a515fc427436df97cc51dc8497642ffc868857ee245314d28b356bd70adba671bd6071301fc0000000000ffffffff487efde2f620566a9b017b2e6e6d42525e4070f73a602f85c6dfd58304518db30000000005516353006a8d8090180244904a0200000000046a65656ab1e9c203000000000451ab63aba06a5449", + "", + 0, + -1414953913, + "bae189eb3d64aedbc28a6c28f6c0ccbd58472caaf0cf45a5aabae3e031dd1fea" + ], + [ + "1345fb2c04bb21a35ae33a3f9f295bece34650308a9d8984a989dfe4c977790b0c21ff9a7f0000000006ac52ac6a0053ffffffff7baee9e8717d81d375a43b691e91579be53875350dfe23ba0058ea950029fcb7020000000753ab53ab63ab52ffffffff684b6b3828dfb4c8a92043b49b8cb15dd3a7c98b978da1d314dce5b9570dadd202000000086353ab6a5200ac63d1a8647bf667ceb2eae7ec75569ca249fbfd5d1b582acfbd7e1fcf5886121fca699c011d0100000003ac006affffffff049b1eb00300000000001e46dc0100000000080065ab6a6a630065ca95b40300000000030051520c8499010000000006ab6aac526a6500000000", + "53526aac636300", + 2, + 1809978100, + "cfeaa36790bc398783d4ca45e6354e1ea52ee74e005df7f9ebd10a680e9607bf" + ], + [ + "7d75dc8f011e5f9f7313ba6aedef8dbe10d0a471aca88bbfc0c4a448ce424a2c5580cda1560300000003ab5152ffffffff01997f8e0200000000096552ac6a65656563530d93bbcc", + "00656a6563", + 0, + 1414485913, + "ec91eda1149f75bffb97612569a78855498c5d5386d473752a2c81454f297fa7" + ], + [ + "1459179504b69f01c066e8ade5e124c748ae5652566b34ed673eea38568c483a5a4c4836ca0100000008ac5352006563656affffffff5d4e037880ab1975ce95ea378d2874dcd49d5e01e1cdbfae3343a01f383fa35800000000095251ac52ac6aac6500ffffffff7de3ae7d97373b7f2aeb4c55137b5e947b2d5fb325e892530cb589bc4f92abd503000000086563ac53ab520052ffffffffb4db36a32d6e543ef49f4bafde46053cb85b2a6c4f0e19fa0860d9083901a1190300000003ab51531bbcfe5504a6dbda040000000008536a5365abac6500d660c80300000000096565abab6a53536a6a54e84e010000000003acac52df2ccf0500000000025351220c857e", + "", + 2, + 1879181631, + "3aad18a209fab8db44954eb55fd3cc7689b5ec9c77373a4d5f4dae8f7ae58d14" + ], + [ + "d98b777f04b1b3f4de16b07a05c31d79965579d0edda05600c118908d7cf642c9cd670093f020000000953005351ac65ab5363a268caad6733b7d1718008997f249e1375eb3ab9fe68ab0fe170d8e745ea24f54ce67f9b00000000066500516a5151ffffffff7ef8040dfcc86a0651f5907e8bfd1017c940f51cf8d57e3d3fe78d57e40b1e610200000003535263ffffffff39846cfed4babc098ff465256ba3820c30d710581316afcb67cd31c623b703360300000001acffffffff03d405120100000000056300006a5201a73d050000000004ab636a6a294c8c000000000006ac65536553ac00000000", + "63525351abac", + 1, + 2018694761, + "86970af23c89b72a4f9d6281e46b9ef5220816bed71ebf1ae20df53f38fe16ff" + ], + [ + "cabb1b06045a895e6dcfc0c1e971e94130c46feace286759f69a16d298c8b0f6fd0afef8f20300000004ac006352ffffffffa299f5edac903072bfb7d29b663c1dd1345c2a33546a508ba5cf17aab911234602000000056a65515365ffffffff89a20dc2ee0524b361231092a070ace03343b162e7162479c96b757739c8394a0300000002abab92ec524daf73fabee63f95c1b79fa8b84e92d0e8bac57295e1d0adc55dc7af5534ebea410200000001534d70e79b04674f6f00000000000600abacab53517d60cc0200000000035265ab96c51d040000000004ac6300ac62a787050000000008006a516563ab63639e2e7ff7", + "6551ac6351ac", + 3, + 1942663262, + "d0c4a780e4e0bc22e2f231e23f01c9d536b09f6e5be51c123d218e906ec518be" + ], + [ + "8b96d7a30132f6005b5bd33ea82aa325e2bcb441f46f63b5fca159ac7094499f380f6b7e2e00000000076aacabac6300acffffffff0158056700000000000465005100c319e6d0", + "52006a", + 0, + -1100733473, + "fb4bd26a91b5cf225dd3f170eb09bad0eac314bc1e74503cc2a3f376833f183e" + ], + [ + "112191b7013cfbe18a175eaf09af7a43cbac2c396f3695bbe050e1e5f4250603056d60910e02000000001c8a5bba03738a22010000000005525352656a77a149010000000002510003b52302000000000351ac52722be8e6", + "65ac6565", + 0, + -1847972737, + "8e795aeef18f510d117dfa2b9f4a2bd2e2847a343205276cedd2ba14548fd63f" + ], + [ + "ce6e1a9e04b4c746318424705ea69517e5e0343357d131ad55d071562d0b6ebfedafd6cb840100000003656553ffffffff67bd2fa78e2f52d9f8900c58b84c27ef9d7679f67a0a6f78645ce61b883fb8de000000000100d699a56b9861d99be2838e8504884af4d30b909b1911639dd0c5ad47c557a0773155d4d303000000046a5151abffffffff9fdb84b77c326921a8266854f7bbd5a71305b54385e747fe41af8a397e78b7fa010000000863acac6a51ab00ac0d2e9b9d049b8173010000000007ac53526a650063ba9b7e010000000008526a00525263acac0ab3fd030000000000ea8a0303000000000200aca61a97b9", + "", + 1, + -1276952681, + "b6ed4a3721be3c3c7305a5128c9d418efa58e419580cec0d83f133a93e3a22c5" + ], + [ + "a7721d94021652d90c79aaf5022d98219337d50f836382403ed313adb1116ba507ac28b0b0010000000551ac6300ab89e6d64a7aa81fb9595368f04d1b36d7020e7adf5807535c80d015f994cce29554fe869b01000000065353ab636500ffffffff024944c90100000000046300635369df9f01000000000000000000", + "656a536551ab", + 0, + -1740151687, + "935892c6f02948f3b08bcd463b6acb769b02c1912be4450126768b055e8f183a" + ], + [ + "2f7353dd02e395b0a4d16da0f7472db618857cd3de5b9e2789232952a9b154d249102245fd030000000151617fd88f103280b85b0a198198e438e7cab1a4c92ba58409709997cc7a65a619eb9eec3c0200000003636aabffffffff0397481c0200000000045300636a0dc97803000000000009d389030000000003ac6a53134007bb", + "0000536552526a", + 0, + -1912746174, + "30c4cd4bd6b291f7e9489cc4b4440a083f93a7664ea1f93e77a9597dab8ded9c" + ], + [ + "7d95473604fd5267d0e1bb8c9b8be06d7e83ff18ad597e7a568a0aa033fa5b4e1e2b6f1007020000000465006a6affffffffaee008503bfc5708bd557c7e78d2eab4878216a9f19daa87555f175490c40aaf000000000263abffffffffabd74f0cff6e7ceb9acc2ee25e65af1abcebb50c08306e6c78fa8171c37613dd010000000552acacababffffffff54a3069393f7930fa1b331cdff0cb945ec21c11d4605d8eedba1d3e094c6ae1f01000000026300ffffffff0182edeb050000000009526353ab5153530065a247e8cd", + "51516aab00", + 2, + -426210430, + "2707ca714af09494bb4cf0794abe33c6cba5f29891d619e76070269d1fa8e690" + ], + [ + "221d4718023d9ca9fe1af178dbfce02b2b369bf823ea3f43f00891b7fef98e215c06b94fdd000000000951005153ab000051acffffffffb1c7ad1c64b7441bf5e70cd0f6eb4ec96821d67fc4997d9e6dfdceadecd36dde01000000070051536a635153ffffffff04e883cd00000000000851ab536553ab0052bbb2f70400000000002f1b2e03000000000165259fcb00000000000010dbde99", + "ab", + 1, + 665721280, + "4abce77432a86dfe608e7c1646c18b5253a373392ff962e288e3ab96bba1ba1d" + ], + [ + "6f66c0b3013e6ae6aabae9382a4326df31c981eac169b6bc4f746edaa7fc1f8c796ef4e374000000000665ab6aabac6affffffff0191c8d6030000000002525300000000", + "6a5352516a635352ab", + 0, + -1299629906, + "48411efeb133c6b7fec4e7bdbe613f827093cb06ea0dbcc2ffcfde3a9ac4356c" + ], + [ + "89e7928c04363cb520eff4465251fd8e41550cbd0d2cdf18c456a0be3d634382abcfd4a2130200000006ac516a6a656355042a796061ed72db52ae47d1607b1ceef6ca6aea3b7eea48e7e02429f382b378c4e51901000000085351ab6352ab5252ffffffff53631cbda79b40183000d6ede011c778f70147dc6fa1aed3395d4ce9f7a8e69701000000096a6553ab52516a52abad0de418d80afe059aab5da73237e0beb60af4ac490c3394c12d66665d1bac13bdf29aa8000000000153f2b59ab6027a33eb040000000007005351ac5100ac88b941030000000003ab0052e1e8a143", + "63656a", + 0, + 1258533326, + "b575a04b0bb56e38bbf26e1a396a76b99fb09db01527651673a073a75f0a7a34" + ], + [ + "ca356e2004bea08ec2dd2df203dc275765dc3f6073f55c46513a588a7abcc4cbde2ff011c7020000000553525100003aefec4860ef5d6c1c6be93e13bd2d2a40c6fb7361694136a7620b020ecbaca9413bcd2a030000000965ac00536352535100ace4289e00e97caaea741f2b89c1143060011a1f93090dc230bee3f05e34fbd8d8b6c399010000000365526affffffff48fc444238bda7a757cb6a98cb89fb44338829d3e24e46a60a36d4e24ba05d9002000000026a53ffffffff03d70b440200000000056a6a526aac853c97010000000002515335552202000000000351635300000000", + "0052", + 3, + -528192467, + "fc93cc056c70d5e033933d730965f36ad81ef64f1762e57f0bc5506c5b507e24" + ], + [ + "82d4fa65017958d53e562fac073df233ab154bd0cf6e5a18f57f4badea8200b217975e31030200000004636aab51ac0891a204227cc9050000000006635200655365bfef8802000000000865650051635252acfc2d09050000000006ab65ac51516380195e030000000007ac52525352510063d50572", + "53", + 0, + -713567171, + "e095003ca82af89738c1863f0f5488ec56a96fb81ea7df334f9344fcb1d0cf40" + ], + [ + "75f6949503e0e47dd70426ef32002d6cdb564a45abedc1575425a18a8828bf385fa8e808e600000000036aabab82f9fd14e9647d7a1b5284e6c55169c8bd228a7ea335987cef0195841e83da45ec28aa2e0300000002516350dc6fe239d150efdb1b51aa288fe85f9b9f741c72956c11d9dcd176889963d699abd63f0000000001ab429a63f502777d20010000000007abac52ac516a53d081d9020000000003acac630c3cc3a8", + "535152516551510000", + 1, + 973814968, + "c6ec1b7cb5c16a1bfd8a3790db227d2acc836300534564252b57bd66acf95092" + ], + [ + "24f24cd90132b2162f938f1c22d3ca5e7daa83515883f31a61a5177aebf99d7db6bdfc398c010000000163ffffffff01d5562d0100000000016300000000", + "5265ac5165ac5252ab", + 0, + 1055129103, + "5eeb03e03806cd7bfd44bbba69c30f84c2c5120df9e68cd8facc605fcfbc9693" + ], + [ + "5ff2cac201423064a4d87a96b88f1669b33adddc6fa9acdc840c0d8a243671e0e6de49a5b00300000005ac6353655353b91db50180db5a03000000000663535151006a047a3aff", + "52ab51ab5365005163", + 0, + -1336626596, + "b8db8d57fe40ab3a99cf2f8ed57da7a65050fcc1d34d4280e25faf10108d3110" + ], + [ + "10011f150220ad76a50ccc7bb1a015eda0ff987e64cd447f84b0afb8dc3060bdae5b36a6900200000000ffffffff1e92dd814dfafa830187bc8e5b9258de2445ec07b02c420ee5181d0b203bb334000000000565ab536a65ffffffff0124e65401000000000800ab636553ab53ac00000000", + "53abab0051", + 0, + 440222748, + "c6675bf229737e005b5c8ffa6f81d9e2c4396840921b6151316f67c4315a4270" + ], + [ + "8b95ec900456648d820a9b8df1d8f816db647df8a8dc9f6e7151ebf6079d90ee3f6861352a02000000085200ab00ac535151ffffffff039b10b845f961225ac0bcaac4f5fe1991029a051aa3d06a3811b5762977a67403000000035252abffffffff8559d65f40d5e261f45aec8aad3d2c56c6114b22b26f7ee54a06f0881be3a7f5010000000765635252536363ffffffff38f8b003b50f6412feb2322b06b270197f81ad69c36af02ca5008b94eee5f650020000000165ffffffff01ae2b00010000000001638eb153a2", + "0053ab5300ac53", + 2, + 1266056769, + "205f3653f0142b35ce3ef39625442efebae98cde8cbf0516b97b51073bb0479f" + ], + [ + "babbb7ea01ab5d584727cb44393b17cf66521606dc81e25d85273be0d57bad43e8f6b6d43501000000036a656aba83a68803fb0f4a000000000005536353ab633fcfe4020000000009ac00acab6351006a65182a0c03000000000453ac5363bee74f44", + "536a6a6a6365ac51ab", + 0, + -799187625, + "3275e98dca37243b977525a07b5d8e369d6c3bdc08cb948029a635547d0d1a4e" + ], + [ + "e86a24bc03e4fae784cdf81b24d120348cb5e52d937cd9055402fdba7e43281e482e77a1c100000000046363006affffffffa5447e9bdcdab22bd20d88b19795d4c8fb263fbbf7ce8f4f9a85f865953a6325020000000663ac53535253ffffffff9f8b693bc84e0101fc73748e0513a8cecdc264270d8a4ee1a1b6717607ee1eaa00000000026a513417bf980158d82c020000000009005253005351acac5200000000", + "6353516365536a6a", + 2, + -563792735, + "508129278ef07b43112ac32faf00170ad38a500eed97615a860fd58baaad174b" + ], + [ + "53bd749603798ed78798ef0f1861b498fc61dcee2ee0f2b37cddb115b118e73bc6a5a47a0201000000096a63656a6aab6a000007ff674a0d74f8b4be9d2e8e654840e99d533263adbdd0cf083fa1d5dd38e44d2d163d900100000007abab5251ac6a51c8b6b63f744a9b9273ccfdd47ceb05d3be6400c1ed0f7283d32b34a7f4f0889cccf06be30000000009516a52636551ab516a9ac1fe63030c677e05000000000027bc610000000000086565636a635100526e2dc60200000000015300000000", + "6552536a515351ab", + 1, + -1617066878, + "fe516df92299e995b8e6489be824c6839543071ec5e9286060b2600935bf1f20" + ], + [ + "691bf9fc028ca3099020b79184e70039cf53b3c7b3fe695d661fd62d7b433e65feda2150610000000003ac63abffffffff2c814c15b142bc944192bddccb90a392cd05b968b599c1d8cd99a55a28a243fd0100000009ab5300526a5200abac98516a5803dfd3540500000000046552ac522838120100000000040053ab6a4409a903000000000665636a5300658759621b", + "65ac5165ab", + 0, + -359941441, + "d582c442e0ecc400c7ba33a56c93ad9c8cfd45af820350a13623594b793486f0" + ], + [ + "536bc5e60232eb60954587667d6bcdd19a49048d67a027383cc0c2a29a48b960dc38c5a0370300000005ac636300abffffffff8f1cfc102f39b1c9348a2195d496e602c77d9f57e0769dabde7eaaedf9c69e250100000006acabab6a6351ffffffff0432f56f0400000000046a5365517fd54b0400000000035265539484e4050000000003536a5376dc25020000000008ac536aab6aab536ab978e686", + "ac0051006a006a006a", + 0, + -273074082, + "f151f1ec305f698d9fdce18ea292b145a58d931f1518cf2a4c83484d9a429638" + ], + [ + "74606eba01c2f98b86c29ba5a32dc7a7807c2abe6ed8d89435b3da875d87c12ae05329e6070200000003510052ffffffff02a1e2c4020000000006516563526a63c68bae04000000000952ab6363ab00006363fe19ae4f", + "63ababacac5365", + 0, + 112323400, + "d1b1d79001b4a0324962607b739972d6f39c1493c4500ce814fd3bd72d32a5a0" + ], + [ + "2ed805e20399e52b5bcc9dc075dad5cf19049ff5d7f3de1a77aee9288e59c5f4986751483f020000000165ffffffff967531a5726e7a653a9db75bd3d5208fa3e2c5e6cd5970c4d3aba84eb644c72c0300000000ffffffffd79030d20c65e5f8d3c55b5692e5bdaa2ae78cfa1935a0282efb97515feac43f030000000400006365261ab88c02bdf66a000000000003ab6351d6ad8b000000000005525152abac00000000", + "630053ab5265", + 0, + 2072814938, + "1d25d16d84d5793be1ad5cda2de9c9cf70e04a66c3dae618f1a7ca4026198e7f" + ], + [ + "fab796ee03f737f07669160d1f1c8bf0800041157e3ac7961fea33a293f976d79ce49c02ab0200000003ac5252eb097ea1a6d1a7ae9dace338505ba559e579a1ee98a2e9ad96f30696d6337adcda5a85f403000000096500abab656a6a656396d5d41a9b11f571d91e4242ddc0cf2420eca796ad4882ef1251e84e42b930398ec69dd80100000005526551ac6a8e5d0de804f763bb0400000000015288271a010000000001acf2bf2905000000000300ab51c9641500000000000952655363636365ac5100000000", + "00ac536552", + 0, + -1854521113, + "f3bbab70b759fe6cfae1bf349ce10716dbc64f6e9b32916904be4386eb461f1f" + ], + [ + "f2b539a401e4e8402869d5e1502dbc3156dbce93583f516a4947b333260d5af1a34810c6a00200000003525363ffffffff01d305e2000000000005acab535200a265fe77", + "", + 0, + -1435650456, + "41617b27321a830c712638dbb156dae23d4ef181c7a06728ccbf3153ec53d7dd" + ], + [ + "9f10b1d8033aee81ac04d84ceee0c03416a784d1017a2af8f8a34d2f56b767aea28ff88c8f02000000025352ffffffff748cb29843bea8e9c44ed5ff258df1faf55fbb9146870b8d76454786c4549de100000000016a5ba089417305424d05112c0ca445bc7107339083e7da15e430050d578f034ec0c589223b0200000007abac53ac6565abffffffff025a4ecd010000000006636563ab65ab40d2700000000000056a6553526333fa296c", + "", + 0, + -395044364, + "20fd0eee5b5716d6cbc0ddf852614b686e7a1534693570809f6719b6fcb0a626" + ], + [ + "ab81755f02b325cbd2377acd416374806aa51482f9cc5c3b72991e64f459a25d0ddb52e66703000000036a00ab8727056d48c00cc6e6222be6608c721bc2b1e69d0ffbadd51d131f05ec54bcd83003aac5000000000003f2cdb60454630e020000000007526aac63000000e9e25c040000000003516a0088c97e0000000000076a535265655263771b5805000000000851ab00ac6565515100000000", + "5151ab00ac", + 0, + -230931127, + "ba0a2c987fcdd74b6915f6462f62c3f126a0750aa70048f7aa20f70726e6a20b" + ], + [ + "7a17e0ef0378dab4c601240639139335da3b7d684600fa682f59b7346ef39386fe9abd69350000000004ac5252ab807f26fb3249326813e18260a603b9ad66f41f05eaa8146f66bcca452162a502aac4aa8b02000000026a534ea460faa7e3d7854ec6c70d7e797025697b547ec500b2c09c873b4d5517767d3f3720660300000000ffffffff01b12e7a02000000000900ab006aab65656a63991c03e2", + "6aab6a", + 1, + -1577994103, + "62cd3413d9d819fb7355336365cf8a2a997f7436cc050a7143972044343b3281" + ], + [ + "ff2ecc09041b4cf5abb7b760e910b775268abee2792c7f21cc5301dd3fecc1b4233ee70a2c0200000009acac5300006a51526affffffffeb39c195a5426afff38379fc85369771e4933587218ef4968f3f05c51d6b7c92000000000165453a5f039b8dbef7c1ffdc70ac383b481f72f99f52b0b3a5903c825c45cfa5d2c0642cd50200000001654b5038e6c49daea8c0a9ac8611cfe904fc206dad03a41fb4e5b1d6d85b1ecad73ecd4c0102000000096a51000053ab656565bdb5548302cc719200000000000452655265214a3603000000000300ab6a00000000", + "52516a006a63", + 1, + -2113289251, + "37ed6fae36fcb3360c69cac8b359daa62230fc1419b2cf992a32d8f3e079dcff" + ], + [ + "70a8577804e553e462a859375957db68cfdf724d68caeacf08995e80d7fa93db7ebc04519d02000000045352ab53619f4f2a428109c5fcf9fee634a2ab92f4a09dc01a5015e8ecb3fc0d9279c4a77fb27e900000000006ab6a51006a6affffffff3ed1a0a0d03f25c5e8d279bb5d931b7eb7e99c8203306a6c310db113419a69ad010000000565516300abffffffff6bf668d4ff5005ef73a1b0c51f32e8235e67ab31fe019bf131e1382050b39a630000000004536a6563ffffffff02faf0bb00000000000163cf2b4b05000000000752ac635363acac15ab369f", + "ac", + 0, + -1175809030, + "1c9d6816c20865849078f9777544b5ddf37c8620fe7bd1618e4b72fb72dddca1" + ], + [ + "a3604e5304caa5a6ba3c257c20b45dcd468f2c732a8ca59016e77b6476ac741ce8b16ca8360200000004acac6553ffffffff695e7006495517e0b79bd4770f955040610e74d35f01e41c9932ab8ccfa3b55d0300000007ac5253515365acffffffff6153120efc5d73cd959d72566fc829a4eb00b3ef1a5bd3559677fb5aae116e38000000000400abab52c29e7abd06ff98372a3a06227386609adc7665a602e511cadcb06377cc6ac0b8f63d4fdb03000000055100acabacffffffff04209073050000000009ab5163ac525253ab6514462e05000000000952abacab636300656a20672c0400000000025153b276990000000000056565ab6a5300000000", + "5351", + 0, + 1460890590, + "249c4513a49076c6618aabf736dfd5ae2172be4311844a62cf313950b4ba94be" + ], + [ + "c6a72ed403313b7d027f6864e705ec6b5fa52eb99169f8ea7cd884f5cdb830a150cebade870100000009ac63ab516565ab6a51ffffffff398d5838735ff43c390ca418593dbe43f3445ba69394a6d665b5dc3b4769b5d700000000075265acab515365ffffffff7ee5616a1ee105fd18189806a477300e2a9cf836bf8035464e8192a0d785eea3030000000700ac6a51516a52ffffffff018075fd0000000000015100000000", + "005251acac5252", + 2, + -656067295, + "2cc1c7514fdc512fd45ca7ba4f7be8a9fe6d3318328bc1a61ae6e7675047e654" + ], + [ + "93c12cc30270fc4370c960665b8f774e07942a627c83e58e860e38bd6b0aa2cb7a2c1e060901000000036300abffffffff4d9b618035f9175f564837f733a2b108c0f462f28818093372eec070d9f0a5440300000001acffffffff039c2137020000000001525500990100000000055265ab636a07980e0300000000005ba0e9d1", + "656a5100", + 1, + 18954182, + "6beca0e0388f824ca33bf3589087a3c8ad0857f9fe7b7609ae3704bef0eb83e2" + ], + [ + "97bddc63015f1767619d56598ad0eb5c7e9f880b24a928fea1e040e95429c930c1dc653bdb0100000008ac53acac00005152aaa94eb90235ed10040000000000287bdd0400000000016a8077673a", + "acac6a536352655252", + 0, + -813649781, + "5990b139451847343c9bb89cdba0e6daee6850b60e5b7ea505b04efba15f5d92" + ], + [ + "cc3c9dd303637839fb727270261d8e9ddb8a21b7f6cbdcf07015ba1e5cf01dc3c3a327745d0300000000d2d7804fe20a9fca9659a0e49f258800304580499e8753046276062f69dbbde85d17cd2201000000096352536a520000acabffffffffbc75dfa9b5f81f3552e4143e08f485dfb97ae6187330e6cd6752de6c21bdfd21030000000600ab53650063ffffffff0313d0140400000000096565515253526aacac167f0a040000000008acab00535263536a9a52f8030000000006abab5151ab63f75b66f2", + "6a635353636a65ac65", + 1, + 377286607, + "dbc7935d718328d23d73f8a6dc4f53a267b8d4d9816d0091f33823bd1f0233e9" + ], + [ + "236f91b702b8ffea3b890700b6f91af713480769dda5a085ae219c8737ebae90ff25915a3203000000056300ac6300811a6a10230f12c9faa28dae5be2ebe93f37c06a79e76214feba49bb017fb25305ff84eb020000000100ffffffff041e351703000000000351ac004ff53e050000000003ab53636c1460010000000000cb55f701000000000651520051ab0000000000", + "acac636a6aac5300", + 0, + 406448919, + "793a3d3c37f6494fab79ff10c16702de002f63e34be25dd8561f424b0ea938c4" + ], + [ + "22e10d2003ab4ea9849a2801921113583b7c35c3710ff49a6003489395789a7cfb1e6051900100000006526a65535151ffffffff82f21e249ec60db33831d33b9ead0d56f6496db64337dcb7f1c3327c47729c4a020000000253abffffffff138f098f0e6a4cf51dc3e7a3b749f487d1ebde71b73b731d1d02ad1180ac7b8c02000000036563acda215011027a9484020000000007635165530000ac4bf6cb0400000000066aacabab65ab3ce3f32c", + "ab0052ab", + 2, + 1136359457, + "b5bd080bbcb8cd652f440484311d7a3cb6a973cd48f03c5c00fd6beb52dfc061" + ], + [ + "c47d5ad60485cb2f7a825587b95ea665a593769191382852f3514a486d7a7a11d220b62c54000000000663655253acab8c3cf32b0285b040e50dcf6987ddf7c385b3665048ad2f9317b9e0c5ba0405d8fde4129b00000000095251ab00ac65635300ffffffff549fe963ee410d6435bb2ed3042a7c294d0c7382a83edefba8582a2064af3265000000000152fffffffff7737a85e0e94c2d19cd1cde47328ece04b3e33cd60f24a8a345da7f2a96a6d0000000000865ab6a0051656aab28ff30d5049613ea020000000005ac51000063f06df1050000000008ac63516aabac5153afef5901000000000700656500655253688bc00000000000086aab5352526a53521ff1d5ff", + "51ac52", + 2, + -1296011911, + "0c1fd44476ff28bf603ad4f306e8b6c7f0135a441dc3194a6f227cb54598642a" + ], + [ + "0b43f122032f182366541e7ee18562eb5f39bc7a8e5e0d3c398f7e306e551cdef773941918030000000863006351ac51acabffffffffae586660c8ff43355b685dfa8676a370799865fbc4b641c5a962f0849a13d8250100000005abab63acabffffffff0b2b6b800d8e77807cf130de6286b237717957658443674df047a2ab18e413860100000008ab6aac655200ab63ffffffff04f1dbca03000000000800635253ab656a52a6eefd0300000000036365655d8ca90200000000005a0d530400000000015300000000", + "65ac65acac", + 0, + 351448685, + "86f26e23822afd1bdfc9fff92840fc1e60089f12f54439e3ab9e5167d0361dcf" + ], + [ + "4b0ecc0c03ba35700d2a30a71f28e432ff6ac7e357533b49f4e97cf28f1071119ad6b97f3e0300000008acab516363ac63acffffffffcd6a2019d99b5c2d639ddca0b1aa5ea7c1326a071255ea226960bd88f45ca57d00000000085253655363005353ffffffffba257635191c9f216de3277be548cb5a2313114cb1a4c563b03b4ef6c0f4f7040300000001abda542edf0495cdc40100000000026353c049e903000000000752516a53ab65512b0f9304000000000963ab516aac65516552fa9ece050000000009acab6500005152530000000000", + "65ab51525352510052", + 1, + -1355414590, + "3cd85f84aae6d702436f3f9b8980adcc1f8f202e957759540a27da0a32fc6c87" + ], + [ + "adaac0a803f66811346271c733036d6e0d45e15a9b602092e2e04ad93564f196e7f020b088000000000600526a636a00700ec3f9db07a3a6ce910bf318c7ec87a876e1f2a3366cc69f20cde09203b99c1cb9d15800000000050000ac636a4d0de554ebe95c6cc14faf5ff6361d1deba9474b8b0fd3b93c011cd96aec783abb3f36830200000005ab65005251ffffffff0464eb10050000000007520000ab6a65ab1beaa80300000000005a2f31050000000006526aab65ac52ba7db10000000000045251ab6a0cfb46e7", + "ab0051ac52636a", + 1, + -184733716, + "961ff413850336d3987c550404fc1d923266ca36cc9ffee7113edb3a9fea7f30" + ], + [ + "af1c4ab301ec462f76ee69ba419b1b2557b7ded639f3442a3522d4f9170b2d6859765c3df402000000016affffffff01a5ca6c000000000008ab52536aab00005300000000", + "6a6351", + 0, + 110304602, + "e88ed2eea9143f2517b15c03db00767eb01a5ce12193b99b964a35700607e5f4" + ], + [ + "0bfd34210451c92cdfa02125a62ba365448e11ff1db3fb8bc84f1c7e5615da40233a8cd368010000000252ac9a070cd88dec5cf9aed1eab10d19529720e12c52d3a21b92c6fdb589d056908e43ea910e0200000009ac516a52656a6a5165ffffffffc3edcca8d2f61f34a5296c405c5f6bc58276416c720c956ff277f1fb81541ddd00000000030063abffffffff811247905cdfc973d179c03014c01e37d44e78f087233444dfdce1d1389d97c302000000065163000063ab1724a26e02ca37c902000000000851ab53525352ac529012a90100000000085200525253535353fa32575b", + "5352ac6351", + 1, + -1087700448, + "b8f1e1f35e3e1368bd17008c756e59cced216b3c699bcd7bebdb5b6c8eec4697" + ], + [ + "2c84c0640487a4a695751d3e4be48019dbaea85a6e854f796881697383ea455347d2b2769001000000055265526500ffffffff6aac176d8aa00778d496a7231eeb7d3334f20c512d3db1683276402100d98de5030000000700536a5263526ac1ee9ceb171c0c984ebaf12c234fd1487fbf3b3d73aa0756907f26837efba78d1bed33200300000001ab4d9e8ec0bed837cb929bbed76ee848959cec59de44bd7667b7631a744f880d5c71a20cfd0100000007005363515300abffffffff023753fb0000000000036565532d3873050000000009005152ab6a63acab5200000000", + "ab650053ab", + 0, + -877941183, + "c49af297dffe2d80deddf10ceea84b99f8554bd2d55bbdc34e449728c31f0835" + ], + [ + "1f7e4b1b045d3efa6cd7a11d7873a8bab886c19bd11fcb6712f0948f2db3a7be76ff76c8f100000000095265ab6a0065ac5363ffffffffdaafcfa6029336c997680a541725190f09a6f6da21e54560eca4b5b8ae987da1000000000952ac52acac52515165ffffffff825a38d3b1e5bb4d10f33653ab3ab6882c7abdaec74460257d1528ce7be3f98e0100000007526a006a656a63c14adc8f04953a5d3d3f89237f38b857dd357713896d36215f7e8b77b11d98ea3cdc93df02000000015212484f6104bfafae0300000000025263a2b0120000000000056563ab00516c4d2605000000000653ac6500655301cc93030000000002acab14643b1f", + "63acac53ab", + 0, + 333824258, + "18da6ceb011cd36f15ad7dd6c55ef07e6f6ed48881ce3bb31416d3c290d9a0e9" + ], + [ + "467a3e7602e6d1a7a531106791845ec3908a29b833598e41f610ef83d02a7da3a1900bf2960000000005ab6a636353ffffffff031db6dac6f0bafafe723b9199420217ad2c94221b6880654f2b35114f44b1df010000000965ab52636a63ac6352ffffffff02b3b95c0100000000026300703216030000000001ab3261c0aa", + "6a", + 0, + 2110869267, + "3078b1d1a7713c6d101c64afe35adfae0977a5ab4c7e07a0b170b041258adbf2" + ], + [ + "8713bc4f01b411149d575ebae575f5dd7e456198d61d238695df459dd9b86c4e3b2734b62e0300000004abac6363ffffffff03b58049050000000002ac653c714c04000000000953656a005151526a527b5a9e03000000000652ac5100525300000000", + "52", + 0, + -647281251, + "0e0bed1bf2ff255aef6e5c587f879ae0be6222ab33bd75ee365ec6fbb8acbe38" + ], + [ + "f2ba8a8701b9c401efe3dd0695d655e20532b90ac0142768cee4a3bb0a89646758f544aa8102000000036a52527899f4e4040c6f0b030000000008636565ab530051ab52b60c000000000009515200ab630053ac53a49c5f040000000008ab53ab516300ab63fa27340300000000015100000000", + "ac63abab5251", + 0, + -1328936437, + "ab61497afd39e61fe06bc5677326919716f9b20083c9f3417dcea905090e0411" + ], + [ + "b5a7df6102107beded33ae7f1dec0531d4829dff7477260925aa2cba54119b7a07d92d5a1d02000000046a516a52803b625c334c1d2107a326538a3db92c6c6ae3f7c3516cd90a09b619ec6f58d10e77bd6703000000056563006a63ffffffff0117484b03000000000853acab52526a65abc1b548a1", + "ac006a525100", + 0, + 2074359913, + "680336db57347d8183b8898cd27a83f1ba5884155aeae5ce20b4840b75e12871" + ], + [ + "278cb16204b9dadf400266106392c4aa9df01ba03af988c8139dae4c1818ac009f13fc5f1a00000000065200ac656a52ffffffffd006bbebd8cbd7bdead24cddc9badfcc6bc0c2e63c037e5c29aa858f5d0f3e7d01000000046a0051acffffffffbc62a5f57e58da0b67956003ae81ac97cb4cbd1d694c914fc41515c008c4d8fd020000000165e329c844bcc16164be64b64a81cbf4ffd41ed2934e0daa0040ccb8365bab0b2a9e401c180300000003ab52abffffffff02588460030000000000a25a12030000000005535100005300000000", + "6553ab6a5300acab51", + 3, + 989407546, + "1c29f110576f4a3b257f67454d99dfc0dee62ef5517ca702848ce4bd2ea1a1d7" + ], + [ + "49eb2178020a04fca08612c34959fd41447319c190fb7ffed9f71c235aa77bec28703aa1820200000003ac6353abaff326071f07ec6b77fb651af06e8e8bd171068ec96b52ed584de1d71437fed186aecf0300000001acffffffff03da3dbe02000000000652ac63ac6aab8f3b680400000000096a536a65636a53516a5175470100000000016500000000", + "6a536365", + 0, + 1283691249, + "c670219a93234929f662ecb9aa148a85a2d281e83f4e53d10509461cdea47979" + ], + [ + "0f96cea9019b4b3233c0485d5b1bad770c246fe8d4a58fb24c3b7dfdb3b0fd90ea4e8e947f0300000006006a5163515303571e1e01906956030000000005ab635353abadc0fbbe", + "acac", + 0, + -1491469027, + "716a8180e417228f769dcb49e0491e3fda63badf3d5ea0ceeac7970d483dd7e2" + ], + [ + "9a7d858604577171f5fe3f3fd3e5e039c4b0a06717a5381e9977d80e9f53e025e0f16d2877020000000752636565536353ffffffff5862bd028e8276e63f044be1dddcbb8d0c3fa097678308abf2b0f45104a93dbd0100000001531200667ba8fdd3b28e98a35da73d3ddfe51e210303d8eb580f923de988ee632d77793892030000000752526363526563ffffffffe9744eb44db2658f120847c77f47786d268c302120d269e6004455aa3ea5f5e20200000009ab6300636aab656551ffffffff03c61a3c020000000009ab516a6aab6aab53ab737f1a05000000000853acabab655365ab92a4a00400000000016367edf6c8", + "535352ab", + 3, + 659348595, + "d36ee79fc80db2e63e05cdc50357d186181b40ae20e3720878284228a13ee8b3" + ], + [ + "148e68480196eb52529af8e83e14127cbfdbd4a174e60a86ac2d86eac9665f46f4447cf7aa01000000045200ac538f8f871401cf240c0300000000065252ab52656a5266cf61", + "", + 0, + -344314825, + "eacc47c5a53734d6ae3aedbc6a7c0a75a1565310851b29ef0342dc4745ceb607" + ], + [ + "e2bc29d4013660631ba14ecf75c60ec5e9bed7237524d8c10f66d0675daa66d1492cb834530200000004ac510065e42d0c9e04f2b26c01000000000951525152acac65ababa35b7504000000000953ac6aac00650053ab94688c0400000000056365526553a1bced0300000000016a00000000", + "65ab0063655353", + 0, + -888431789, + "59a34b3ed3a1cce0b104de8f7d733f2d386ffc7445efae67680cd90bc915f7e0" + ], + [ + "0c8a70d70494dca6ab05b2bc941b5b431c43a292bd8f2f02eab5e240a408ca73a676044a4103000000056a51ab006affffffff84496004e54836c035821f14439149f22e1db834f315b24588ba2f031511926c0100000000ffffffffbbc5e70ed1c3060ba1bfe99c1656a3158a7307c3ce8eb362ec32c668596d2bd30000000009636563635351abab00b039344c6fc4f9bec24322e45407af271b2d3dfec5f259ee2fc7227bc5285e22b3be85b40100000009ac00ab53abac6a5352e5ddfcff02d50231020000000005006a51536ab086d9020000000006ababac51ac6a00000000", + "abab636565acac6a", + 3, + 241546088, + "643a7b4c8d832e14d5c10762e74ec84f2c3f7ed96c03053157f1bed226614911" + ], + [ + "f98f79cf0274b745e1d6f36da7cbe205a79132a7ad462bdc434cfb1dcd62a6977c3d2a5dbc010000000553516a5365ffffffff4f89f485b53cdad7fb80cc1b7e314b9735b9383bc92c1248bb0e5c6173a55c0d010000000353655293f9b014045ad96d02000000000963ac526a53ac636365f4c27904000000000952536563635152526a2788f0030000000002516aff5add01000000000863530051655351abd04716ba", + "ab6552536a53", + 1, + -2128899945, + "56d29f5e300ddfed2cd8dcce5d79826e193981d0b70dc7487772c8a0b3b8d7b1" + ], + [ + "6c7913f902aa3f5f939dd1615114ce961beda7c1e0dd195be36a2f0d9d047c28ac62738c3a020000000453abac00ffffffff477bf2c5b5c6733881447ac1ecaff3a6f80d7016eee3513f382ad7f554015b970100000007ab6563acab5152ffffffff04e58fe1040000000009ab00526aabab526553e59790010000000002ab525a834b03000000000035fdaf0200000000086551ac65515200ab00000000", + "63ac53", + 1, + 1285478169, + "1536da582a0b6de017862445e91ba14181bd6bf953f4de2f46b040d351a747c9" + ], + [ + "4624aa9204584f06a8a325c84e3b108cafb97a387af62dc9eab9afd85ae5e2c71e593a3b690200000003636a005eb2b44eabbaeca6257c442fea00107c80e32e8715a1293cc164a42e62ce14fea146220c020000000090b9ee38106e3310037bfc519fd209bdbd21c588522a0e96df5fba4e979392bc993bfe9f01000000086363636a635353ab6f1907d218ef6f3c729d9200e23c1dbff2df58b8b1282c6717b26cf760ee4c880d23f4d100000000086a516a536a525163ffffffff01d6f162050000000000ebbab208", + "525365ab0053", + 1, + -1515409325, + "6cf9cd409b7185b1f118171f0a34217af5b612ea54195ea186505b667c19337f" + ], + [ + "16562fc503f1cf9113987040c408bfd4523f1512da699a2ca6ba122dc65677a4c9bf7763830000000003636552ffffffff1ec1fab5ff099d1c8e6b068156f4e39b5543286bab53c6d61e2582d1e07c96cf02000000045163656affffffffd0ef40003524d54c08cb4d13a5ee61c84fbb28cde9eca7a6d11ba3a9335d8c620100000007635153536a6300fbb84fc2012003a601000000000363ab6a00000000", + "63636a006a6aab", + 0, + -1310262675, + "1efbf3d37a92bc03d9eb950b792f307e95504f7c4998f668aa250707ebb752ac" + ], + [ + "531665d701f86bacbdb881c317ef60d9cd1baeffb2475e57d3b282cd9225e2a3bf9cbe0ded01000000086300ac515263acabffffffff0453a8500100000000086353acab516a6565e5e9200500000000026a52a44caa00000000000453ac000065e41b0500000000076500ac0065526ab4476f4d", + "006563006aab00636a", + 0, + 1770013777, + "0898b26dd3ca08632a5131fa48eb55b44386d0c5070c24d6e329673d5e3693b8" + ], + [ + "0f1227a20140655a3da36e413b9b5d108a866f6f147eb4940f032f5a89854eae6d7c3a91600100000009525363515153515253e37a79480161ab61020000000001ab00000000", + "ab65005200", + 0, + -1996383599, + "979782dc3f36d908d37d7e4046a38d306b4b08ddc60a5eba355fe3d6da1b29a9" + ], + [ + "063ff6eb01aff98d0d2a6db224475010edb634c2f3b46257084676adeb84165a4ff8558d7601000000066353006a5165deb3262c042d109c0000000000076363ab52ac005200b9c4050000000007516300ac510063cfffc800000000000200639e815501000000000700526a52ac6365ac7b07b8", + "656552abac6500", + 0, + -1559847112, + "674a4bcb04247f8dc98780f1792cac86b8aee41a800fc1e6f5032f6e1dccde65" + ], + [ + "3320f6730132f830c4681d0cae542188e4177cad5d526fae84565c60ceb5c0118e844f90bd030000000163ffffffff0257ec5a040000000005525251ac6538344d000000000002515200000000", + "5352656a53ac516a65", + 0, + 788050308, + "3afacaca0ef6be9d39e71d7b1b118994f99e4ea5973c9107ca687d28d8eba485" + ], + [ + "c13aa4b702eedd7cde09d0416e649a890d40e675aa9b5b6d6912686e20e9b9e10dbd40abb1000000000863ab6353515351ac11d24dc4cc22ded7cdbc13edd3f87bd4b226eda3e4408853a57bcd1becf2df2a1671fd1600000000045165516affffffff01baea300100000000076aab52ab53005300000000", + "0065", + 0, + -1195908377, + "241a23e7b1982d5f78917ed97a8678087acbbffe7f624b81df78a5fe5e41e754" + ], + [ + "d9a6f20e019dd1b5fae897fb472843903f9c3c2293a0ffb59cff2b413bae6eceab574aaf9d030000000663ab006a515102f54939032df5100100000000056a51ab65530ec28f010000000004ac5100007e874905000000000651005265ac6a00000000", + "abacab63acacabab", + 0, + 271463254, + "1326a46f4c21e7619f30a992719a905aa1632aaf481a57e1cbd7d7c22139b41e" + ], + [ + "157c81bf0490432b3fcb3f9a5b79e5f91f67f05efb89fa1c8740a3fe7e9bdc18d7cb6acd2203000000026351ffffffff912e48e72bbcf8a540b693cf8b028e532a950e6e63a28801f6eaad1afcc52ad00000000000b1a4b170a2b9e60e0cad88a0085137309f6807d25d5afb5c1e1d32aa10ba1cdf7df596dd0000000009525165656a51ab65ab3674fba32a76fe09b273618d5f14124465933f4190ba4e0fd09d838daafc6223b31642ac00000000086a53536551ac6565ffffffff01fe9fb6030000000008ab51656a5165636a00000000", + "ab00ab6a6551", + 3, + -64357617, + "1ddaab7f973551d71f16bd70c4c4edbf7225e64e784a6da0ee7f7a9fe4f12a0b" + ], + [ + "a2692fff03b2387f5bacd5640c86ba7df574a0ee9ed7f66f22c73cccaef3907eae791cbd230200000004536363abffffffff4d9fe7e5b375de88ba48925d9b2005447a69ea2e00495a96eafb2f144ad475b40000000008000053000052636537259bee3cedd3dcc07c8f423739690c590dc195274a7d398fa196af37f3e9b4a1413f810000000006ac63acac52abffffffff04c65fe60200000000075151536365ab657236fc020000000009005263ab00656a6a5195b8b6030000000007ac5165636aac6a7d7b66010000000002acab00000000", + "51", + 2, + -826546582, + "925037c7dc7625f3f12dc83904755a37016560de8e1cdd153c88270a7201cf15" + ], + [ + "2c5b003201b88654ac2d02ff6762446cb5a4af77586f05e65ee5d54680cea13291efcf930d0100000005ab536a006a37423d2504100367000000000004536a515335149800000000000152166aeb03000000000452510063226c8e03000000000000000000", + "635251", + 0, + 1060344799, + "7e058ca5dd07640e4aae7dea731cfb7d7fef1bfd0d6d7b6ce109d041f4ca2a31" + ], + [ + "f981b9e104acb93b9a7e2375080f3ea0e7a94ce54cd8fb25c57992fa8042bdf4378572859f0100000002630008604febba7e4837da77084d5d1b81965e0ea0deb6d61278b6be8627b0d9a2ecd7aeb06a0300000005ac5353536a42af3ef15ce7a2cd60482fc0d191c4236e66b4b48c9018d7dbe4db820f5925aad0e8b52a0300000008ab0063510052516301863715efc8608bf69c0343f18fb81a8b0c720898a3563eca8fe630736c0440a179129d03000000086aac6a52ac6a63ac44fec4c00408320a03000000000062c21c030000000007ac6a655263006553835f0100000000015303cd60000000000005535263536558b596e0", + "00", + 0, + -2140385880, + "49870a961263354c9baf108c6979b28261f99b374e97605baa532d9fa3848797" + ], + [ + "e7416df901269b7af14a13d9d0507709b3cd751f586ce9d5da8d16a121e1bd481f5a086e1103000000056aab005200ffffffff01aa269c040000000006acac6a6a5263ee718de6", + "ab525363", + 0, + 1309186551, + "eea7d2212bda2d408fff146f9ae5e85e6b640a93b9362622bb9d5e6e36798389" + ], + [ + "402a815902193073625ab13d876190d1bbb72aecb0ea733c3330f2a4c2fe6146f322d8843a0300000008656aab0000535363fffffffff9dccdec5d8509d9297d26dfcb1e789cf02236c77dc4b90ebccbf94d1b5821150300000001510bf1f96a03c5c145000000000002ac6ae11b1c0100000000055163516a5239c8a600000000000365636300000000", + "63536aacab", + 0, + -1811424955, + "0090803a20102a778ab967a74532faee13e03b702083b090b1497bc2267ee2fe" + ], + [ + "c4b702e502f1a54f235224f0e6de961d2e53b506ab45b9a40805d1dacd35148f0acf24ca5e00000000085200ac65ac53acabf34ba6099135658460de9d9b433b84a8562032723635baf21ca1db561dce1c13a06f4407000000000851ac006a63516aabffffffff02a853a603000000000163d17a67030000000005ab63006a5200000000", + "ac5363515153", + 1, + 480734903, + "5c46f7ac3d6460af0da28468fcc5b3c87f2b9093d0f837954b7c8174b4d7b6e7" + ], + [ + "9b83f78704f492b9b353a3faad8d93f688e885030c274856e4037818848b99e490afef27770200000000ffffffff36b60675a5888c0ef4d9e11744ecd90d9fe9e6d8abb4cff5666c898fdce98d9e00000000056aab656352596370fca7a7c139752971e169a1af3e67d7656fc4fc7fd3b98408e607c2f2c836c9f27c030000000653ac51ab6300a0761de7e158947f401b3595b7dc0fe7b75fa9c833d13f1af57b9206e4012de0c41b8124030000000953656a53ab53510052242e5f5601bf83b301000000000465516a6300000000", + "63515200ac656365", + 3, + -150879312, + "9cf05990421ea853782e4a2c67118e03434629e7d52ab3f1d55c37cf7d72cdc4" + ], + [ + "f492a9da04f80b679708c01224f68203d5ea2668b1f442ebba16b1aa4301d2fe5b4e2568f3010000000953005351525263ab65ffffffff93b34c3f37d4a66df255b514419105b56d7d60c24bf395415eda3d3d8aa5cd0101000000020065ffffffff9dba34dabdc4f1643b372b6b77fdf2b482b33ed425914bb4b1a61e4fad33cf390000000002ab52ffffffffbbf3dc82f397ef3ee902c5146c8a80d9a1344fa6e38b7abce0f157be7adaefae0000000009515351005365006a51ffffffff021359ba010000000000403fea0200000000095200ac6353abac635300000000", + "00ac51acacac", + 0, + -2115078404, + "fd44fc98639ca32c927929196fc3f3594578f4c4bd248156a25c04a65bf3a9f3" + ], + [ + "2f73e0b304f154d3a00fde2fdd40e791295e28d6cb76af9c0fd8547acf3771a02e3a92ba37030000000852ac6351ab6565639aa95467b065cec61b6e7dc4d6192b5536a7c569315fb43f470078b31ed22a55dab8265f02000000080065636a6aab6a53ffffffff9e3addbff52b2aaf9fe49c67017395198a9b71f0aa668c5cb354d06c295a691a0100000000ffffffff45c2b4019abaf05c5e484df982a4a07459204d1343a6ee5badade358141f8f990300000007ac516a6aacac6308655cd601f3bc2f0000000000015200000000", + "", + 0, + -2082053939, + "9a95e692e1f78efd3e46bb98f178a1e3a0ef60bd0301d9f064c0e5703dc879c2" + ], + [ + "5a60b9b503553f3c099f775db56af3456330f1e44e67355c4ab290d22764b9144a7b5f959003000000030052acbd63e0564decc8659aa53868be48c1bfcda0a8c9857b0db32a217bc8b46d9e7323fe9649020000000553ac6551abd0ecf806211db989bead96c09c7f3ec5f73c1411d3329d47d12f9e46678f09bac0dc383e0200000000ffffffff01494bb202000000000500516551ac00000000", + "ac", + 0, + 1169947809, + "62a36c6e8da037202fa8aeae03e533665376d5a4e0a854fc4624a75ec52e4eb1" + ], + [ + "7e98d353045569c52347ca0ff2fdba608829e744f61eb779ffdb5830aae0e6d6857ab2690e03000000075365acab656352ffffffffa890dd37818776d12da8dca53d02d243ef23b4535c67016f4c58103eed85360f030000000093dbacdc25ca65d2951e047d6102c4a7da5e37f3d5e3c8b87c29b489360725dcd117ee2003000000056a6300ac53c7e99fa1dc2b8b51733034e6555f6d6de47dbbf1026effac7db80cb2080678687380dc1e02000000075352005263516affffffff04423272040000000008ab6353ab65510051e0f53b0500000000086300516552635152f74a5f04000000000853acab0053ab52ab0e8e5f00000000000951ac5363516a6aabab00000000", + "6a5163ab52", + 3, + 890006103, + "476868cecd1763c91dade98f17defa42d31049547df45acffa1cc5ae5c3d75d6" + ], + [ + "e3649aa40405e6ffe377dbb1bbbb672a40d8424c430fa6512c6165273a2b9b6afa9949ec430200000007630052ab655153a365f62f2792fa90c784efe3f0981134d72aac0b1e1578097132c7f0406671457c332b84020000000353ab6ad780f40cf51be22bb4ff755434779c7f1def4999e4f289d2bd23d142f36b66fbe5cfbb4b01000000076a5252abac52ab1430ffdc67127c9c0fc97dcd4b578dab64f4fb9550d2b59d599773962077a563e8b6732c02000000016affffffff04cb2687000000000002ab636e320904000000000252acf70e9401000000000100dc3393050000000006ab0063536aacbc231765", + "65520053", + 3, + -2016196547, + "f64f805f0ff7f237359fa6b0e58085f3c766d1859003332223444fd29144112a" + ], + [ + "1d033569040700441686672832b531ab55db89b50dc1f9fc00fb72218b652da9dcfbc83be901000000066551ac526a632b390f9ad068e5fdee6563e88e2a8e4e09763c861072713dc069893dc6bbc9db3f00e26502000000096a5363526565525252ffffffff8a36bdd0aaf38f6707592d203e14476ca9f259021e487135c7e8324244057ed90300000000ed3fb2a3dfd4d46b5f3603fe0148653911988457bd0ed7f742b07c452f5476c228ff9f600200000007526aac00525152ffffffff04b88e48030000000000c753d602000000000853510000006553518fda2603000000000853ac52acac5263534839f1030000000006ac006aacac5300000000", + "516553635300ab0052", + 1, + 2075958316, + "c2cefaec2293134acbcf6d2a8bf2b3eb42e4ec04ee8f8bf30ff23e65680677c1" + ], + [ + "4c4be7540344050e3044f0f1d628039a334a7c1f7b4573469cfea46101d6888bb6161fe9710200000000ffffffffac85a4fdad641d8e28523f78cf5b0f4dc74e6c5d903c10b358dd13a5a1fd8a06000000000163e0ae75d05616b72467b691dc207fe2e65ea35e2eadb7e06ea442b2adb9715f212c0924f10200000000ffffffff0194ddfe02000000000265ac00000000", + "00006500", + 1, + -479922562, + "d66924d49f03a6960d3ca479f3415d638c45889ce9ab05e25b65ac260b51d634" + ], + [ + "202c18eb012bc0a987e69e205aea63f0f0c089f96dd8f0e9fcde199f2f37892b1d4e6da90302000000055352ac6565ffffffff0257e5450100000000025300ad257203000000000000000000", + "520052ac6a005265", + 0, + 168054797, + "502967a6f999f7ee25610a443caf8653dda288e6d644a77537bcc115a8a29894" + ], + [ + "32fa0b0804e6ea101e137665a041cc2350b794e59bf42d9b09088b01cde806ec1bbea077df0200000008515153650000006506a11c55904258fa418e57b88b12724b81153260d3f4c9f080439789a391ab147aabb0fa0000000007000052ac51ab510986f2a15c0d5e05d20dc876dd2dafa435276d53da7b47c393f20900e55f163b97ce0b800000000008ab526a520065636a8087df7d4d9c985fb42308fb09dce704650719140aa6050e8955fa5d2ea46b464a333f870000000009636300636a6565006affffffff01994a0d040000000002536500000000", + "516563530065", + 2, + -163068286, + "f58637277d2bc42e18358dc55f7e87e7043f5e33f4ce1fc974e715ef0d3d1c2a" + ], + [ + "ae23424d040cd884ebfb9a815d8f17176980ab8015285e03fdde899449f4ae71e04275e9a80100000007ab006553530053ffffffff018e06db6af519dadc5280c07791c0fd33251500955e43fe4ac747a4df5c54df020000000251ac330e977c0fec6149a1768e0d312fdb53ed9953a3737d7b5d06aad4d86e9970346a4feeb5030000000951ab51ac6563ab526a67cabc431ee3d8111224d5ecdbb7d717aa8fe82ce4a63842c9bd1aa848f111910e5ae1eb0100000004ac515300bfb7e0d7048acddc030000000009636a5253636a655363a3428e040000000001525b99c6050000000004655265ab717e6e020000000000d99011eb", + "ac6a6a516565", + 1, + -716251549, + "b098eb9aff1bbd375c70a0cbb9497882ab51f3abfebbf4e1f8d74c0739dc7717" + ], + [ + "030f44fc01b4a9267335a95677bd190c1c12655e64df74addc53b753641259af1a54146baa020000000152e004b56c04ba11780300000000026a53f125f001000000000251acd2cc7c03000000000763536563655363c9b9e50500000000015200000000", + "ac", + 0, + -1351818298, + "19dd32190ed2a37be22f0224a9b55b91e37290577c6c346d36d32774db0219a3" + ], + [ + "c05f448f02817740b30652c5681a3b128322f9dc97d166bd4402d39c37c0b14506d8adb5890300000003536353ffffffffa188b430357055ba291c648f951cd2f9b28a2e76353bef391b71a889ba68d5fc02000000056565526a6affffffff02745f73010000000001ab3ec34c0400000000036aac5200000000", + "516551510053", + 0, + -267877178, + "3a1c6742d4c374f061b1ebe330b1e169a113a19792a1fdde979b53e094cc4a3c" + ], + [ + "163ba45703dd8c2c5a1c1f8b806afdc710a2a8fc40c0138e2d83e329e0e02a9b6c837ff6b8000000000700655151ab6a522b48b8f134eb1a7e6f5a6fa319ce9d11b36327ba427b7d65ead3b4a6a69f85cda8bbcd22030000000563656552acffffffffdbcf4955232bd11eef0cc6954f3f6279675b2956b9bcc24f08c360894027a60201000000066500006500abffffffff04d0ce9d0200000000008380650000000000015233f360040000000003006aabedcf0801000000000000000000", + "000065006500ac", + 0, + 216965323, + "9afe3f4978df6a86e9a8ebd62ef6a9d48a2203f02629349f1864ef2b8b92fd55" + ], + [ + "07f7f5530453a12ad0c7eb8fbc3f140c7ab6818144d67d2d8752600ca5d9a9358e2dff87d4000000000663526aab526a9e599c379d455e2da36d0cde88d931a863a3e97e01e93b9edb65856f3d958dc08b92b720000000000165bbc8d66dae3b1b170a6e2457f5b161465cb8706e0e6ffc6af55deb918365f14c5f40d4890100000000a7bd77c069ee4b48638e2363fcf2a86b02bea022047bd9fcb16d2b94ad068308d19b31cb00000000066aab5300ab529672aa8f01dbd8a205000000000663536353006a02e99901", + "ac006351006a63ab63", + 1, + 119789359, + "6629a1e75c6ae8f4f9d5f734246b6a71682a5ea57246040ef0584f6b97916175" + ], + [ + "fe647f950311bf8f3a4d90afd7517df306e04a344d2b2a2fea368935faf11fa6882505890d0000000005ab5100516affffffff43c140947d9778718919c49c0535667fc6cc727f5876851cb8f7b6460710c7f60100000000ffffffffce4aa5d90d7ab93cbec2e9626a435afcf2a68dd693c15b0e1ece81a9fcbe025e0300000000ffffffff02f34806020000000002515262e54403000000000965635151ac655363636de5ce24", + "6a005100ac516351", + 2, + 989643518, + "818a7ceaf963f52b5c48a7f01681ac6653c26b63a9f491856f090d9d60f2ffe3" + ], + [ + "a1050f8604d0f9d2feefcdb5051ae0052f38e21bf39daf583fd0c3900faa3eab5d431c0bbe030000000653536a005151683d27e5c6e0da8f22125823f32d5d98477d8098ef36263b9694d61d4d85d3f2ac02b7570200000007000052005165abffffffff0cad981542bcb54a87d9400aa63e514c7c6fab7158c2b1fb37821ea755eb162a0200000000b94feb5100e5ef3bf8ed8d43356c8a8d5ac6c7e80d7ff6040f4f0aa19abbe783f4f461240200000007636500000052655686fd70042be3ad02000000000465ab636a15680b000000000004acac53511277c705000000000452635252d27a0102000000000000000000", + "6a6aacab65655251", + 1, + -982144648, + "dfcf484111801989eb6df8dc2bafb944d7365ffeb36a575a08f3270d3ef24c9f" + ], + [ + "cef7316804c3e77fe67fc6207a1ea6ae6eb06b3bf1b3a4010a45ae5c7ad677bb8a4ebd16d90200000009ac536a5152ac5263005301ab8a0da2b3e0654d31a30264f9356ba1851c820a403be2948d35cafc7f9fe67a06960300000006526a63636a53ffffffffbada0d85465199fa4232c6e4222df790470c5b7afd54704595a48eedd7a4916b030000000865ab63ac006a006ab28dba4ad55e58b5375053f78b8cdf4879f723ea4068aed3dd4138766cb4d80aab0aff3d0300000003ac6a00ffffffff010f5dd6010000000006ab006aab51ab00000000", + "", + 1, + 889284257, + "d0f32a6db43378af84b063a6706d614e2d647031cf066997c48c04de3b493a94" + ], + [ + "7b3ff28004ba3c7590ed6e36f45453ebb3f16636fe716acb2418bb2963df596a50ed954d2e03000000065251515265abffffffff706ee16e32e22179400c9841013971645dabf63a3a6d2d5feb42f83aa468983e030000000653ac51ac5152ffffffffa03a16e5e5de65dfa848b9a64ee8bf8656cc1f96b06a15d35bd5f3d32629876e020000000043c1a3965448b3b46f0f0689f1368f3b2981208a368ec5c30defb35595ef9cf95ffd10e902000000036aac65253a5bbe042e907204000000000800006565656352634203b4020000000002656336b3b7010000000001ab7a063f0100000000026500a233cb76", + "006551636a53ac5251", + 1, + -1144216171, + "68c7bd717b399b1ee33a6562a916825a2fed3019cdf4920418bb72ffd7403c8c" + ], + [ + "d5c1b16f0248c60a3ddccf7ebd1b3f260360bbdf2230577d1c236891a1993725e262e1b6cb000000000363636affffffff0a32362cfe68d25b243a015fc9aa172ea9c6b087c9e231474bb01824fd6bd8bc0300000005ab52ab516affffffff0420d9a70200000000045152656a45765d0000000000055252536a5277bad100000000000252ab3f3f3803000000000463acac5200000000", + "52636a52ab65", + 1, + 1305123906, + "978dc178ecd03d403b048213d904653979d11c51730381c96c4208e3ea24243a" + ], + [ + "1be8ee5604a9937ebecffc832155d9ba7860d0ca451eaced58ca3688945a31d93420c27c460100000006abac5300535288b65458af2f17cbbf7c5fbcdcfb334ffd84c1510d5500dc7d25a43c36679b702e850f7c0200000003005300ffffffff7c237281cb859653eb5bb0a66dbb7aeb2ac11d99ba9ed0f12c766a8ae2a2157203000000086aabac526365acabfffffffff09d3d6639849f442a6a52ad10a5d0e4cb1f4a6b22a98a8f442f60280c9e5be80200000007ab00ab6565ab52ffffffff0398fe83030000000005526aababacbdd6ec010000000005535252ab6a82c1e6040000000001652b71c40c", + "6563526353656351", + 2, + -853634888, + "0d936cceda2f56c7bb87d90a7b508f6208577014ff280910a710580357df25f3" + ], + [ + "9e0f99c504fbca858c209c6d9371ddd78985be1ab52845db0720af9ae5e2664d352f5037d4010000000552ac53636affffffff0e0ce866bc3f5b0a49748f597c18fa47a2483b8a94cef1d7295d9a5d36d31ae7030000000663515263ac635bb5d1698325164cdd3f7f3f7831635a3588f26d47cc30bf0fefd56cd87dc4e84f162ab702000000036a6365ffffffff85c2b1a61de4bcbd1d5332d5f59f338dd5e8accbc466fd860f96eef1f54c28ec030000000165ffffffff04f5cabd010000000007000052ac526563c18f1502000000000465510051dc9157050000000008655363ac525253ac506bb600000000000865656a53ab63006a00000000", + "006a6a0052", + 0, + 1186324483, + "2f9b7348600336512686e7271c53015d1cb096ab1a5e0bce49acd35bceb42bc8" + ], + [ + "11ce51f90164b4b54b9278f0337d95c50d16f6828fcb641df9c7a041a2b274aa70b1250f2b0000000008ab6a6a65006551524c9fe7f604af44be050000000005525365006521f79a0300000000015306bb4e04000000000265ac99611a05000000000765acab656500006dc866d0", + "", + 0, + -1710478768, + "cfa4b7573559b3b199478880c8013fa713ca81ca8754a3fd68a6d7ee6147dc5a" + ], + [ + "86bc233e02ba3c647e356558e7252481a7769491fb46e883dd547a4ce9898fc9a1ca1b77790000000006ab5351abab51f0c1d09c37696d5c7c257788f5dff5583f4700687bcb7d4acfb48521dc953659e325fa390300000003acac5280f29523027225af03000000000963abac0065ab65acab7e59d90400000000016549dac846", + "53006aac52acac", + 0, + 711159875, + "880330ccde00991503ea598a6dfd81135c6cda9d317820352781417f89134d85" + ], + [ + "beac155d03a853bf18cd5c490bb2a245b3b2a501a3ce5967945b0bf388fec2ba9f04c03d68030000000012fe96283aec4d3aafed8f888b0f1534bd903f9cd1af86a7e64006a2fa0d2d30711af770010000000163ffffffffd963a19d19a292104b9021c535d3e302925543fb3b5ed39fb2124ee23a9db00302000000056500ac63acffffffff01ad67f503000000000300ac5189f78db2", + "53536a636500", + 2, + 748992863, + "bde3dd0575164d7ece3b5783ce0783ffddb7df98f178fe6468683230314f285a" + ], + [ + "81dab34a039c9e225ba8ef421ec8e0e9d46b5172e892058a9ade579fe0eb239f7d9c97d45b0300000009ac65655351ab526363ffffffff10c0faaf7f597fc8b00bbc67c3fd4c6b70ca6b22718d15946bf6b032e62dae570000000005536a00ab6a02cddec3acf985bbe62c96fccf17012a87026ed63fc6756fa39e286eb4c2dd79b59d37400300000002516affffffff04f18b8d03000000000753abab5152636564411c02000000000400ab6300e965750300000000001bd2cf02000000000565ab526aab00000000", + "006551ab", + 0, + -1488174485, + "a3d65a8cd0c1eea8558d01396b929520a2221c29d9f25f29035b8abae874447f" + ], + [ + "489ebbf10478e260ba88c0168bd7509a651b36aaee983e400c7063da39c93bf28100011f280100000004abab63ab2fc856f05f59b257a4445253e0d91b6dffe32302d520ac8e7f6f2467f7f6b4b65f2f59e903000000096353abacab6351656affffffff0122d9480db6c45a2c6fd68b7bc57246edffbf6330c39ccd36aa3aa45ec108fc030000000265ab9a7e78a69aadd6b030b12602dff0739bbc346b466c7c0129b34f50ae1f61e634e11e9f3d0000000006516a53525100ffffffff011271070000000000086563ab6353536352c4dd0e2c", + "", + 0, + -293358504, + "4eba3055bc2b58765593ec6e11775cea4b6493d8f785e28d01e2d5470ea71575" + ], + [ + "6911195d04f449e8eade3bc49fd09b6fb4b7b7ec86529918b8593a9f6c34c2f2d301ec378b000000000263ab49162266af054643505b572c24ff6f8e4c920e601b23b3c42095881857d00caf56b28acd030000000565525200ac3ac4d24cb59ee8cfec0950312dcdcc14d1b360ab343e834004a5628d629642422f3c5acc02000000035100accf99b663e3c74787aba1272129a34130668a877cc6516bfb7574af9fa6d07f9b4197303400000000085351ab5152635252ffffffff042b3c95000000000000ff92330200000000046a5252ab884a2402000000000853530065520063000d78be03000000000953abab52ab53ac65aba72cb34b", + "6a", + 2, + -637739405, + "6b80d74eb0e7ee59d14f06f30ba7d72a48d3a8ff2d68d3b99e770dec23e9284f" + ], + [ + "746347cf03faa548f4c0b9d2bd96504d2e780292730f690bf0475b188493fb67ca58dcca4f0000000002005336e3521bfb94c254058e852a32fc4cf50d99f9cc7215f7c632b251922104f638aa0b9d080100000008656aac5351635251ffffffff4da22a678bb5bb3ad1a29f97f6f7e5b5de11bb80bcf2f7bb96b67b9f1ac44d09030000000365ababffffffff036f02b30000000000076353ab6aac63ac50b72a050000000002acaba8abf804000000000663006a6a6353797eb999", + "acac5100", + 1, + -1484493812, + "164c32a263f357e385bd744619b91c3f9e3ce6c256d6a827d6defcbdff38fa75" + ], + [ + "e17149010239dd33f847bf1f57896db60e955117d8cf013e7553fae6baa9acd3d0f1412ad90200000006516500516500cb7b32a8a67d58dddfb6ceb5897e75ef1c1ff812d8cd73875856487826dec4a4e2d2422a0100000004ac525365196dbb69039229270400000000070000535351636a8b7596020000000006ab51ac52655131e99d040000000003516551ee437f5c", + "ac656a53", + 1, + 1102662601, + "8858bb47a042243f369f27d9ab4a9cd6216adeac1c1ac413ed0890e46f23d3f3" + ], + [ + "144971940223597a2d1dec49c7d4ec557e4f4bd207428618bafa3c96c411752d494249e1fb0100000004526a5151ffffffff340a545b1080d4f7e2225ff1c9831f283a7d4ca4d3d0a29d12e07d86d6826f7f0200000003006553ffffffff03c36965000000000000dfa9af00000000000451636aac7f7d140300000000016300000000", + "", + 1, + -108117779, + "c84fcaf9d779df736a26cc3cabd04d0e61150d4d5472dd5358d6626e610be57f" + ], + [ + "b11b6752044e650b9c4744fb9c930819227d2ac4040d8c91a133080e090b042a142e93906e0000000003650053ffffffff6b9ce7e29550d3c1676b702e5e1537567354b002c8b7bb3d3535e63ad03b50ea01000000055100516300fffffffffcf7b252fea3ad5a108af3640a9bc2cd724a7a3ce22a760fba95496e88e2f2e801000000036a00ac7c58df5efba193d33d9549547f6ca839f93e14fa0e111f780c28c60cc938f785b363941b000000000863ab51516552ac5265e51fcd0308e9830400000000036a00abab72190300000000016a63d0710000000000050051ab6a6300000000", + "53005165ac51ab65", + 0, + 229563932, + "e562579d1a2b10d1c5e45c06513456002a6bec157d7eb42511d30b118103c052" + ], + [ + "2aee6b9a02172a8288e02fac654520c9dd9ab93cf514d73163701f4788b4caeeb9297d2e250300000004ab6363008fb36695528d7482710ea2926412f877a3b20acae31e9d3091406bfa6b62ebf9d9d2a6470100000009535165536a63520065ffffffff03f7b560050000000003acab6a9a8338050000000000206ce90000000000056552516a5100000000", + "5252", + 1, + -1102319963, + "fa4676c374ae3a417124b4c970d1ed3319dc3ac91fb36efca1aa9ed981a8aa1b" + ], + [ + "9554595203ad5d687f34474685425c1919e3d2cd05cf2dac89d5f33cd3963e5bb43f8706480100000000ffffffff9de2539c2fe3000d59afbd376cb46cefa8bd01dbc43938ff6089b63d68acdc2b02000000096553655251536a6500fffffffff9695e4016cd4dfeb5f7dadf00968e6a409ef048f81922cec231efed4ac78f5d010000000763abab6a5365006caaf0070162cc640200000000045163ab5100000000", + "", + 0, + -1105256289, + "e8e10ed162b1a43bfd23bd06b74a6c2f138b8dc1ab094ffb2fa11d5b22869bee" + ], + [ + "04f51f2a0484cba53d63de1cb0efdcb222999cdf2dd9d19b3542a896ca96e23a643dfc45f00200000007acac53510063002b091fd0bfc0cfb386edf7b9e694f1927d7a3cf4e1d2ce937c1e01610313729ef6419ae7030000000165a3372a913c59b8b3da458335dc1714805c0db98992fd0d93f16a7f28c55dc747fe66a5b503000000095351ab65ab52536351ffffffff5650b318b3e236802a4e41ed9bc0a19c32b7aa3f9b2cda1178f84499963a0cde000000000165ffffffff0383954f04000000000553ac536363a8fc90030000000000a2e315000000000005acab00ab5100000000", + "0053", + 2, + -1424653648, + "a5bc0356f56b2b41a2314ec05bee7b91ef57f1074bcd2efc4da442222269d1a3" + ], + [ + "5e4fab42024a27f0544fe11abc781f46596f75086730be9d16ce948b04cc36f86db7ad50fd01000000026a00613330f4916285b5305cc2d3de6f0293946aa6362fc087727e5203e558c676b314ef8dd401000000001af590d202ba496f040000000001009e3c9604000000000351ac51943d64d3", + "51acabab5100ab52", + 1, + -129301207, + "556c3f90aa81f9b4df5b92a23399fe6432cf8fecf7bba66fd8fdb0246440036c" + ], + [ + "a115284704b88b45a5f060af429a3a8eab10b26b7c15ed421258f5320fa22f4882817d6c2b0300000003005300ffffffff4162f4d738e973e5d26991452769b2e1be4b2b5b7e8cbeab79b9cf9df2882c040000000006636aac63ac5194abc8aa22f8ddc8a7ab102a58e39671683d1891799d19bd1308d24ea6d365e571172f1e030000000700515352515153ffffffff4da7ad75ce6d8541acbb0226e9818a1784e9c97c54b7d1ff82f791df1c6578f60000000000ffffffff01b1f265040000000009ab0051ac656a516a5300000000", + "51abab6352535265", + 0, + -1269106800, + "0ef7b6e87c782fa33fe109aab157a2d9cddc4472864f629510a1c92fa1fe7fc1" + ], + [ + "f3f771ae02939752bfe309d6c652c0d271b7cab14107e98032f269d92b2a8c8853ab057da8010000000563ab6a6365670c305c38f458e30a7c0ab45ee9abd9a8dc03bae1860f965ffced879cb2e5d0bb156821020000000153ffffffff025dc619050000000002ac51ec0d250100000000076a5200636a6363333aecd8", + "650053ac515100ab", + 1, + 1812404608, + "a7aa34bf8a5644f03c6dd8801f9b15ba2e07e07256dbf1e02dad59f0d3e17ea9" + ], + [ + "fd3e267203ae7d6d3975e738ca84f12540229bb237dd228d5f688e9d5ba53fce4302b0334d01000000026353ffffffff602a3ab75af7aa951d93093e345ef0037a2863f3f580a9b1a575fffe68e677450300000000239e476d1e8f81e8b6313880d8a49b27c1b00af467f29756e76f675f084a5676539636ab030000000765ab6351acac52d9217747044d773204000000000752ac51526353acc33e45050000000005516500005115d889040000000004ab5163510cbbbd0200000000016500000000", + "65ac526aac6a53ab52", + 2, + -886179388, + "bc46f3f83058ddf5bebd9e1f2c117a673847c4dc5e31cfb24bac91adf30877cf" + ], + [ + "f380ae23033646af5dfc186f6599098015139e961919aea28502ea2d69474413d94a555ea2000000000853635265abacac5314da394b99b07733341ddba9e86022637be3b76492992fb0f58f23c915098979250a96620300000003ab6300ffffffff4bb6d1c0a0d84eac7f770d3ad0fdc5369ae42a21bbe4c06e0b5060d5990776220300000000ffffffff0486fd70020000000007ac6500635252acf3fd72010000000005656a6a6551212de90500000000096365006a63635153000fa33100000000000600535151656300000000", + "ab52", + 2, + -740890152, + "f804fc4d81f039009ed1f2cccb5c91da797543f235ac71b214c20e763a6d86d7" + ], + [ + "5c45d09801bb4d8e7679d857b86b97697472d514f8b76d862460e7421e8617b15a2df217c6010000000863acacab6565006affffffff01156dbc03000000000952ac63516551ac6aac00000000", + "6aabac", + 0, + 1310125891, + "270445ab77258ced2e5e22a6d0d8c36ac7c30fff9beefa4b3e981867b03fa0ad" + ], + [ + "4ecc6bde030ca0f83c0ed3d4b777f94c0c88708c6c933fe1df6874f296d425cac95355c23d0000000006ac6a51536a52f286a0969d6170e20f2a8000193807f5bc556770e9d82341ef8e17b0035eace89c76edd50200000007ac65525100656affffffff5bade6e462fac1927f078d69d3a981f5b4c1e59311a38efcb9a910aa436afaa80000000007ac6a006352ab52ffffffff0331e58902000000000763ac53636352abb8b3ca000000000001637a1d26040000000009535263ac6a5352ab655ae34a39", + "6a65ab", + 2, + 2142728517, + "4a3415eb1677ae4e0c939644a4cfd5dc6299780b55cd0dc735967057b6b1526a" + ], + [ + "a59484b501eb50114be0fc79e72ab9bc9f4a5f7acdf274a56d6b68684eb68cf8b07ec5d1c2000000000765abab00ab00639e09aa940141e3530200000000046500ac6500000000", + "00516565ab", + 0, + -1561622405, + "d60bbadd2cc0674100baa08d0e0493ee4248f0304b3eb778da942041f503a896" + ], + [ + "53dc1a88046531c7b57a35f4d9adf101d068bf8d63fbbedaf4741dba8bc5e92c8725def571030000000453655251fcdf116a226b3ec240739c4c7493800e4edfe67275234e371a227721eac43d3d9ecaf1b50300000003ac0052ffffffff2c9279ffeea4718d167e9499bd067600715c14484e373ef93ae4a31d2f5671ab0000000009516553ac636a6a65001977752eeba95a8f16b88c571a459c2f2a204e23d48cc7090e4f4cc35846ca7fc0a455ce00000000055165ac0063188143f80205972902000000000765ac63ac516353c7b6a50000000000036a510000000000", + "655351536a", + 0, + 103806788, + "b276584d3514e5b4e058167c41dc02915b9d97f6795936a51f40e894ed8508bc" + ], + [ + "53f8959f01ddb36afdcd20167edcbb75a63d18654fdcf10bc0004c761ab450fe236d79cb2702000000065151650063653435003a033a5e34050000000009ac52516a630000516ab86db3030000000002006344ac090500000000046363ab00f3644537", + "5263abab63ac656353", + 0, + -218513553, + "f1f2a489682e42a6fc20025dfc89584d17f150b2d7ae3ddedd2bf43d5e24f37f" + ], + [ + "5a06cb4602dcfc85f49b8d14513f33c48f67146f2ee44959bbca092788e6823b2719f3160b0200000001ab3c013f2518035b9ea635f9a1c74ec1a3fb7496a160f46aae2e09bfc5cd5111a0f20969e003000000015158c89ab7049f20d6010000000008ac6a52abac53515349765e00000000000300ab638292630100000000045351ab0086da09010000000006656a6365525300000000", + "526a63", + 1, + 1502936586, + "bdfaff8a4e775379c5dc26e024968efa805f923de53fa8272dd53ec582afa0c5" + ], + [ + "ca9d84fa0129011e1bf27d7cb71819650b59fb292b053d625c6f02b0339249b498ff7fd4b601000000025352ffffffff032173a0040000000008525253abab5152639473bb030000000009005153526a53535151d085bd0000000000086a5365ab5165655300000000", + "005152ac51", + 0, + 580353445, + "c629d93b02037f40aa110e46d903edb34107f64806aa0c418d435926feef68b8" + ], + [ + "e3cdbfb4014d90ae6a4401e85f7ac717adc2c035858bf6ff48979dd399d155bce1f150daea0300000002ac51a67a0d39017f6c71040000000005535200535200000000", + "", + 0, + -1899950911, + "c1c7df8206e661d593f6455db1d61a364a249407f88e99ecad05346e495b38d7" + ], + [ + "b2b6b9ab0283d9d73eeae3d847f41439cd88279c166aa805e44f8243adeb3b09e584efb1df00000000026300ffffffff7dfe653bd67ca094f8dab51007c6adaced09de2af745e175b9714ca1f5c68d050000000003ac6500aa8e596903fd3f3204000000000553ac6a6a533a2e210500000000075253acabab526392d0ee020000000008520065635200ab5200000000", + "65acacac65005365", + 0, + 28298553, + "39c2aaa2496212b3ab120ab7d7f37c5e852bfe38d20f5226413a2268663eeae8" + ], + [ + "f30c5c3d01a6edb9e10fafaf7e85db14e7fec558b9dca4a80b05d7c3a2944d282c5018f4680200000003005263ffffffff04aac3530300000000026551bc2419010000000009005163acab6a5100658e7085050000000000c5e4ec050000000007656a6a635365ab2d8e8882", + "abac53ab005251ac52", + 0, + -490287546, + "877e347ec7487497769e2581142276d1a8d813b652e4483cf9cc993d16354417" + ], + [ + "4314339e01de40faabcb1b970245a7f19eedbc17c507dac86cf986c2973715035cf95736ae0200000007abababababab65bde67b900151510b04000000000853ac00655200535300000000", + "52", + 0, + 399070095, + "47585dc25469d04ff3a60939d0a03779e3e81a411bf0ca18b91bb925ebd30718" + ], + [ + "2d4cf4e9031b3e175b2ff18cd933151379d9cfac4713d8bd0e63b70bd4a92277aa7af901ab000000000565515353abffffffff557666c7f3be9cdecdad44c3df206eb63a2da4ed1f159d21193882a9f0340081020000000963ab53ab5252ac63abffffffff8a8c897bdb87e93886aad5ded9d82a13101d5476554386373646ca5e23612e450300000009006a526552abab6a635ac03fc00198bb02040000000009525100526a6563636a1d052834", + "ab52ac00acac6a", + 0, + -1469882480, + "09ed6563a454814ab7e3b4c28d56d8751162b77df1825b37ba66c6147750b2a3" + ], + [ + "f063171b03e1830fdc1d685a30a377537363ccafdc68b42bf2e3acb908dac61ee24b37595c020000000765ac5100ab6aacf447bc8e037b89d6cadd62d960cc442d5ced901d188867b5122b42a862929ce45e7b628d010000000253aba009a1ba42b00f1490b0b857052820976c675f335491cda838fb7934d5eea0257684a2a202000000001e83cf2401a7f777030000000008ab6553526a53526a00000000", + "", + 2, + 1984790332, + "c19caada8e71535e29a86fa29cfd9b74a0c7412003fc722a121005e461e01636" + ], + [ + "cf7bdc250249e22cbe23baf6b648328d31773ea0e771b3b76a48b4748d7fbd390e88a004d30000000003ac536a4ab8cce0e097136c90b2037f231b7fde2063017facd40ed4e5896da7ad00e9c71dd70ae600000000096a0063516352525365ffffffff01b71e3e00000000000300536a00000000", + "", + 1, + 546970113, + "6a815ba155270af102322c882f26d22da11c5330a751f520807936b320b9af5d" + ], + [ + "ac7a125a0269d35f5dbdab9948c48674616e7507413cd10e1acebeaf85b369cd8c88301b7c030000000963656aac6a530053abffffffffed94c39a582e1a46ce4c6bffda2ccdb16cda485f3a0d94b06206066da12aecfe010000000752abab63536363ef71dcfb02ee07fa0400000000016a6908c802000000000751656a6551abac688c2c2d", + "6a6351526551", + 0, + 858400684, + "552ff97d7924f51cda6d1b94be53483153ef725cc0a3a107adbef220c753f9a6" + ], + [ + "3a1f454a03a4591e46cf1f7605a3a130b631bf4dfd81bd2443dc4fac1e0a224e74112884fe0000000005516aac6a53a87e78b55548601ffc941f91d75eab263aa79cd498c88c37fdf275a64feff89fc1710efe03000000016a39d7ef6f2a52c00378b4f8f8301853b61c54792c0f1c4e2cd18a08cb97a7668caa008d970200000002656affffffff017642b20100000000096a63535253abac6a6528271998", + "51", + 2, + 1459585400, + "e9a7f21fc2d38be7be47095fbc8f1bf8923660aa4d71df6d797ae0ba5ca4d5b0" + ], + [ + "f59366cc0114c2a18e6bd1347ed9470f2522284e9e835dd5c5f7ef243639ebea95d9b232b6020000000153474b62eb045c00170500000000096352ab516352ab5200038a520400000000086aab5253656a63005b968904000000000963536353ac0053635387106002000000000000000000", + "ab52526300ab51", + 0, + 1834116153, + "cdf51f6e3a9dc2be5a59ea4c00f5aac1e1426a5202c325e6cf2567d07d8d8de4" + ], + [ + "6269e0fa0173e76e89657ca495913f1b86af5b8f1c1586bcd6c960aede9bc759718dfd5044000000000352ac530e2c7bd90219849b000000000007ab00ab6a53006319f281000000000007ab00515165ac5200000000", + "6a", + 0, + -2039568300, + "62094f98234a05bf1b9c7078c5275ed085656856fb5bdfd1b48090e86b53dd85" + ], + [ + "eb2bc00604815b9ced1c604960d54beea4a3a74b5c0035d4a8b6bfec5d0c9108f143c0e99a0000000000ffffffff22645b6e8da5f11d90e5130fd0a0df8cf79829b2647957471d881c2372c527d8010000000263acffffffff1179dbaf17404109f706ae27ad7ba61e860346f63f0c81cb235d2b05d14f2c1003000000025300264cb23aaffdc4d6fa8ec0bb94eff3a2e50a83418a8e9473a16aaa4ef8b855625ed77ef40100000003ac51acf8414ad404dd328901000000000652526500006ab6261c000000000002526a72a4c9020000000006ac526500656586d2e7000000000006656aac00ac5279cd8908", + "51", + 1, + -399279379, + "d37532e7b2b8e7db5c7c534197600397ebcc15a750e3af07a3e2d2e4f84b024f" + ], + [ + "dc9fe6a8038b84209bbdae5d848e8c040433237f415437592907aa798bf30d9dbbddf0ff85010000000153ffffffff23269a7ea29fcf788db483b8d4c4b35669e582608644259e950ce152b0fa6e050000000003acababffffffff65de94857897ae9ea3aa0b938ba6e5adf374d48469922d2b36dbb83d3b8c8261010000000452ac5200ffffffff02856e9b0300000000026a51980c8e02000000000365ab63d2648db4", + "00ab0051ac526565", + 2, + 1562581941, + "5cef9d8e18a2d5a70448f17b465d411a19dab78f0ddf1672ffd518b188f52433" + ], + [ + "eba8b0de04ac276293c272d0d3636e81400b1aaa60db5f11561480592f99e6f6fa13ad387002000000070053acab536563bebb23d66fd17d98271b182019864a90e60a54f5a615e40b643a54f8408fa8512cfac927030000000963ac6a6aabac65ababffffffff890a72192bc01255058314f376bab1dc72b5fea104c154a15d6faee75dfa5dba020000000100592b3559b0085387ac7575c05b29b1f35d9a2c26a0c27903cc0f43e7e6e37d5a60d8305a030000000252abffffffff0126518f05000000000000000000", + "005300635252635351", + 1, + 664344756, + "26dc2cba4bd5334e5c0b3a520b44cc1640c6b923d10e576062f1197171724097" + ], + [ + "91bd040802c92f6fe97411b159df2cd60fb9571764b001f31657f2d616964637605875c2a901000000055263006a65ffffffff3651df372645f50cf4e32fdf6e61c766e912e16335db2b40c5d52fe89eefe7cd00000000040065ab65ffffffff03ca8625030000000009ab51ac63530052ab52c6bf14020000000006ab00ab52005167d270000000000007ab53525351636a00000000", + "5151ab63005252ac", + 1, + 1983087664, + "3e5aa0200248d8d86ede3b315ca1b857018b89184a4bd023bd88ab12e499f6e1" + ], + [ + "185cda1a01ecf7a8a8c28466725b60431545fc7a3367ab68e34d486e8ea85ee3128e0d8384000000000465ac63abec88b7bb031c56eb04000000000965636a51005252006a7c78d5040000000007acac63abac51ac3024a40500000000086300526a51abac51464c0e8c", + "0065535265515352", + 0, + 1594558917, + "b5280b9610c0625a65b36a8c2402a95019a7bbb9dd3de77f7c3cb1d82c3263ba" + ], + [ + "a9531f07034091668b65fea8b1a79700d586ac9e2f42ca0455a26abe41f9e1805d009a0f5702000000096365516365ac5263ab3619bac643a9e28ee47855118cf80c3a74531cdf198835d206d0fe41804e325a4f9f105e03000000016a58e3ab0d46375d98994daf0fa7c600d2bb4669e726fca0e3a3f21ea0d9e777396740328f0100000008636a5363ab526a538d3ea7700304cb66030000000007515163ab52ab510184030500000000085353636565ac0051d9cff402000000000751ab52ab5352abf0e36254", + "ab5353ac5365acab", + 2, + 1633101834, + "04c9ef72f33668ca449c0415becf62cc0b8e0c75f9c8813852d42a58acf107c8" + ], + [ + "6b5ecc7903fe0ba37ea551df92a59e12bad0a3065846ba69179a8f4a741a2b4fcf679aac810200000004535263529a3d343293b99ab425e7ef8529549d84f480bcd92472bab972ea380a302128ae14dfcd0200000000025163ffffffff24636e4545cab9bf87009119b7fc3ec4d5ee9e206b90f35d1df8a563b6cd097a010000000852abac53005153abc64467860406e832020000000009526300006a53ac6352ac1395010000000002ac53b117f300000000000863655351acab00651edf02030000000008ab51ac6353535252628ef71d", + "ab63ab6a52ac526563", + 2, + -1559697626, + "8f07ece7d65e509f1e0780584ef8d271c1c61a13b10335d5faafc7afc8b5b8ec" + ], + [ + "92c9fb780138abc472e589d5b59489303f234acc838ca66ffcdf0164517a8679bb622a4267020000000153468e373d04de03fa020000000009ac006a5265ab5163006af649050000000007515153006a00658ceb59030000000001ac36afa0020000000009ab53006351ab51000000000000", + "6a", + 0, + 2059357502, + "e2358dfb51831ee81d7b0bc602a65287d6cd2dbfacf55106e2bf597e22a4b573" + ], + [ + "6f62138301436f33a00b84a26a0457ccbfc0f82403288b9cbae39986b34357cb2ff9b889b302000000045253655335a7ff6701bac9960400000000086552ab656352635200000000", + "6aac51", + 0, + 1444414211, + "502a2435fd02898d2ff3ab08a3c19078414b32ec9b73d64a944834efc9dae10c" + ], + [ + "9981143a040a88c2484ac3abe053849e72d04862120f424f373753161997dd40505dcb4783030000000700536365536565a2e10da3f4b1c1ad049d97b33f0ae0ea48c5d7c30cc8810e144ad93be97789706a5ead180100000003636a00ffffffffbdcbac84c4bcc87f03d0ad83fbe13b369d7e42ddb3aecf40870a37e814ad8bb5010000000963536a5100636a53abffffffff883609905a80e34202101544f69b58a0b4576fb7391e12a769f890eef90ffb72020000000651656352526affffffff04243660000000000004ab5352534a9ce001000000000863656363ab6a53652df19d030000000003ac65acedc51700000000000000000000", + "ac6300acac", + 2, + 293672388, + "7ba99b289c04718a7283f150d831175ed6303081e191a0608ea81f78926c5bdf" + ], + [ + "a2bb630b01989bc5d643f2da4fb9b55c0cdf846ba06d1dbe372893024dbbe5b9b8a1900af802000000055265ac63aca7a68d2f04916c74010000000003abac007077f0040000000001007d4127010000000005ac516aac000f31e8030000000000571079c9", + "65ab0051ac", + 0, + -1103627693, + "92d53b4390262e6b288e8a32e0cfc36cd5adfdfabfe96c7bfd4a19d65e233761" + ], + [ + "49f7d0b6037bba276e910ad3cd74966c7b3bc197ffbcfefd6108d6587006947e97789835ea0300000008526a52006a650053ffffffff8d7b6c07cd10f4c4010eac7946f61aff7fb5f3920bdf3467e939e58a1d4100ab03000000076aac63ac535351ffffffff8f48c3ba2d52ad67fbcdc90d8778f3c8a3894e3c35b9730562d7176b81af23c80100000003ab5265ffffffff0301e3ef0300000000046a525353e899ac0500000000075153ab6a65abac259bea0400000000007b739972", + "53516aacac6aac", + 1, + 955403557, + "5d366a7f4346ae18aeb7c9fc4dab5af71173184aa20ed22fcb4ea8511ad25449" + ], + [ + "58a4fed801fbd8d92db9dfcb2e26b6ff10b120204243fee954d7dcb3b4b9b53380e7bb8fb60100000003006351ffffffff02a0795b050000000006536351ac6aac2718d00200000000075151acabac515354d21ba1", + "005363515351", + 0, + -1322430665, + "bbee941bbad950424bf40e3623457db47f60ed29deaa43c99dec702317cb3326" + ], + [ + "32765a0b02e455793d9ce530e9f6a44bcbc612e893a875b5da61d822dc56d8245166c398b403000000085353abac6300006a6bdee2a78d0d0b6a5ea666eed70b9bfea99d1d612ba3878f615c4da10d4a521cba27155002000000035363abffffffff043cd42401000000000551656a53653685320100000000030000511881bc0500000000065165abab636a20169f010000000007acab656aac63acdb0706a8", + "65ac53ab53", + 0, + 1936499176, + "5c5a9c3a5de7dc7a82bc171c9d3505913b8bcc450bc8b2d11772c1a1d781210b" + ], + [ + "17fad0d303da0d764fedf9f2887a91ea625331b28704940f41e39adf3903d8e75683ef6d46020000000151ffffffffff376eea4e880bcf0f03d33999104aafed2b3daf4907950bb06496af6b51720a020000000900636a63525253525196521684f3b08497bad2c660b00b43a6a517edc58217876eb5e478aa3b5fda0f29ee1bea00000000046aacab6affffffff03dde8e2050000000007ac5365ac51516a14772e000000000005630000abacbbb360010000000006ab5251ab656a50f180f0", + "0053", + 0, + -1043701251, + "a3bdf8771c8990971bff9b4e7d59b7829b067ed0b8d3ac1ec203429811384668" + ], + [ + "236c32850300045e292c84ede2b9ab5733ba08315a2bb09ab234c4b4e8894808edbdac0d3b020000000653635363abacffffffffd3f696bb31fdd18a72f3fc2bb9ae54b416a253fc37c1a0f0180b52d35bad49440100000004650053abffffffffa85c75a2406d82a93b12e555b66641c1896a4e83ae41ef1038218311e38ace060200000006abab006a51ac104b5e6701e2842c04000000000800630051ac0000ab00000000", + "ab63ac6a516a", + 1, + -1709887524, + "8c29ea8ef60c5a927fccdba8ea385db6b6b84d98e891db45f5d4ee3148d3f5a7" + ], + [ + "b78d5fd601345f3100af494cdf447e7d4076179f940035b0ebe8962587d4d0c9c6c9fc34ee0300000003516a6affffffff03dc5c890100000000085353ac53ac6a52534ac941040000000007ac63656a51ab51d4266b0100000000036aacac70731f2d", + "005351ab0053", + 0, + -1789071265, + "d5f1c1cb35956a5711d67bfb4cedbc67e77c089b912d688ad440ff735adb390d" + ], + [ + "5a2257df03554550b774e677f348939b37f8e765a212e566ce6b60b4ea8fed4c9504b7f7d1000000000653655265ab5258b67bb931df15b041177cf9599b0604160b79e30f3d7a594e7826bae2c29700f6d8f8f40300000005515300ac6a159cf8808a41f504eb5c2e0e8a9279f3801a5b5d7bc6a70515fbf1c5edc875bb4c9ffac500000000050063510052ffffffff0422a90105000000000965006a650000516a006417d2020000000006526363ab00524d969d0100000000035153acc4f077040000000005ac5200636500000000", + "6a52", + 1, + -1482463464, + "37b794b05d0687c9b93d5917ab068f6b2f0e38406ff04e7154d104fc1fb14cdc" + ], + [ + "e0032ad601269154b3fa72d3888a3151da0aed32fb2e1a15b3ae7bee57c3ddcffff76a1321010000000100110d93ae03f5bd080100000000075263516a6551002871e60100000000046a005252eaa753040000000004ab6aab526e325c71", + "630052", + 0, + -1857873018, + "ea117348e94de86381bb8ad1c7f93b8c623f0272104341701bb54e6cb433596c" + ], + [ + "014b2a5304d46764817aca180dca50f5ab25f2e0d5749f21bb74a2f8bf6b8b7b3fa8189cb7030000000965ac5165ab6a51ac6360ecd91e8abc7e700a4c36c1a708a494c94bb20cbe695c408543146566ab22be43beae9103000000045163ab00ffffffffffa48066012829629a9ec06ccd4905a05df0e2b745b966f6a269c9c8e13451fc00000000026565ffffffffc40ccadc21e65fe8a4b1e072f4994738ccaf4881ae6fede2a2844d7da4d199ab02000000065152ab536aabffffffff01b6e054030000000004515352ab3e063432", + "", + 0, + 1056459916, + "a7aff48f3b8aeb7a4bfe2e6017c80a84168487a69b69e46681e0d0d8e63a84b6" + ], + [ + "c4ef04c103c5dde65410fced19bf6a569549ecf01ceb0db4867db11f2a3a3eef0320c9e8e001000000085100536a53516aabffffffff2a0354fa5bd96f1e28835ffe30f52e19bd7d5150c687d255021a6bec03cf4cfd03000000056a006300514900c5b01d3d4ae1b97370ff1155b9dd0510e198d266c356d6168109c54c11b4c283dca00300000002ababffffffff02e19e3003000000000451655351fa5c0003000000000163ef1fc64b", + "51636a51ab630065", + 1, + -1754709177, + "0a281172d306b6a32e166e6fb2a2cc52c505c5d60ea448e9ba7029aa0a2211e1" + ], + [ + "29083fe00398bd2bb76ceb178f22c51b49b5c029336a51357442ed1bac35b67e1ae6fdf13100000000066a6500acab51ffffffffe4ca45c9dc84fd2c9c47c7281575c2ba4bf33b0b45c7eca8a2a483f9e3ebe4b3010000000200abffffffffdf47ad2b8c263fafb1e3908158b18146357c3a6e0832f718cd464518a219d18303000000096352ac656351ac0052daddfb3b0231c36f00000000000400526a5275c7e0020000000001ab00000000", + "acab536aac52", + 2, + 300802386, + "82ebc07b16cff0077e9c1a279373185b3494e39d08fd3194aae6a4a019377509" + ], + [ + "1201ab5d04f89f07c0077abd009762e59db4bb0d86048383ba9e1dad2c9c2ad96ef660e6d00200000007ab6a65ac5200652466fa5143ab13d55886b6cdc3d0f226f47ec1c3020c1c6e32602cd3428aceab544ef43e00000000086a6a6a526a6a5263ffffffffd5be0b0be13ab75001243749c839d779716f46687e2e9978bd6c9e2fe457ee48020000000365abab1e1bac0f72005cf638f71a3df2e3bbc0fa35bf00f32d9c7dc9c39a5e8909f7d53170c8ae0200000008ab6a51516363516affffffff02f0a6210500000000036300ac867356010000000009acab65ac6353536a659356d367", + "ac53535252", + 0, + 917543338, + "418acc156c2bc76a5d7baa58db29f1b4cf6c266c9222ed167ef5b4d47f0e0f41" + ], + [ + "344fa11e01c19c4dd232c77742f0dd0aeb3695f18f76da627628741d0ee362b0ea1fb3a2180200000007635151005100529bab25af01937c1f0500000000055153ab53656e7630af", + "6351005163ac51", + 0, + -629732125, + "228ca52a0a376fe0527a61cfa8da6d7baf87486bba92d49dfd3899cac8a1034f" + ], + [ + "b2fda1950191358a2b855f5626a0ebc830ab625bea7480f09f9cd3b388102e35c0f303124c030000000565ac65ab53ffffffff03f9c5ec04000000000765ab51516551650e2b9f0500000000045365525284e8f6040000000001ac00000000", + "ac51655253", + 0, + 1433027632, + "d2fa7e13c34cecda5105156bd2424c9b84ee0a07162642b0706f83243ff811a8" + ], + [ + "a4a6bbd201aa5d882957ac94f2c74d4747ae32d69fdc765add4acc2b68abd1bdb8ee333d6e0300000008516a6552515152abffffffff02c353cb040000000007ac6351ab51536588bd320500000000066552525253ac00000000", + "", + 0, + 1702060459, + "499da7d74032388f820645191ac3c8d20f9dba8e8ded7fa3a5401ea2942392a1" + ], + [ + "584e8d6c035a6b2f9dac2791b980a485994bf38e876d9dda9b77ad156eee02fa39e19224a60300000003ab636529db326cc8686a339b79ab6b6e82794a18e0aabc19d9ad13f31dee9d7aad8eff38288588020000000452530052ffffffff09a41f07755c16cea1c7e193c765807d18cadddca6ec1c2ed7f5dcdca99e90e80000000001acffffffff01cba62305000000000451ac63acccdf1f67", + "ab536a6363", + 2, + -27393461, + "1125645b49202dca2df2d76dae51877387903a096a9d3f66b5ac80e042c95788" + ], + [ + "83a583d204d926f2ee587a83dd526cf1e25a44bb668e45370798f91a2907d184f7cddcbbc7030000000700ab6565536a539f71d3776300dffdfa0cdd1c3784c9a1f773e34041ca400193612341a9c42df64e3f550e01000000050052515251ffffffff52dab2034ab0648553a1bb8fc4e924b2c89ed97c18dfc8a63e248b454035564b01000000015139ab54708c7d4d2c2886290f08a5221cf69592a810fd1979d7b63d35c271961e710424fd0300000005ac65ac5251ffffffff01168f7c030000000000a85e5fb0", + "6a536353656a00", + 0, + 179595345, + "5350a31ac954a0b49931239d0ecafbf34d035a537fd0c545816b8fdc355e9961" + ], + [ + "ffd35d51042f290108fcb6ea49a560ba0a6560f9181da7453a55dfdbdfe672dc800b39e7320200000006630065516a65f2166db2e3827f44457e86dddfd27a8af3a19074e216348daa0204717d61825f198ec0030100000006ab51abab00abffffffffdf41807adb7dff7db9f14d95fd6dc4e65f8402c002d009a3f1ddedf6f4895fc8030000000500ab006a65a5a848345052f860620abd5fcd074195548ce3bd0839fa9ad8642ed80627bf43a0d47dbd010000000765ab006a656a53b38cdd6502a186da05000000000765ab00ab006a53527c0e0100000000085365ab51acacac52534bd1b1", + "6a635253ac0000", + 0, + 1095082149, + "3c05473a816621a3613f0e903faa1a1e44891dd40862b029e41fc520776350fa" + ], + [ + "6c9a4b98013c8f1cae1b1df9f0f2de518d0c50206a0ab871603ac682155504c0e0ce946f460100000000ffffffff04e9266305000000000753535100ac6aacded39e04000000000365ac6ab93ccd010000000002515397bf3d050000000003ab636300000000", + "63520052ac656353", + 0, + -352633155, + "936eff8cdfd771be24124da87c7b24feb48da7cbc2c25fb5ba13d1a23255d902" + ], + [ + "e01dc7f0021dc07928906b2946ca3e9ac95f14ad4026887101e2d722c26982c27dc2b59fdb0000000005ac5200516ab5a31ffadcbe74957a5a3f97d7f1475cc6423fc6dbc4f96471bd44c70cc736e7dec0d1ea020000000951636a526a52abac53ffffffff04bc2edd05000000000252ab528c7b02000000000952ac51526500525353324820040000000002005380c713000000000009630065ab00ac525252451bbb48", + "53ab65ac", + 0, + -552384418, + "69c0b30f4c630a6c878fde6ea6b74dae94f4eb3bcfbde2dc3649e1a9ada00757" + ], + [ + "009046a1023f266d0113556d604931374d7932b4d6a7952d08fbd9c9b87cbd83f4f4c178b4030000000452ac526346e73b438c4516c60edd5488023131f07acb5f9ea1540b3e84de92f4e3c432289781ea4900000000046500655357dfd6da02baef910100000000026a007d101703000000000800516500abacac5100000000", + "6aab6553ac", + 0, + -802456605, + "f8757fbb4448ca34e0cd41b997685b37238d331e70316659a9cc9087d116169d" + ], + [ + "df76ec0801a3fcf3d18862c5f686b878266dd5083f16cf655facab888b4cb3123b3ce5db7e01000000010010e7ac6a0233c83803000000000365ac51faf14a040000000004ac51655100000000", + "6353acab", + 0, + 15705861, + "e7d873aa079a19ec712b269a37d2670f60d8cb334c4f97e2e3fd10eeb8ee5f5e" + ], + [ + "828fd3e0031084051ccef9cfdd97fae4d9cc50c0dae36bd22a3ff332881f17e9756c3e288e0200000004ab535363961a2ccccaf0218ec6a16ba0c1d8b5e93cfd025c95b6e72bc629ec0a3f47da7a4c396dad01000000025353ffffffff19ad28747fb32b4caf7b5dbd9b2da5a264bedb6c86d3a4805cd294ae53a86ac40200000009ab53535351ab6551abffffffff04a41650030000000005656aab6aab8331a304000000000700516365ac516a0d2a47010000000007abac516353abacdebc19040000000006ab5300636a6300000000", + "51ab52ab53ac52", + 0, + 1866105980, + "311094b4d73e31aefc77e97859ef07ca2f07a7b7e4d7def80c69d3f5d58527e5" + ], + [ + "c4b80f850323022205b3e1582f1ed097911a81be593471a8dce93d5c3a7bded92ef6c7c1260100000002006affffffff70294d62f37c3da7c5eae5d67dce6e1b28fedd7316d03f4f48e1829f78a88ae801000000096a5200530000516351f6b7b544f7c39189d3a2106ca58ce4130605328ce7795204be592a90acd81bef517d6f170200000000ffffffff012ab8080000000000075100006365006335454c1e", + "53ac6a536aacac", + 0, + -1124103895, + "06277201504e6bf8b8c94136fad81b6e3dadacb9d4a2c21a8e10017bfa929e0e" + ], + [ + "8ab69ed50351b47b6e04ac05e12320984a63801716739ed7a940b3429c9c9fed44d3398ad40300000006536a516a52638171ef3a46a2adb8025a4884b453889bc457d63499971307a7e834b0e76eec69c943038a0300000000ffffffff566bb96f94904ed8d43d9d44a4a6301073cef2c011bf5a12a89bedbaa03e4724030000000265acb606affd01edea38050000000008515252516aacac6300000000", + "65000000006365ac53", + 0, + -1338942849, + "7912573937824058103cb921a59a7f910a854bf2682f4116a393a2045045a8c3" + ], + [ + "2484991e047f1cf3cfe38eab071f915fe86ebd45d111463b315217bf9481daf0e0d10902a402000000006e71a424eb1347ffa638363604c0d5eccbc90447ff371e000bf52fc743ec832851bb564a0100000001abffffffffef7d014fad3ae7927948edbbb3afe247c1bcbe7c4c8f5d6cf97c799696412612020000000851536a5353006a001dfee0d7a0dd46ada63b925709e141863f7338f34f7aebde85d39268ae21b77c3068c01d0000000008535151ab00636563ffffffff018478070200000000095200635365ac52ab5341b08cd3", + "", + 3, + 265623923, + "24cb420a53b4f8bb477f7cbb293caabfd2fc47cc400ce37dbbab07f92d3a9575" + ], + [ + "54839ef9026f65db30fc9cfcb71f5f84d7bb3c48731ab9d63351a1b3c7bc1e7da22bbd508e0300000000442ad138f170e446d427d1f64040016032f36d8325c3b2f7a4078766bdd8fb106e52e8d20000000003656500ffffffff02219aa101000000000851ababac52ab00659646bd02000000000552acacabac24c394a5", + "ac", + 0, + 906807497, + "69264faadcd1a581f7000570a239a0a26b82f2ad40374c5b9c1f58730514de96" + ], + [ + "5036d7080434eb4eef93efda86b9131b0b4c6a0c421e1e5feb099a28ff9dd8477728639f77030000000951516aab535152ab5391429be9cce85d9f3d358c5605cf8c3666f034af42740e94d495e28b9aaa1001ba0c87580300000008006552ab00ab006affffffffd838978e10c0c78f1cd0a0830d6815f38cdcc631408649c32a25170099669daa0000000002acab8984227e804ad268b5b367285edcdf102d382d027789250a2c0641892b480c21bf84e3fb0100000000b518041e023d8653010000000001004040fb0100000000080051ac5200636a6300000000", + "52ac", + 0, + 366357656, + "bd0e88829afa6bdc1e192bb8b2d9d14db69298a4d81d464cbd34df0302c634c6" + ], + [ + "9ad5ccf503fa4facf6a27b538bc910cce83c118d6dfd82f3fb1b8ae364a1aff4dcefabd38f03000000096365655263ac655300807c48130c5937190a996105a69a8eba585e0bd32fadfc57d24029cbed6446d30ebc1f100100000004000053650f0ccfca1356768df7f9210cbf078a53c72e0712736d9a7a238e0115faac0ca383f219d0010000000600ab536552002799982b0221b8280000000000000c41320000000000086552ac6365636a6595f233a3", + "6a5152", + 2, + 553208588, + "f99c29a79f1d73d2a69c59abbb5798e987639e36d4c44125d8dc78a94ddcfb13" + ], + [ + "669538a204047214ce058aed6a07ca5ad4866c821c41ac1642c7d63ed0054f84677077a84f030000000853abacab6a655353ffffffff70c2a071c115282924e3cb678b13800c1d29b6a028b3c989a598c491bc7c76c5030000000752ac52ac5163ac80420e8a6e43d39af0163271580df6b936237f15de998e9589ec39fe717553d415ac02a4030000000463635153184ad8a5a4e69a8969f71288c331aff3c2b7d1b677d2ebafad47234840454b624bf7ac1d03000000056a63abab63df38c24a02fbc63a040000000002ab535ec3dc050000000002536500000000", + "635153", + 3, + -190399351, + "9615541884dfb1feeb08073a6a6aa73ef694bc5076e52187fdf4138a369f94d9" + ], + [ + "a7f139e502af5894be88158853b7cbea49ba08417fbbca876ca6614b5a41432be34499987b000000000765635165abac63ffffffff8b8d70e96c7f54eb70da0229b548ced438e1ca2ba5ddd648a027f72277ee1efc0100000001abffffffff044f2c4204000000000165e93f550100000000050000526a6a94550304000000000365536aadc21c0300000000016300000000", + "6aacac6363ab5265ac", + 1, + 2143189425, + "6e3f97955490d93d6a107c18d7fe402f1cada79993bb0ff0d096357261b3a724" + ], + [ + "3b94438f0366f9f53579a9989b86a95d134256ce271da63ca7cd16f7dd5e4bffa17d35133f010000000100ffffffff1aaad0c721e06ec00d07e61a84fb6dc840b9a968002ce7e142f943f06fd143a10100000008535151ac51ab0053b68b8e9c672daf66041332163e04db3f6048534bd718e1940b3fc3811c4eef5b7a56888b01000000001d58e38c012e38e700000000000852ab53ac6365536a00000000", + "ab655352", + 1, + -935223304, + "b3b336de141d4f071313a2207b2a0c7cf54a070dd8d234a511b7f1d13e23b0c4" + ], + [ + "e5dca8a20456de0a67e185fa6ea94085ceae478d2c15c73cb931a500db3a1b6735dd1649ec0200000005ab536aabab32d11bbdcb81361202681df06a6b824b12b5cb40bb1a672cf9af8f2a836e4d95b7839327030000000951005365ab65abacabb345085932939eef0c724adef8a57f9e1bf5813852d957c039b6a12d9c2f201ea520fb030000000009ac5352005165acac6a5efc6072f1a421dc7dc714fc6368f6d763a5d76d0278b95fc0503b9268ccfadb48213a2500000000026a53ffffffff039ee1c4020000000009ac5353ab6353535163184018000000000005655265526a9a4a8a050000000001ac00000000", + "65ab53ab6a00ab6553", + 2, + 1902561212, + "7928ae8e86c0b0cad1b2c120ea313087437974382ee6d46443ca5ac3f5878b88" + ], + [ + "972128b904e7b673517e96e98d80c0c8ceceae76e2f5c126d63da77ffd7893fb53308bb2da0300000006ac6552ab52acffffffff4cac767c797d297c079a93d06dc8569f016b4bf7a7d79b605c526e1d36a40e2202000000095365ab636aac6a6a6a69928d2eddc836133a690cfb72ec2d3115bf50fb3b0d10708fa5d2ebb09b4810c426a1db01000000060052526300001e8e89585da7e77b2dd2e30625887f0660accdf29e53a614d23cf698e6fc8ab03310e87700000000076a520051acac6555231ddb0330ec2d03000000000200abfaf457040000000004ab6a6352bdc42400000000000153d6dd2f04", + "", + 0, + 209234698, + "4a92fec1eb03f5bd754ee9bfd70707dc4420cc13737374f4675f48529be518e4" + ], + [ + "1fb4085b022c6cfb848f8af7ba3ba8d21bd23ffa9f0bfd181cb68bcaaf2074e66d4974a31602000000090000006a6a6500acab6c12c07d9f3dbd2d93295c3a49e3757119767097e7fd5371f7d1ba9ba32f1a67a5a426f00000000000ffffffff018fd2fc04000000000363ac5100000000", + "65ab006a6aab526a", + 0, + 1431502299, + "8b7dd0ff12ca0d8f4dbf9abf0abba00e897c2f6fd3b92c79f5f6a534e0b33b32" + ], + [ + "5374f0c603d727f63006078bd6c3dce48bd5d0a4b6ea00a47e5832292d86af258ea0825c260000000009655353636352526a6af2221067297d42a9f8933dfe07f61a574048ff9d3a44a3535cd8eb7de79fb7c45b6f47320200000003ac006affffffff153d917c447d367e75693c5591e0abf4c94bbdd88a98ab8ad7f75bfe69a08c470200000005ac65516365ffffffff037b5b7b000000000001515dc4d904000000000004bb26010000000004536a6aac00000000", + "516552516352ac", + 2, + 328538756, + "8bb7a0129eaf4b8fc23e911c531b9b7637a21ab11a246352c6c053ff6e93fcb6" + ], + [ + "c441132102cc82101b6f31c1025066ab089f28108c95f18fa67db179610247086350c163bd010000000651525263ab00ffffffff9b8d56b1f16746f075249b215bdb3516cbbe190fef6292c75b1ad8a8988897c3000000000751ab6553abab00ffffffff02f9078b000000000009ab0053ac51ac00ab51c0422105000000000651006563525200000000", + "ac51", + 0, + -197051790, + "55acd8293ed0be6792150a3d7ced6c5ccd153ca7daf09cee035c1b0dac92bb96" + ], + [ + "ab82ad3b04545bd86b3bb937eb1af304d3ef1a6d1343ed809b4346cafb79b7297c09e1648202000000086351ac5200535353ffffffff95d32795bbaaf5977a81c2128a9ec0b3c7551b9b1c3d952876fcb423b2dfb9e80000000005515363acac47a7d050ec1a603627ce6cd606b3af314fa7964abcc579d92e19c7aba00cf6c3090d6d4601000000056a516551633e794768bfe39277ebc0db18b5afb5f0c8117dde9b4dfd5697e9027210eca76a9be20d63000000000700520063ab6aacffffffff01ec2ddc050000000008ac52ac65ac65ac5100000000", + "536300abab", + 1, + -2070209841, + "b362da5634f20be7267de78b545d81773d711b82fe9310f23cd0414a8280801d" + ], + [ + "8bff9d170419fa6d556c65fa227a185fe066efc1decf8a1c490bc5cbb9f742d68da2ab7f320100000007ab000053525365a7a43a80ab9593b9e8b6130a7849603b14b5c9397a190008d89d362250c3a2257504eb810200000007acabacac00ab51ee141be418f003e75b127fd3883dbf4e8c3f6cd05ca4afcaac52edd25dd3027ae70a62a00000000008ac52526a5200536affffffffb8058f4e1d7f220a1d1fa17e96d81dfb9a304a2de4e004250c9a576963a586ae0300000005abacac5363b9bc856c039c01d804000000000951656aac53005365acb0724e00000000000565abab63acea7c7a0000000000036a00ac00000000", + "6565", + 1, + -1349282084, + "2b822737c2affeefae13451d7c9db22ff98e06490005aba57013f6b9bbc97250" + ], + [ + "0e1633b4041c50f656e882a53fde964e7f0c853b0ada0964fc89ae124a2b7ffc5bc97ea6230100000006ac6aacacabacffffffff2e35f4dfcad2d53ea1c8ada8041d13ea6c65880860d96a14835b025f76b1fbd9000000000351515121270867ef6bf63a91adbaf790a43465c61a096acc5a776b8e5215d4e5cd1492e611f761000000000600ac6aab5265ffffffff63b5fc39bcac83ca80ac36124abafc5caee608f9f63a12479b68473bd4bae769000000000965ac52acac5263acabffffffff0163153e020000000008ab005165ab65515300000000", + "6a6aac00", + 0, + -968477862, + "20732d5073805419f275c53784e78db45e53332ee618a9fcf60a3417a6e2ca69" + ], + [ + "2b052c24022369e956a8d318e38780ef73b487ba6a8f674a56bdb80a9a63634c6110fb5154010000000251acffffffff48fe138fb7fdaa014d67044bc05940f4127e70c113c6744fbd13f8d51d45143e01000000005710db3804e01aa9030000000008acac6a516a5152abfd55aa01000000000751ab510000ac636d6026010000000000b97da9000000000000fddf3b53", + "006552", + 0, + 595461670, + "685d67d84755906d67a007a7d4fa311519467b9bdc6a351913246a41e082a29f" + ], + [ + "073bc856015245f03b2ea2da62ccedc44ecb99e4250c7042f596bcb23b294c9dc92cfceb6b02000000095163abab52abab636afe292fb303b7c3f001000000000352636af3c49502000000000400ac6a535851850100000000066aac6553ab6500000000", + "ab6aab53006aab52", + 0, + 247114317, + "123916c6485cf23bfea95654a8815fbf04ce4d21a3b7f862805c241472906658" + ], + [ + "7888b71403f6d522e414d4ca2e12786247acf3e78f1918f6d727d081a79813d129ee8befce0100000009ab516a6353ab6365abffffffff4a882791bf6400fda7a8209fb2c83c6eef51831bdf0f5dacde648859090797ec030000000153ffffffffbb08957d59fa15303b681bad19ccf670d7d913697a2f4f51584bf85fcf91f1f30200000008526565ac52ac63acffffffff0227c0e8050000000001ac361dc801000000000800515165ab00ab0000000000", + "656a", + 2, + 1869281295, + "f43378a0b7822ad672773944884e866d7a46579ee34f9afc17b20afc1f6cf197" + ], + [ + "cc4dda57047bd0ca6806243a6a4b108f7ced43d8042a1acaa28083c9160911cf47eab910c40200000007526a0000ab6a63e4154e581fcf52567836c9a455e8b41b162a78c85906ccc1c2b2b300b4c69caaaa2ba0230300000008ab5152ac5100ab65ffffffff69696b523ed4bd41ecd4d65b4af73c9cf77edf0e066138712a8e60a04614ea1c0300000004ab6a000016c9045c7df7836e05ac4b2e397e2dd72a5708f4a8bf6d2bc36adc5af3cacefcf074b8b403000000065352ac5252acffffffff01d7e380050000000000cf4e699a", + "525163656351", + 1, + -776533694, + "ff18c5bffd086e00917c2234f880034d24e7ea2d1e1933a28973d134ca9e35d2" + ], + [ + "b7877f82019c832707a60cf14fba44cfa254d787501fdd676bd58c744f6e951dbba0b3b77f0200000009ac515263ac53525300a5a36e500148f89c0500000000085265ac6a6a65acab00000000", + "6563", + 0, + -1785108415, + "cb6e4322955af12eb29613c70e1a00ddbb559c887ba844df0bcdebed736dffbd" + ], + [ + "aeb14046045a28cc59f244c2347134d3434faaf980961019a084f7547218785a2bd03916f3000000000165f852e6104304955bda5fa0b75826ee176211acc4a78209816bbb4419feff984377b2352200000000003a94a5032df1e0d60390715b4b188c330e4bb7b995f07cdef11ced9d17ee0f60bb7ffc8e0100000002516513e343a5c1dc1c80cd4561e9dddad22391a2dbf9c8d2b6048e519343ca1925a9c6f0800a020000000665516365ac513180144a0290db27000000000006ab655151ab5138b187010000000007ab5363abac516a9e5cd98a", + "53ac", + 0, + 478591320, + "e8d89a302ae626898d4775d103867a8d9e81f4fd387af07212adab99946311ef" + ], + [ + "c9270fe004c7911b791a00999d108ce42f9f1b19ec59143f7b7b04a67400888808487bd59103000000066a0052ac6565b905e76687be2dd7723b22c5e8269bc0f2000a332a289cfc40bc0d617cfe3214a61a85a30300000007ac63ac00635251560871209f21eb0268f175b8b4a06edd0b04162a974cf8b5dada43e499a1f22380d35ede0300000000792213fc58b6342cc8100079f9f5f046fb89f2d92cf0a2cb6d07304d32d9da858757037c0000000008abab51636565516affffffff02c72a8b03000000000452acac530dfb9f05000000000096f94307", + "5253ab536351", + 3, + 543688436, + "0278adbcc476d135493ae9bdcd7b3c2002df17f2d81c17d631c50c73e546c264" + ], + [ + "57a5a04c0278c8c8e243d2df4bb716f81d41ac41e2df153e7096f5682380c4f441888d9d260300000004ab63ab6afdbe4203525dff42a7b1e628fe22bccaa5edbb34d8ab02faff198e085580ea5fcdb0c61b0000000002ac6affffffff03375e6c05000000000663ab516a6a513cb6260400000000007ca328020000000006516a636a52ab94701cc7", + "0053ac5152", + 0, + -550925626, + "b7ca991ab2e20d0158168df2d3dd842a57ab4a3b67cca8f45b07c4b7d1d11126" + ], + [ + "072b75a504ad2550c2e9a02614bc9b2a2f50b5b553af7b87c0ef07c64ddc8d8934c96d216401000000036aabaca1387242a5bcd21099b016ad6045bed7dce603472757d9822cc5f602caa4ae20414d378b02000000026a63e4ac816734acdc969538d6f70b8ab43a2589f55e0177a4dc471bdd0eb61d59f0f46f6bb801000000065351526aab52d9f2977be76a492c3a7617b7a16dc29a3b0a7618f328c2f7d4fd9bafe760dc427a5066ef000000000465635165ffffffff02c5793600000000000165296820050000000002ac6300000000", + "53006a6aac0052ab", + 2, + 66084636, + "437e89bb6f70fd2ed2feef33350b6f6483b891305e574da03e580b3efd81ae13" + ], + [ + "7e27c42d0279c1a05eeb9b9faedcc9be0cab6303bde351a19e5cbb26dd0d594b9d74f40d2b020000000200518c8689a08a01e862d5c4dcb294a2331912ff11c13785be7dce3092f154a005624970f84e0200000000500cf5a601e74c1f0000000000076aab52636a6a5200000000", + "6500006a5351", + 0, + 449533391, + "535ba819d74770d4d613ee19369001576f98837e18e1777b8246238ff2381dd0" + ], + [ + "11414de403d7f6c0135a9df01cb108c1359b8d4e105be50a3dcba5e6be595c8817217490b20000000003005263ffffffff0c6becb9c3ad301c8dcd92f5cbc07c8bed7973573806d1489316fc77a829da03030000000700005253535352ffffffff2346d74ff9e12e5111aa8779a2025981850d4bf788a48de72baa2e321e4bc9ca00000000056352acab63cc585b64045e0385050000000009ab5253ab516aacac00efa9cf0300000000065200635151acbe80330400000000070063635100ab000be159050000000007525300655300ac00000000", + "51656a0051ab", + 0, + 683137826, + "d4737f3b58f3e5081b35f36f91acde89dda00a6a09d447e516b523e7a99264d5" + ], + [ + "1c6b5f29033fc139338658237a42456123727c8430019ca25bd71c6168a9e35a2bf54538d80100000008536aac52ac6a6a52ffffffff3fb36be74036ff0c940a0247c451d923c65f826793d0ac2bb3f01ecbec8033290100000007ab000051ab6363ffffffff5d9eca0cf711685105bd060bf7a67321eaef95367acffab36ce8dedddd632ee2000000000652ac6a63ac517167319e032d26de040000000003516363dc38fb010000000000b37b00000000000006ab520051ac534baba51f", + "636300ababac6563", + 0, + -2049129935, + "3282a2ec6b8c87c9303e6060c17b421687db1bd35fbfa0345b48f2490e15b6cc" + ], + [ + "978b9dad0214cfc7ce392d74d9dcc507350dc34007d72e4125861c63071ebf2cc0a6fd4856020000000651ac6a6aab52ffffffff47f20734e3370e733f87a6edab95a7a268ae44db7a8974e255614836b22938720200000008635265ac51516553ffffffff0137b2560100000000035252ac2f3363e9", + "006aab6352", + 1, + 2014249801, + "55611a5fb1483bce4c14c33ed15198130e788b72cd8929b2ceef4dd68b1806bf" + ], + [ + "442f1c8703ab39876153c241ab3d69f432ba6db4732bea5002be45c8ca10c3a2356fe0e9590300000001accb2b679cab7c58a660cb6d4b3452c21cd7251a1b77a52c300f655f5baeb6fa27ff5b79880300000003005252e5ccf55712bc8ed6179f6726f8a78f3018a7a0391594b7e286ef5ee99efdcde302a102cc0200000009006352526351536a63ffffffff04443f63030000000006536a63ab63651405fb020000000009ac535351525300ab6a9f172b000000000004ab535263ad5c50050000000008656a65ab630000ac00000000", + "65636aab006552", + 2, + 2125838294, + "b3ff10f21e71ebc8b25fe058c4074c42f08617e0dcc03f9e75d20539d3242644" + ], + [ + "2b3470dd028083910117f86614cdcfb459ee56d876572510be4df24c72e8f58c70d5f5948b03000000066aab65635265da2c3aac9d42c9baafd4b655c2f3efc181784d8cba5418e053482132ee798408ba43ccf90300000000ffffffff047dda4703000000000765516a52ac53009384a603000000000651636a63ab6a8cf57a03000000000352ab6a8cf6a405000000000952636a6a6565525100661e09cb", + "ac520063ac6a6a52", + 1, + 1405647183, + "9b360c3310d55c845ef537125662b9fe56840c72136891274e9fedfef56f9bb5" + ], + [ + "d74282b501be95d3c19a5d9da3d49c8a88a7049c573f3788f2c42fc6fa594f59715560b9b00000000009655353525265ac52ac9772121f028f8303030000000003510065af5f47040000000007ac516a6551630000000000", + "acab53006363ac", + 0, + -1113209770, + "2f482b97178f17286f693796a756f4d7bd2dfcdbecd4142528eec1c7a3e5101a" + ], + [ + "3a5644a9010f199f253f858d65782d3caec0ac64c3262b56893022b9796086275c9d4d097b02000000009d168f7603a67b30050000000007ac51536a0053acd9d88a050000000007655363535263ab3cf1f403000000000352ac6a00000000", + "005363536565acac6a", + 0, + -1383947195, + "6390ab0963cf611e0cea35a71dc958b494b084e6fd71d22217fdc5524787ade6" + ], + [ + "67b3cc43049d13007485a8133b90d94648bcf30e83ba174f5486ab42c9107c69c5530c5e1f0000000003005100ffffffff9870ebb65c14263282ea8d41e4f4f40df16b565c2cf86f1d22a9494cad03a67f01000000016a5a121bee5e359da548e808ae1ad6dfccae7c67cbb8898d811638a1f455a671e822f228ef030000000151c1fcc9f9825f27c0dde27ea709da62a80a2ff9f6b1b86a5874c50d6c37d39ae31fb6c8a0030000000163553b8786020ca74a00000000000665635153ab5275c0760000000000020052e659b05d", + "636aab6a6a", + 0, + -342795451, + "f77c3322c97b1681c17b1eba461fa27b07e04c1534e8aaf735a49cab72c7c2e2" + ], + [ + "bda1ff6804a3c228b7a12799a4c20917301dd501c67847d35da497533a606701ad31bf9d5e0300000001ac16a6c5d03cf516cd7364e4cbbf5aeccd62f8fd03cb6675883a0636a7daeb650423cb1291010000000500656553ac4a63c30b6a835606909c9efbae1b2597e9db020c5ecfc0642da6dc583fba4e84167539a8020000000865525353515200acffffffff990807720a5803c305b7da08a9f24b92abe343c42ac9e917a84e1f335aad785d00000000026a52ffffffff04981f20030000000001ab8c762200000000000253ab690b9605000000000151ce88b301000000000753526a6a51006500000000", + "000052ac52530000", + 1, + -1809193140, + "5299b0fb7fc16f40a5d6b337e71fcd1eb04d2600aefd22c06fe9c71fe0b0ba54" + ], + [ + "2ead28ff0243b3ab285e5d1067f0ec8724224402b21b9cef9be962a8b0d153d401be99bbee0000000004ac635153ffffffff6985987b7c1360c9fa8406dd6e0a61141709f0d5195f946da55ed83be4e3895301000000020053ffffffff016503d20500000000085251ac6a65656a6a00000000", + "51abab", + 1, + 1723793403, + "67483ee62516be17a2431a163e96fd88a08ff2ce8634a52e42c1bc04e30f3f8a" + ], + [ + "db4904e6026b6dd8d898f278c6428a176410d1ffbde75a4fa37cda12263108ccd4ca6137440100000007656a0000515263ffffffff1db7d5005c1c40da0ed17b74cf6b2a6ee2c33c9e0bacda76c0da2017dcac2fc70200000004abab6a53ffffffff0454cf2103000000000153463aef000000000009ab6a630065ab52636387e0ed050000000000e8d16f05000000000352ac63e4521b22", + "", + 1, + 1027042424, + "48315a95e49277ab6a2d561ee4626820b7bab919eea372b6bf4e9931ab221d04" + ], + [ + "dca31ad10461ead74751e83d9a81dcee08db778d3d79ad9a6d079cfdb93919ac1b0b61871102000000086500525365ab51ac7f7e9aed78e1ef8d213d40a1c50145403d196019985c837ffe83836222fe3e5955e177e70100000006525152525300ffffffff5e98482883cc08a6fe946f674cca479822f0576a43bf4113de9cbf414ca628060100000006ac53516a5253ffffffff07490b0b898198ec16c23b75d606e14fa16aa3107ef9818594f72d5776805ec502000000036a0052ffffffff01932a2803000000000865ab6551ac6a516a2687aa06", + "635300ac", + 2, + -1880362326, + "74d6a2fa7866fd8b74b2e34693e2d6fd690410384b7afdcd6461b1ae71d265ce" + ], + [ + "e14e1a9f0442ab44dfc5f6d945ad1ff8a376bc966aad5515421e96ddbe49e529614995cafc03000000055165515165fffffffff97582b8290e5a5cfeb2b0f018882dbe1b43f60b7f45e4dd21dbd3a8b0cfca3b0200000000daa267726fe075db282d694b9fee7d6216d17a8c1f00b2229085495c5dc5b260c8f8cd5d000000000363ac6affffffffaab083d22d0465471c896a438c6ac3abf4d383ae79420617a8e0ba8b9baa872b010000000963526563ac5363ababd948b5ce022113440200000000076a636552006a53229017040000000000e6f62ac8", + "526353636a65", + 3, + -485265025, + "1bc8ad76f9b7c366c5d052dc479d6a8a2015566d3a42e93ab12f727692c89d65" + ], + [ + "720d4693025ca3d347360e219e9bc746ef8f7bc88e8795162e5e2f0b0fc99dc17116fc937100000000046353520045cb1fd79824a100d30b6946eab9b219daea2b0cdca6c86367c0c36af98f19ac64f3575002000000008a1c881003ed16f3050000000008536a63630000abac45e0e704000000000151f6551a05000000000963536565515363abab00000000", + "6553ab6a6a510000ab", + 1, + 1249091393, + "a575fa4f59a8e90cd07de012c78fe8f981183bb170b9c50fcc292b8c164cbc3b" + ], + [ + "69df842a04c1410bfca10896467ce664cfa31c681a5dac10106b34d4b9d4d6d0dc1eac01c1000000000551536a5165269835ca4ad7268667b16d0a2df154ec81e304290d5ed69e0069b43f8c89e673328005e200000000076a5153006aacabffffffffc9314bd80b176488f3d634360fcba90c3a659e74a52e100ac91d3897072e3509010000000765abac51636363ffffffff0e0768b13f10f0fbd2fa3f68e4b4841809b3b5ba0e53987c3aaffcf09eee12bf0300000008ac535263526a53ac514f4c2402da8fab0400000000001ef15201000000000451526a52d0ec9aca", + "525365ac52", + 1, + 313967049, + "a72a760b361af41832d2c667c7488dc9702091918d11e344afc234a4aea3ec44" + ], + [ + "adf2340d03af5c589cb5d28c06635ac07dd0757b884d4777ba85a6a7c410408ad5efa8b19001000000045100ab00ffffffff808dc0231c96e6667c04786865727013922bcb7db20739b686f0c17f5ba70e8f0300000000fd2332a654b580881a5e2bfec8313f5aa878ae94312f37441bf2d226e7fc953dcf0c77ab000000000163aa73dc580412f8c2050000000005636aacac63da02d502000000000153e74b52020000000001536b293d030000000009636552ababacab526500000000", + "000052ab52ababab", + 0, + -568651175, + "2c45d021db545df7167ac03c9ee56473f2398d9b2b739cf3ff3e074501d324f8" + ], + [ + "e4fec9f10378a95199c1dd23c6228732c9de0d7997bf1c83918a5cfd36012476c0c3cba24002000000085165536500ac0000ad08ab93fb49d77d12a7ccdbb596bc5110876451b53a79fdce43104ff1c316ad63501de801000000046a6352ab76af9908463444aeecd32516a04dd5803e02680ed7f16307242a794024d93287595250f4000000000089807279041a82e603000000000200521429100200000000055253636a63f20b940400000000004049ed04000000000500ab5265ab43dfaf7d", + "6563526aac", + 2, + -1923470368, + "32f3c012eca9a823bebb9b282240aec40ca65df9f38da43b1dcfa0cac0c0df7e" + ], + [ + "4000d3600100b7a3ff5b41ec8d6ccdc8b2775ad034765bad505192f05d1f55d2bc39d0cbe10100000007ab5165ac6a5163ffffffff034949150100000000026a6a92c9f6000000000008ab6553ab6aab635200e697040000000007636a5353525365237ae7d2", + "52000063", + 0, + -880046683, + "c76146f68f43037289aaeb2bacf47408cddc0fb326b350eb4f5ef6f0f8564793" + ], + [ + "eabc0aa701fe489c0e4e6222d72b52f083166b49d63ad1410fb98caed027b6a71c02ab830c03000000075253ab63530065ffffffff01a5dc0b05000000000253533e820177", + "", + 0, + 954499283, + "1d849b92eedb9bf26bd4ced52ce9cb0595164295b0526842ab1096001fcd31b1" + ], + [ + "d48d55d304aad0139783b44789a771539d052db565379f668def5084daba0dfd348f7dcf6b00000000006826f59e5ffba0dd0ccbac89c1e2d69a346531d7f995dea2ca6d7e6d9225d81aec257c6003000000096a655200ac656552acffffffffa188ffbd5365cae844c8e0dea6213c4d1b2407274ae287b769ab0bf293e049eb0300000005ac6a6aab51ad1c407c5b116ca8f65ed496b476183f85f072c5f8a0193a4273e2015b1cc288bf03e9e2030000000252abffffffff04076f44040000000006655353abab53be6500050000000003ac65ac3c15040500000000095100ab536353516a52ed3aba04000000000900ac53ab53636aabac00000000", + "5253526563acac", + 2, + -1506108646, + "bbee17c8582514744bab5df50012c94b0db4aff5984d2e13a8d09421674404e2" + ], + [ + "9746f45b039bfe723258fdb6be77eb85917af808211eb9d43b15475ee0b01253d33fc3bfc502000000065163006a655312b12562dc9c54e11299210266428632a7d0ee31d04dfc7375dcad2da6e9c11947ced0e000000000009074095a5ac4df057554566dd04740c61490e1d3826000ad9d8f777a93373c8dddc4918a00000000025351ffffffff01287564030000000004636a00ab00000000", + "52", + 2, + -1380411075, + "84af1623366c4db68d81f452b86346832344734492b9c23fbb89015e516c60b2" + ], + [ + "8731b64903d735ba16da64af537eaf487b57d73977f390baac57c7b567cb2770dfa2ef65870100000001635aedd990c42645482340eacb0bfa4a0a9e888057389c728b5b6a8691cdeb1a6a67b45e140200000008ac53526a52516551ffffffff45c4f567c47b8d999916fd49642cbc5d10d43c304b99e32d044d35091679cb860100000003006a51ffffffff0176d6c200000000000000000000", + "ab6a65ab53", + 2, + -1221546710, + "ccfdba36d9445f4451fb7cbf0752cc89c23d4fc6fff0f3930d20e116f9db0b95" + ], + [ + "f5cfc52f016209ab1385e890c2865a74e93076595d1ca77cbe8fbf2022a2f2061a90fb0f3e010000000253acffffffff027de73f0200000000085252ac510052acac49cd6a020000000000e6c2cb56", + "516552535300ab63", + 0, + -1195302704, + "5532717402a2da01a1da912d824964024185ca7e8d4ad1748659dc393a14182b" + ], + [ + "df0a32ae01c4672fd1abd0b2623aae0a1a8256028df57e532f9a472d1a9ceb194267b6ee190200000009536a6a51516a525251b545f9e803469a2302000000000465526500810631040000000000441f5b050000000006530051006aaceb183c76", + "536a635252ac6a", + 0, + 1601138113, + "9a0435996cc58bdba09643927fe48c1fc908d491a050abbef8daec87f323c58f" + ], + [ + "d102d10c028b9c721abb259fe70bc68962f6cae384dabd77477c59cbeb1fb26266e091ba3e0100000002516affffffffe8d7305a74f43e30c772109849f4cd6fb867c7216e6d92e27605e69a0818899700000000026a65ecf82d58027db4620500000000026552c28ed3010000000001ab00000000", + "0051ab515365", + 1, + -131815460, + "1d1757a782cb5860302128bcbe9398243124a2f82d671a113f74f8e582c7a182" + ], + [ + "cef930ed01c36fcb1d62ceef931bef57098f27a77a4299904cc0cbb44504802d535fb11557010000000153ffffffff02c8657403000000000863ac655253520063d593380400000000046aab536a00000000", + "656a0051ab6365ab53", + 0, + -351313308, + "e69dba3efb5c02af2ab1087d0a990678784671f4744d01ca097d71aec14dd8e9" + ], + [ + "b1c0b71804dff30812b92eefb533ac77c4b9fdb9ab2f77120a76128d7da43ad70c20bbfb990200000002536392693e6001bc59411aebf15a3dc62a6566ec71a302141b0c730a3ecc8de5d76538b30f55010000000665535252ac514b740c6271fb9fe69fdf82bf98b459a7faa8a3b62f3af34943ad55df4881e0d93d3ce0ac0200000000c4158866eb9fb73da252102d1e64a3ce611b52e873533be43e6883137d0aaa0f63966f060000000001abffffffff04a605b604000000000851006a656a630052f49a0300000000000252515a94e1050000000009abac65ab0052abab00fd8dd002000000000651535163526a2566852d", + "ac5363", + 0, + -1718831517, + "b0dc030661783dd9939e4bf1a6dfcba809da2017e1b315a6312e5942d714cf05" + ], + [ + "6a270ee404ebc8d137cfd4bb6b92aa3702213a3139a579c1fc6f56fbc7edd9574ef17b13f30100000009ab00ab656565ababacffffffffaa65b1ab6c6d87260d9e27a472edceb7dd212483e72d90f08857abf1dbfd46d10100000000fffffffff93c4c9c84c4dbbe8a912b99a2830cfe3401aebc919041de063d660e585fc9f002000000096aabacab52ac6a53acfa6dcef3f28355a8d98eee53839455445eeee83eecd2c854e784efa53cee699dbfecaebd0100000003ab6a51ffffffff04f7d71b050000000009ac6a536aac6a6365513c37650500000000065265abab6a53fa742002000000000039ed82030000000009516aac635165ab51ab2fdabd17", + "ab535252526563", + 1, + -1326210506, + "1dec0d5eb921bf5b2df39c8576e19c38d0c17254a4a0b78ac4b5422bcc426258" + ], + [ + "3657e4260304ccdc19936e47bdf058d36167ee3d4eb145c52b224eff04c9eb5d1b4e434dfc0000000001ab58aefe57707c66328d3cceef2e6f56ab6b7465e587410c5f73555a513ace2b232793a74400000000036a006522e69d3a785b61ad41a635d59b3a06b2780a92173f85f8ed428491d0aaa436619baa9c4501000000046351abab2609629902eb7793050000000000a1b967040000000003525353a34d6192", + "516a", + 0, + -1761874713, + "0a2ff41f6d155d8d0e37cd9438f3b270df9f9214cda8e95c76d5a239ca189df2" + ], + [ + "a0eb6dc402994e493c787b45d1f946d267b09c596c5edde043e620ce3d59e95b2b5b93d43002000000096a5252526aac63ab6555694287a279e29ee491c177a801cd685b8744a2eab83824255a3bcd08fc0e3ea13fb8820000000009abab6365ab52ab0063ffffffff029e424a040000000008acab53ab516a636a23830f0400000000016adf49c1f9", + "ac0065ac6500005252", + 1, + 669294500, + "e05e3d383631a7ed1b78210c13c2eb26564e5577db7ddfcea2583c7c014091d4" + ], + [ + "6e67c0d3027701ef71082204c85ed63c700ef1400c65efb62ce3580d187fb348376a23e9710200000001655b91369d3155ba916a0bc6fe4f5d94cad461d899bb8aaac3699a755838bfc229d6828920010000000765536353526a52ffffffff04c0c792000000000005650052535372f79e000000000001527fc0ee010000000005ac5300ab65d1b3e902000000000251aba942b278", + "6a5151", + 0, + 1741407676, + "e657e2c8ec4ebc769ddd3198a83267b47d4f2a419fc737e813812acefad92ff7" + ], + [ + "8f53639901f1d643e01fc631f632b7a16e831d846a0184cdcda289b8fa7767f0c292eb221a00000000046a53abacffffffff037a2daa01000000000553ac6a6a51eac349020000000005ac526552638421b3040000000007006a005100ac63048a1492", + "ac65", + 0, + 1033685559, + "da86c260d42a692358f46893d6f91563985d86eeb9ea9e21cd38c2d8ffcfcc4d" + ], + [ + "491f99cb01bdfba1aa235e5538dac081fae9ce55f9622de483afe7e65105c2b0db75d360d200000000045251636340b60f0f041421330300000000096351ac000051636553ce2822040000000005516a00ac5180c8e40300000000025100caa8570400000000020000cfdc8da6", + "6a5100516aab655365", + 0, + -953727341, + "397c68803b7ce953666830b0221a5e2bcf897aa2ded8e36a6b76c497dcb1a2e1" + ], + [ + "b3cad3a7041c2c17d90a2cd994f6c37307753fa3635e9ef05ab8b1ff121ca11239a0902e700300000009ab635300006aac5163ffffffffcec91722c7468156dce4664f3c783afef147f0e6f80739c83b5f09d5a09a57040200000004516a6552ffffffff969d1c6daf8ef53a70b7cdf1b4102fb3240055a8eaeaed2489617cd84cfd56cf020000000352ab53ffffffff46598b6579494a77b593681c33422a99559b9993d77ca2fa97833508b0c169f80200000009655300655365516351ffffffff04d7ddf800000000000853536a65ac6351ab09f3420300000000056aab65abac33589d04000000000952656a65655151acac944d6f0400000000006a8004ba", + "005165", + 1, + 1035865506, + "fe1dc9e8554deecf8f50c417c670b839cc9d650722ebaaf36572418756075d58" + ], + [ + "e1cfd73b0125add9e9d699f5a45dca458355af175a7bd4486ebef28f1928d87864384d02df02000000036a0051ffffffff0357df030100000000036a5365777e2d04000000000763ab6a00005265f434a601000000000351655100000000", + "ab53ab", + 0, + -1936500914, + "950f4b4f72ccdf8a6a0f381265d6c8842fdb7e8b3df3e9742905f643b2432b69" + ], + [ + "cf781855040a755f5ba85eef93837236b34a5d3daeb2dbbdcf58bb811828d806ed05754ab8010000000351ac53ffffffffda1e264727cf55c67f06ebcc56dfe7fa12ac2a994fecd0180ce09ee15c480f7d00000000096351516a51acac00ab53dd49ff9f334befd6d6f87f1a832cddfd826a90b78fd8cf19a52cb8287788af94e939d6020000000700525251ac526310d54a7e8900ed633f0f6f0841145aae7ee0cbbb1e2a0cae724ee4558dbabfdc58ba6855010000000552536a53abfd1b101102c51f910500000000096300656a525252656a300bee010000000009ac52005263635151abe19235c9", + "53005365", + 2, + 1422854188, + "d5981bd4467817c1330da72ddb8760d6c2556cd809264b2d85e6d274609fc3a3" + ], + [ + "fea256ce01272d125e577c0a09570a71366898280dda279b021000db1325f27edda41a53460100000002ab53c752c21c013c2b3a01000000000000000000", + "65", + 0, + 1145543262, + "076b9f844f6ae429de228a2c337c704df1652c292b6c6494882190638dad9efd" + ] +] diff --git a/pkg/txscript/data/tx_invalid.json b/pkg/txscript/data/tx_invalid.json new file mode 100755 index 0000000..b75f670 --- /dev/null +++ b/pkg/txscript/data/tx_invalid.json @@ -0,0 +1,1221 @@ +[ + [ + "The following are deserialized transactions which are invalid." + ], + [ + "They are in the form" + ], + [ + "[[[prevout hash, prevout index, prevout scriptPubKey, amount?], [input 2], ...]," + ], + [ + "serializedTransaction, verifyFlags]" + ], + [ + "Objects that are only a single string (like this one) are ignored" + ], + [ + "0e1b5688cf179cd9f7cbda1fac0090f6e684bbf8cd946660120197c3f3681809 but with extra junk appended to the end of the scriptPubKey" + ], + [ + [ + [ + "6ca7ec7b1847f6bdbd737176050e6a08d66ccd55bb94ad24f4018024107a5827", + 0, + "0x41 0x043b640e983c9690a14c039a2037ecc3467b27a0dcd58f19d76c7bc118d09fec45adc5370a1c5bf8067ca9f5557a4cf885fdb0fe0dcc9c3a7137226106fbc779a5 CHECKSIG VERIFY 1" + ] + ], + "010000000127587a10248001f424ad94bb55cd6cd6086a0e05767173bdbdf647187beca76c000000004948304502201b822ad10d6adc1a341ae8835be3f70a25201bbff31f59cbb9c5353a5f0eca18022100ea7b2f7074e9aa9cf70aa8d0ffee13e6b45dddabf1ab961bda378bcdb778fa4701ffffffff0100f2052a010000001976a914fc50c5907d86fed474ba5ce8b12a66e0a4c139d888ac00000000", + "P2SH" + ], + [ + "This is the nearly-standard transaction with CHECKSIGVERIFY 1 instead of CHECKSIG from tx_valid.json" + ], + [ + "but with the signature duplicated in the scriptPubKey with a non-standard pushdata prefix" + ], + [ + "See FindAndDelete, which will only remove if it uses the same pushdata prefix as is standard" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "DUP HASH160 0x14 0x5b6462475454710f3c22f5fdf0b40704c92f25c3 EQUALVERIFY CHECKSIGVERIFY 1 0x4c 0x47 0x3044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a01" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "Same as above, but with the sig in the scriptSig also pushed with the same non-standard OP_PUSHDATA" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "DUP HASH160 0x14 0x5b6462475454710f3c22f5fdf0b40704c92f25c3 EQUALVERIFY CHECKSIGVERIFY 1 0x4c 0x47 0x3044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a01" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006b4c473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "This is the nearly-standard transaction with CHECKSIGVERIFY 1 instead of CHECKSIG from tx_valid.json" + ], + [ + "but with the signature duplicated in the scriptPubKey with a different hashtype suffix" + ], + [ + "See FindAndDelete, which will only remove if the signature, including the hash type, matches" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "DUP HASH160 0x14 0x5b6462475454710f3c22f5fdf0b40704c92f25c3 EQUALVERIFY CHECKSIGVERIFY 1 0x47 0x3044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a81" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "An invalid P2SH Transaction" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x7a052c840ba73af26755de42cf01cc9e0a49fef0 EQUAL" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "Tests for CheckTransaction()" + ], + [ + "No inputs" + ], + [ + "Skipped because this is not checked by btcscript, this is a problem for chain." + ], + [ + "No outputs" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x05ab9e14d983742513f0f451e105ffb4198d1dd4 EQUAL" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022100f16703104aab4e4088317c862daec83440242411b039d14280e03dd33b487ab802201318a7be236672c5c56083eb7a5a195bc57a40af7923ff8545016cd3b571e2a601232103c40e5d339df3f30bf753e7e04450ae4ef76c9e45587d1d993bdc4cd06f0651c7acffffffff0000000000", + "P2SH" + ], + [ + "Negative output" + ], + [ + "Removed because btcscript doesn't do tx sanity checking." + ], + [ + "MAX_MONEY + 1 output" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x32afac281462b822adbec5094b8d4d337dd5bd6a EQUAL" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010140075af0750700015100000000", + "P2SH" + ], + [ + "MAX_MONEY output + 1 output" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0xb558cbf4930954aa6a344363a15668d7477ae716 EQUAL" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510001000000000000015100000000", + "P2SH" + ], + [ + "Duplicate inputs" + ], + [ + "Removed because btcscript doesn't check input duplication, btcchain does" + ], + [ + "Coinbase of size 1" + ], + [ + "Note the input is just required to make the tester happy" + ], + [ + "Removed because btcscript doesn't handle coinbase checking, btcchain does" + ], + [ + "Coinbase of size 101" + ], + [ + "Note the input is just required to make the tester happy" + ], + [ + "Removed because btcscript doesn't handle coinbase checking, btcchain does" + ], + [ + "Null txin" + ], + [ + "Removed because btcscript doesn't do tx sanity checking." + ], + [ + "Same as the transactions in valid with one input SIGHASH_ALL and one SIGHASH_ANYONECANPAY, but we set the _ANYONECANPAY sequence number, invalidating the SIGHASH_ALL signature" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x21 0x035e7f0d4d0841bcd56c39337ed086b1a633ee770c1ffdd94ac552a95ac2ce0efc CHECKSIG" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000200", + 0, + "0x21 0x035e7f0d4d0841bcd56c39337ed086b1a633ee770c1ffdd94ac552a95ac2ce0efc CHECKSIG" + ] + ], + "01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df10101000000000200000000000000000000000000000000000000000000000000000000000000000000484730440220201dc2d030e380e8f9cfb41b442d930fa5a685bb2c8db5906671f865507d0670022018d9e7a8d4c8d86a73c2a724ee38ef983ec249827e0e464841735955c707ece98101000000010100000000000000015100000000", + "P2SH" + ], + [ + "CHECKMULTISIG with incorrect signature order" + ], + [ + "Note the input is just required to make the tester happy" + ], + [ + [ + [ + "b3da01dd4aae683c7aee4d5d8b52a540a508e1115f77cd7fa9a291243f501223", + 0, + "HASH160 0x14 0xb1ce99298d5f07364b57b1e5c9cc00be0b04a954 EQUAL" + ] + ], + "01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe000048304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f401483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000", + "P2SH" + ], + [ + "The following is a tweaked form of 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63" + ], + [ + "It is an OP_CHECKMULTISIG with the dummy value missing" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004847304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH" + ], + [ + "CHECKMULTISIG SCRIPT_VERIFY_NULLDUMMY tests:" + ], + [ + "The following is a tweaked form of 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63" + ], + [ + "It is an OP_CHECKMULTISIG with the dummy value set to something other than an empty string" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a010047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH,NULLDUMMY" + ], + [ + "As above, but using a OP_1" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH,NULLDUMMY" + ], + [ + "As above, but using a OP_1NEGATE" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH,NULLDUMMY" + ], + [ + "As above, but with the dummy byte missing" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004847304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH,NULLDUMMY" + ], + [ + "Empty stack when we try to run CHECKSIG" + ], + [ + [ + [ + "ad503f72c18df5801ee64d76090afe4c607fb2b822e9b7b63c5826c50e22fc3b", + 0, + "0x21 0x027c3a97665bf283a102a587a62a30a0c102d4d3b141015e2cae6f64e2543113e5 CHECKSIG NOT" + ] + ], + "01000000013bfc220ec526583cb6b7e922b8b27f604cfe0a09764de61e80f58dc1723f50ad0000000000ffffffff0101000000000000002321027c3a97665bf283a102a587a62a30a0c102d4d3b141015e2cae6f64e2543113e5ac00000000", + "P2SH" + ], + [ + "Inverted versions of tx_valid CODESEPARATOR IF block tests" + ], + [ + "CODESEPARATOR in an unexecuted IF block does not change what is hashed" + ], + [ + [ + [ + "a955032f4d6b0c9bfe8cad8f00a8933790b9c1dc28c82e0f48e75b35da0e4944", + 0, + "IF CODESEPARATOR ENDIF 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 CHECKSIGVERIFY CODESEPARATOR 1" + ] + ], + "010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0151ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "As above, with the IF block executed" + ], + [ + [ + [ + "a955032f4d6b0c9bfe8cad8f00a8933790b9c1dc28c82e0f48e75b35da0e4944", + 0, + "IF CODESEPARATOR ENDIF 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 CHECKSIGVERIFY CODESEPARATOR 1" + ] + ], + "010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510100ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "CHECKLOCKTIMEVERIFY tests" + ], + [ + "By-height locks, with argument just beyond tx nLockTime" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "499999999 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000fe64cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "By-time locks, with argument just beyond tx nLockTime (but within numerical boundaries)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "500000001 CHECKLOCKTIMEVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Argument missing" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000001b1010000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Argument negative with by-blockheight nLockTime=0" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "-1 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Argument negative with by-blocktime nLockTime=500,000,000" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "-1 CHECKLOCKTIMEVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000004005194b1010000000100000000000000000002000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Input locked" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1ffffffff0100000000000000000002000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Another input being unlocked isn't sufficient; the CHECKLOCKTIMEVERIFY-using input must be unlocked" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKLOCKTIMEVERIFY 1" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000200", + 1, + "1" + ] + ], + "010000000200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00020000000000000000000000000000000000000000000000000000000000000100000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Argument/tx height/time mismatch, both versions" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKLOCKTIMEVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b100000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "499999999 CHECKLOCKTIMEVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "500000000 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "500000000 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Argument 2^32 with nLockTime=2^32-1" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967296 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Same, but with nLockTime=2^31-1" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffff7f", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "6 byte non-minimally-encoded arguments are invalid even if their contents are valid" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x06 0x000000000000 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Failure due to failing CHECKLOCKTIMEVERIFY in scriptSig" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Failure due to failing CHECKLOCKTIMEVERIFY in redeemScript" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0xc5b93064159b3b2d6ab506a41b1f50463771b988 EQUAL" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "A transaction with a non-standard DER signature." + ], + [ + [ + [ + "b1dbc81696c8a9c0fccd0693ab66d7c368dbc38c0def4e800685560ddd1b2132", + 0, + "DUP HASH160 0x14 0x4b3bd7eba3bc0284fd3007be7f3be275e94f5826 EQUALVERIFY CHECKSIG" + ] + ], + "010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000", + "P2SH,DERSIG" + ], + [ + "CHECKSEQUENCEVERIFY tests" + ], + [ + "By-height locks, with argument just beyond txin.nSequence" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4259839 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feff40000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "By-time locks, with argument just beyond txin.nSequence (but within numerical boundries)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194305 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4259839 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feff40000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument missing" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument negative with by-blockheight txin.nSequence=0" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "-1 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument negative with by-blocktime txin.nSequence=CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "-1 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument/tx height/time mismatch, both versions" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "65535 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194304 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4259839 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "6 byte non-minimally-encoded arguments are invalid even if their contents are valid" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x06 0x000000000000 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Failure due to failing CHECKSEQUENCEVERIFY in scriptSig" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1" + ] + ], + "02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Failure due to failing CHECKSEQUENCEVERIFY in redeemScript" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x7c17aff532f22beb54069942f9bf567a66133eaf EQUAL" + ] + ], + "0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Failure due to insufficient tx.nVersion (<2)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKSEQUENCEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194304 CHECKSEQUENCEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Unknown witness program version (with DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x60 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" + ], + [ + "Unknown length for witness program v0" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x15 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3fff", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash Single|AnyoneCanPay (same index output value changed)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e80300000000000001516c070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash None|AnyoneCanPay (input sequence changed)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff000100000000000000000000000000000000000000000000000000000000000001000000000100000000010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash All|AnyoneCanPay (third output value changed)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151540b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with a push of 521 bytes" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x20 0x33198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349", + 1000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd0902000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000", + "P2SH,WITNESS" + ], + [ + "Witness with unknown version which push false on the stack should be invalid (even without DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x02 0x0000", + 2000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015101010100000000", + "P2SH,WITNESS" + ], + [ + "Witness program should leave clean stack" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x20 0x2f04a3aa051f1f60d695f6c44c0c3d383973dfd446ace8962664a76bb10e31a8", + 2000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01000000000000000001510102515100000000", + "P2SH,WITNESS" + ], + [ + "Witness v0 with a push of 2 bytes" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x02 0x0001", + 2000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015101040002000100000000", + "P2SH,WITNESS" + ], + [ + "Unknown witness version with non empty scriptSig" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x02 0x0001", + 2000 + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000151ffffffff010000000000000000015100000000", + "P2SH,WITNESS" + ], + [ + "Non witness Single|AnyoneCanPay hash input's position (permutation)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x21 0x03596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71 CHECKSIG", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x21 0x03596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71 CHECKSIG", + 1001 + ] + ], + "010000000200010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff0001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff02e9030000000000000151e803000000000000015100000000", + "P2SH,WITNESS" + ], + [ + "P2WSH with a redeem representing a witness scriptPubKey should fail" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x20 0x34b6c399093e06cf9f0f7f660a1abcfe78fcf7b576f43993208edd9518a0ae9b", + 1000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0001045102010100000000", + "P2SH,WITNESS" + ], + [ + "33 bytes push should be considered a witness scriptPubKey" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x21 0xff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbff", + 1000 + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", + "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" + ], + [ + "FindAndDelete tests" + ], + [ + "This is a test of FindAndDelete. The first tx is a spend of normal scriptPubKey and the second tx is a spend of bare P2WSH." + ], + [ + "The redeemScript/witnessScript is CHECKSIGVERIFY <0x30450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01>." + ], + [ + "The signature is <0x30450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01> ," + ], + [ + "where the pubkey is obtained through key recovery with sig and the wrong sighash." + ], + [ + "This is to show that FindAndDelete is applied only to non-segwit scripts" + ], + [ + "To show that the tests are 'correctly wrong', they should pass by modifying OP_CHECKSIG under interpreter.cpp" + ], + [ + "by replacing (sigversion == SIGVERSION_BASE) with (sigversion != SIGVERSION_BASE)" + ], + [ + "Non-segwit: wrong sighash (without FindAndDelete) = 1ba1fe3bc90c5d1265460e684ce6774e324f0fabdf67619eda729e64e8b6bc08" + ], + [ + [ + [ + "f18783ace138abac5d3a7a5cf08e88fe6912f267ef936452e0c27d090621c169", + 7000, + "HASH160 0x14 0x0c746489e2d83cdbb5b90b432773342ba809c134 EQUAL", + 200000 + ] + ], + "010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012103b12a1ec8428fc74166926318c15e17408fea82dbb157575e16a8c365f546248f4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000", + "P2SH,WITNESS" + ], + [ + "BIP143: wrong sighash (with FindAndDelete) = 71c9cd9b2869b9c70b01b1f0360c148f42dee72297db312638df136f43311f23" + ], + [ + [ + [ + "f18783ace138abac5d3a7a5cf08e88fe6912f267ef936452e0c27d090621c169", + 7500, + "0x00 0x20 0x9e1be07558ea5cc8e02ed1d80c0911048afad949affa36d5c3951e3159dbea19", + 200000 + ] + ], + "0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9d7ed6e161f0e255c10bbfcca0128a9e2035c2c8da58899c54d22d3a31afdef4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000", + "P2SH,WITNESS" + ], + [ + "This is multisig version of the FindAndDelete tests" + ], + [ + "Script is 2 CHECKMULTISIGVERIFY DROP" + ], + [ + "52af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175" + ], + [ + "Signature is 0 2 " + ], + [ + "Should pass by replacing (sigversion == SIGVERSION_BASE) with (sigversion != SIGVERSION_BASE) under OP_CHECKMULTISIG" + ], + [ + "Non-segwit: wrong sighash (without FindAndDelete) = 4bc6a53e8e16ef508c19e38bba08831daba85228b0211f323d4cb0999cf2a5e8" + ], + [ + [ + [ + "9628667ad48219a169b41b020800162287d2c0f713c04157e95c484a8dcb7592", + 7000, + "HASH160 0x14 0x5748407f5ca5cdca53ba30b79040260770c9ee1b EQUAL", + 200000 + ] + ], + "01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596015221023fd5dd42b44769c5653cbc5947ff30ab8871f240ad0c0e7432aefe84b5b4ff3421039d52178dbde360b83f19cf348deb04fa8360e1bf5634577be8e50fafc2b0e4ef4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000", + "P2SH,WITNESS" + ], + [ + "BIP143: wrong sighash (with FindAndDelete) = 17c50ec2181ecdfdc85ca081174b248199ba81fff730794d4f69b8ec031f2dce" + ], + [ + [ + [ + "9628667ad48219a169b41b020800162287d2c0f713c04157e95c484a8dcb7592", + 7500, + "0x00 0x20 0x9b66c15b4e0b4eb49fa877982cafded24859fe5b0e2dbfbe4f0df1de7743fd52", + 200000 + ] + ], + "010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601010221023cb6055f4b57a1580c5a753e19610cafaedf7e0ff377731c77837fd666eae1712102c1b1db303ac232ffa8e5e7cc2cf5f96c6e40d3e6914061204c0541cb2043a0969552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000", + "P2SH,WITNESS" + ], + [ + "Make diffs cleaner by leaving a comment here without comma at the end" + ] +] diff --git a/pkg/txscript/data/tx_valid.json b/pkg/txscript/data/tx_valid.json new file mode 100755 index 0000000..914c690 --- /dev/null +++ b/pkg/txscript/data/tx_valid.json @@ -0,0 +1,2020 @@ +[ + [ + "The following are deserialized transactions which are valid." + ], + [ + "They are in the form" + ], + [ + "[[[prevout hash, prevout index, prevout scriptPubKey, amount?], [input 2], ...]," + ], + [ + "serializedTransaction, verifyFlags]" + ], + [ + "Objects that are only a single string (like this one) are ignored" + ], + [ + "The following is 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63" + ], + [ + "It is of particular interest because it contains an invalidly-encoded signature which OpenSSL accepts" + ], + [ + "See http://r6.ca/blog/20111119T211504Z.html" + ], + [ + "It is also the first OP_CHECKMULTISIG transaction in standard form" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000490047304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH" + ], + [ + "The following is a tweaked form of 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63" + ], + [ + "It is an OP_CHECKMULTISIG with an arbitrary extra byte stuffed into the signature at pos length - 2" + ], + [ + "The dummy byte is fine however, so the NULLDUMMY flag should be happy" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a0048304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2bab01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH,NULLDUMMY" + ], + [ + "The following is a tweaked form of 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63" + ], + [ + "It is an OP_CHECKMULTISIG with the dummy value set to something other than an empty string" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba260000000004a01ff47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH" + ], + [ + "As above, but using a OP_1" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000495147304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH" + ], + [ + "As above, but using a OP_1NEGATE" + ], + [ + [ + [ + "60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1", + 0, + "1 0x41 0x04cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4 0x41 0x0461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af 2 OP_CHECKMULTISIG" + ] + ], + "0100000001b14bdcbc3e01bdaad36cc08e81e69c82e1060bc14e518db2b49aa43ad90ba26000000000494f47304402203f16c6f40162ab686621ef3000b04e75418a0c0cb2d8aebeac894ae360ac1e780220ddc15ecdfc3507ac48e1681a33eb60996631bf6bf5bc0a0682c4db743ce7ca2b01ffffffff0140420f00000000001976a914660d4ef3a743e3e696ad990364e555c271ad504b88ac00000000", + "P2SH" + ], + [ + "The following is c99c49da4c38af669dea436d3e73780dfdb6c1ecf9958baa52960e8baee30e73" + ], + [ + "It is of interest because it contains a 0-sequence as well as a signature of SIGHASH type 0 (which is not a real type)" + ], + [ + [ + [ + "406b2b06bcd34d3c8733e6b79f7a394c8a431fbf4ff5ac705c93f4076bb77602", + 0, + "DUP HASH160 0x14 0xdc44b1164188067c3a32d4780f5996fa14a4f2d9 EQUALVERIFY CHECKSIG" + ] + ], + "01000000010276b76b07f4935c70acf54fbf1f438a4c397a9fb7e633873c4dd3bc062b6b40000000008c493046022100d23459d03ed7e9511a47d13292d3430a04627de6235b6e51a40f9cd386f2abe3022100e7d25b080f0bb8d8d5f878bba7d54ad2fda650ea8d158a33ee3cbd11768191fd004104b0e2c879e4daf7b9ab68350228c159766676a14f5815084ba166432aab46198d4cca98fa3e9981d0a90b2effc514b76279476550ba3663fdcaff94c38420e9d5000000000100093d00000000001976a9149a7b0f3b80c6baaeedce0a0842553800f832ba1f88ac00000000", + "P2SH" + ], + [ + "A nearly-standard transaction with CHECKSIGVERIFY 1 instead of CHECKSIG" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "DUP HASH160 0x14 0x5b6462475454710f3c22f5fdf0b40704c92f25c3 EQUALVERIFY CHECKSIGVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "Same as above, but with the signature duplicated in the scriptPubKey with the proper pushdata prefix" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "DUP HASH160 0x14 0x5b6462475454710f3c22f5fdf0b40704c92f25c3 EQUALVERIFY CHECKSIGVERIFY 1 0x47 0x3044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a01" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006a473044022067288ea50aa799543a536ff9306f8e1cba05b9c6b10951175b924f96732555ed022026d7b5265f38d21541519e4a1e55044d5b9e17e15cdbaf29ae3792e99e883e7a012103ba8c8b86dea131c22ab967e6dd99bdae8eff7a1f75a2c35f1f944109e3fe5e22ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "The following is f7fdd091fa6d8f5e7a8c2458f5c38faffff2d3f1406b6e4fe2c99dcc0d2d1cbb" + ], + [ + "It caught a bug in the workaround for 23b397edccd3740a74adb603c9756370fafcde9bcc4483eb271ecad09a94dd63 in an overly simple implementation" + ], + [ + [ + [ + "b464e85df2a238416f8bdae11d120add610380ea07f4ef19c5f9dfd472f96c3d", + 0, + "DUP HASH160 0x14 0xbef80ecf3a44500fda1bc92176e442891662aed2 EQUALVERIFY CHECKSIG" + ], + [ + "b7978cc96e59a8b13e0865d3f95657561a7f725be952438637475920bac9eb21", + 1, + "DUP HASH160 0x14 0xbef80ecf3a44500fda1bc92176e442891662aed2 EQUALVERIFY CHECKSIG" + ] + ], + "01000000023d6cf972d4dff9c519eff407ea800361dd0a121de1da8b6f4138a2f25de864b4000000008a4730440220ffda47bfc776bcd269da4832626ac332adfca6dd835e8ecd83cd1ebe7d709b0e022049cffa1cdc102a0b56e0e04913606c70af702a1149dc3b305ab9439288fee090014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff21ebc9ba20594737864352e95b727f1a565756f9d365083eb1a8596ec98c97b7010000008a4730440220503ff10e9f1e0de731407a4a245531c9ff17676eda461f8ceeb8c06049fa2c810220c008ac34694510298fa60b3f000df01caa244f165b727d4896eb84f81e46bcc4014104266abb36d66eb4218a6dd31f09bb92cf3cfa803c7ea72c1fc80a50f919273e613f895b855fb7465ccbc8919ad1bd4a306c783f22cd3227327694c4fa4c1c439affffffff01f0da5200000000001976a914857ccd42dded6df32949d4646dfa10a92458cfaa88ac00000000", + "P2SH" + ], + [ + "The following tests for the presence of a bug in the handling of SIGHASH_SINGLE" + ], + [ + "It results in signing the constant 1, instead of something generated based on the transaction," + ], + [ + "when the input doing the signing has an index greater than the maximum output index" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000200", + 0, + "1" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "DUP HASH160 0x14 0xe52b482f2faa8ecbf0db344f93c84ac908557f33 EQUALVERIFY CHECKSIG" + ] + ], + "01000000020002000000000000000000000000000000000000000000000000000000000000000000000151ffffffff0001000000000000000000000000000000000000000000000000000000000000000000006b483045022100c9cdd08798a28af9d1baf44a6c77bcc7e279f47dc487c8c899911bc48feaffcc0220503c5c50ae3998a733263c5c0f7061b483e2b56c4c41b456e7d2f5a78a74c077032102d5c25adb51b61339d2b05315791e21bbe80ea470a49db0135720983c905aace0ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "An invalid P2SH Transaction" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x7a052c840ba73af26755de42cf01cc9e0a49fef0 EQUAL" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000009085768617420697320ffffffff010000000000000000015100000000", + "NONE" + ], + [ + "A valid P2SH Transaction using the standard transaction type put forth in BIP 16" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x8febbed40483661de6958d957412f82deed8e2f7 EQUAL" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100c66c9cdf4c43609586d15424c54707156e316d88b0a1534c9e6b0d4f311406310221009c0fe51dbc9c4ab7cc25d3fdbeccf6679fe6827f08edf2b4a9f16ee3eb0e438a0123210338e8034509af564c62644c07691942e0c056752008a173c89f60ab2a88ac2ebfacffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "Tests for CheckTransaction()" + ], + [ + "MAX_MONEY output" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x32afac281462b822adbec5094b8d4d337dd5bd6a EQUAL" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006e493046022100e1eadba00d9296c743cb6ecc703fd9ddc9b3cd12906176a226ae4c18d6b00796022100a71aef7d2874deff681ba6080f1b278bac7bb99c61b08a85f4311970ffe7f63f012321030c0588dc44d92bdcbf8e72093466766fdc265ead8db64517b0c542275b70fffbacffffffff010040075af0750700015100000000", + "P2SH" + ], + [ + "MAX_MONEY output + 0 output" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0xb558cbf4930954aa6a344363a15668d7477ae716 EQUAL" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000006d483045022027deccc14aa6668e78a8c9da3484fbcd4f9dcc9bb7d1b85146314b21b9ae4d86022100d0b43dece8cfb07348de0ca8bc5b86276fa88f7f2138381128b7c36ab2e42264012321029bb13463ddd5d2cc05da6e84e37536cb9525703cfd8f43afdb414988987a92f6acffffffff020040075af075070001510000000000000000015100000000", + "P2SH" + ], + [ + "Coinbase of size 2" + ], + [ + "Note the input is just required to make the tester happy" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000000", + -1, + "1" + ] + ], + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025151ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "Coinbase of size 100" + ], + [ + "Note the input is just required to make the tester happy" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000000", + -1, + "1" + ] + ], + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff6451515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151515151ffffffff010000000000000000015100000000", + "P2SH" + ], + [ + "Simple transaction with first input is signed with SIGHASH_ALL, second with SIGHASH_ANYONECANPAY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x21 0x035e7f0d4d0841bcd56c39337ed086b1a633ee770c1ffdd94ac552a95ac2ce0efc CHECKSIG" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000200", + 0, + "0x21 0x035e7f0d4d0841bcd56c39337ed086b1a633ee770c1ffdd94ac552a95ac2ce0efc CHECKSIG" + ] + ], + "010000000200010000000000000000000000000000000000000000000000000000000000000000000049483045022100d180fd2eb9140aeb4210c9204d3f358766eb53842b2a9473db687fa24b12a3cc022079781799cd4f038b85135bbe49ec2b57f306b2bb17101b17f71f000fcab2b6fb01ffffffff0002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000", + "P2SH" + ], + [ + "Same as above, but we change the sequence number of the first input to check that SIGHASH_ANYONECANPAY is being followed" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x21 0x035e7f0d4d0841bcd56c39337ed086b1a633ee770c1ffdd94ac552a95ac2ce0efc CHECKSIG" + ], + [ + "0000000000000000000000000000000000000000000000000000000000000200", + 0, + "0x21 0x035e7f0d4d0841bcd56c39337ed086b1a633ee770c1ffdd94ac552a95ac2ce0efc CHECKSIG" + ] + ], + "01000000020001000000000000000000000000000000000000000000000000000000000000000000004948304502203a0f5f0e1f2bdbcd04db3061d18f3af70e07f4f467cbc1b8116f267025f5360b022100c792b6e215afc5afc721a351ec413e714305cb749aae3d7fee76621313418df101010000000002000000000000000000000000000000000000000000000000000000000000000000004847304402205f7530653eea9b38699e476320ab135b74771e1c48b81a5d041e2ca84b9be7a802200ac8d1f40fb026674fe5a5edd3dea715c27baa9baca51ed45ea750ac9dc0a55e81ffffffff010100000000000000015100000000", + "P2SH" + ], + [ + "afd9c17f8913577ec3509520bd6e5d63e9c0fd2a5f70c787993b097ba6ca9fae which has several SIGHASH_SINGLE signatures" + ], + [ + [ + [ + "63cfa5a09dc540bf63e53713b82d9ea3692ca97cd608c384f2aa88e51a0aac70", + 0, + "DUP HASH160 0x14 0xdcf72c4fd02f5a987cf9b02f2fabfcac3341a87d EQUALVERIFY CHECKSIG" + ], + [ + "04e8d0fcf3846c6734477b98f0f3d4badfb78f020ee097a0be5fe347645b817d", + 1, + "DUP HASH160 0x14 0xdcf72c4fd02f5a987cf9b02f2fabfcac3341a87d EQUALVERIFY CHECKSIG" + ], + [ + "ee1377aff5d0579909e11782e1d2f5f7b84d26537be7f5516dd4e43373091f3f", + 1, + "DUP HASH160 0x14 0xdcf72c4fd02f5a987cf9b02f2fabfcac3341a87d EQUALVERIFY CHECKSIG" + ] + ], + "010000000370ac0a1ae588aaf284c308d67ca92c69a39e2db81337e563bf40c59da0a5cf63000000006a4730440220360d20baff382059040ba9be98947fd678fb08aab2bb0c172efa996fd8ece9b702201b4fb0de67f015c90e7ac8a193aeab486a1f587e0f54d0fb9552ef7f5ce6caec032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff7d815b6447e35fbea097e00e028fb7dfbad4f3f0987b4734676c84f3fcd0e804010000006b483045022100c714310be1e3a9ff1c5f7cacc65c2d8e781fc3a88ceb063c6153bf950650802102200b2d0979c76e12bb480da635f192cc8dc6f905380dd4ac1ff35a4f68f462fffd032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff3f1f097333e4d46d51f5e77b53264db8f7f5d2e18217e1099957d0f5af7713ee010000006c493046022100b663499ef73273a3788dea342717c2640ac43c5a1cf862c9e09b206fcb3f6bb8022100b09972e75972d9148f2bdd462e5cb69b57c1214b88fc55ca638676c07cfc10d8032103579ca2e6d107522f012cd00b52b9a65fb46f0c57b9b8b6e377c48f526a44741affffffff0380841e00000000001976a914bfb282c70c4191f45b5a6665cad1682f2c9cfdfb88ac80841e00000000001976a9149857cc07bed33a5cf12b9c5e0500b675d500c81188ace0fd1c00000000001976a91443c52850606c872403c0601e69fa34b26f62db4a88ac00000000", + "P2SH" + ], + [ + "ddc454a1c0c35c188c98976b17670f69e586d9c0f3593ea879928332f0a069e7, which spends an input that pushes using a PUSHDATA1 that is negative when read as signed" + ], + [ + [ + [ + "c5510a5dd97a25f43175af1fe649b707b1df8e1a41489bac33a23087027a2f48", + 0, + "0x4c 0xae 0x606563686f2022553246736447566b58312b5a536e587574356542793066794778625456415675534a6c376a6a334878416945325364667657734f53474f36633338584d7439435c6e543249584967306a486956304f376e775236644546673d3d22203e20743b206f70656e73736c20656e63202d7061737320706173733a5b314a564d7751432d707269766b65792d6865785d202d64202d6165732d3235362d636263202d61202d696e207460 DROP DUP HASH160 0x14 0xbfd7436b6265aa9de506f8a994f881ff08cc2872 EQUALVERIFY CHECKSIG" + ] + ], + "0100000001482f7a028730a233ac9b48411a8edfb107b749e61faf7531f4257ad95d0a51c5000000008b483045022100bf0bbae9bde51ad2b222e87fbf67530fbafc25c903519a1e5dcc52a32ff5844e022028c4d9ad49b006dd59974372a54291d5764be541574bb0c4dc208ec51f80b7190141049dd4aad62741dc27d5f267f7b70682eee22e7e9c1923b9c0957bdae0b96374569b460eb8d5b40d972e8c7c0ad441de3d94c4a29864b212d56050acb980b72b2bffffffff0180969800000000001976a914e336d0017a9d28de99d16472f6ca6d5a3a8ebc9988ac00000000", + "P2SH" + ], + [ + "Correct signature order" + ], + [ + "Note the input is just required to make the tester happy" + ], + [ + [ + [ + "b3da01dd4aae683c7aee4d5d8b52a540a508e1115f77cd7fa9a291243f501223", + 0, + "HASH160 0x14 0xb1ce99298d5f07364b57b1e5c9cc00be0b04a954 EQUAL" + ] + ], + "01000000012312503f2491a2a97fcd775f11e108a540a5528b5d4dee7a3c68ae4add01dab300000000fdfe0000483045022100f6649b0eddfdfd4ad55426663385090d51ee86c3481bdc6b0c18ea6c0ece2c0b0220561c315b07cffa6f7dd9df96dbae9200c2dee09bf93cc35ca05e6cdf613340aa0148304502207aacee820e08b0b174e248abd8d7a34ed63b5da3abedb99934df9fddd65c05c4022100dfe87896ab5ee3df476c2655f9fbe5bd089dccbef3e4ea05b5d121169fe7f5f4014c695221031d11db38972b712a9fe1fc023577c7ae3ddb4a3004187d41c45121eecfdbb5b7210207ec36911b6ad2382860d32989c7b8728e9489d7bbc94a6b5509ef0029be128821024ea9fac06f666a4adc3fc1357b7bec1fd0bdece2b9d08579226a8ebde53058e453aeffffffff0180380100000000001976a914c9b99cddf847d10685a4fabaa0baf505f7c3dfab88ac00000000", + "P2SH" + ], + [ + "cc60b1f899ec0a69b7c3f25ddf32c4524096a9c5b01cbd84c6d0312a0c478984, which is a fairly strange transaction which relies on OP_CHECKSIG returning 0 when checking a completely invalid sig of length 0" + ], + [ + [ + [ + "cbebc4da731e8995fe97f6fadcd731b36ad40e5ecb31e38e904f6e5982fa09f7", + 0, + "0x2102085c6600657566acc2d6382a47bc3f324008d2aa10940dd7705a48aa2a5a5e33ac7c2103f5d0fb955f95dd6be6115ce85661db412ec6a08abcbfce7da0ba8297c6cc0ec4ac7c5379a820d68df9e32a147cffa36193c6f7c43a1c8c69cda530e1c6db354bfabdcfefaf3c875379a820f531f3041d3136701ea09067c53e7159c8f9b2746a56c3d82966c54bbc553226879a5479827701200122a59a5379827701200122a59a6353798277537982778779679a68" + ] + ], + "0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000", + "P2SH" + ], + [ + "Empty pubkey" + ], + [ + [ + [ + "229257c295e7f555421c1bfec8538dd30a4b5c37c1c8810bbe83cafa7811652c", + 0, + "0x00 CHECKSIG NOT" + ] + ], + "01000000012c651178faca83be0b81c8c1375c4b0ad38d53c8fe1b1c4255f5e795c25792220000000049483045022100d6044562284ac76c985018fc4a90127847708c9edb280996c507b28babdc4b2a02203d74eca3f1a4d1eea7ff77b528fde6d5dc324ec2dbfdb964ba885f643b9704cd01ffffffff010100000000000000232102c2410f8891ae918cab4ffc4bb4a3b0881be67c7a1e7faa8b5acf9ab8932ec30cac00000000", + "P2SH" + ], + [ + "Empty signature" + ], + [ + [ + [ + "9ca93cfd8e3806b9d9e2ba1cf64e3cc6946ee0119670b1796a09928d14ea25f7", + 0, + "0x21 0x028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02 CHECKSIG NOT" + ] + ], + "0100000001f725ea148d92096a79b1709611e06e94c63c4ef61cbae2d9b906388efd3ca99c000000000100ffffffff0101000000000000002321028a1d66975dbdf97897e3a4aef450ebeb5b5293e4a0b4a6d3a2daaa0b2b110e02ac00000000", + "P2SH" + ], + [ + [ + [ + "444e00ed7840d41f20ecd9c11d3f91982326c731a02f3c05748414a4fa9e59be", + 0, + "1 0x00 0x21 0x02136b04758b0b6e363e7a6fbe83aaf527a153db2b060d36cc29f7f8309ba6e458 2 CHECKMULTISIG" + ] + ], + "0100000001be599efaa4148474053c2fa031c7262398913f1dc1d9ec201fd44078ed004e44000000004900473044022022b29706cb2ed9ef0cb3c97b72677ca2dfd7b4160f7b4beb3ba806aa856c401502202d1e52582412eba2ed474f1f437a427640306fd3838725fab173ade7fe4eae4a01ffffffff010100000000000000232103ac4bba7e7ca3e873eea49e08132ad30c7f03640b6539e9b59903cf14fd016bbbac00000000", + "P2SH" + ], + [ + [ + [ + "e16abbe80bf30c080f63830c8dbf669deaef08957446e95940227d8c5e6db612", + 0, + "1 0x21 0x03905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9f 0x00 2 CHECKMULTISIG" + ] + ], + "010000000112b66d5e8c7d224059e946749508efea9d66bf8d0c83630f080cf30be8bb6ae100000000490047304402206ffe3f14caf38ad5c1544428e99da76ffa5455675ec8d9780fac215ca17953520220779502985e194d84baa36b9bd40a0dbd981163fa191eb884ae83fc5bd1c86b1101ffffffff010100000000000000232103905380c7013e36e6e19d305311c1b81fce6581f5ee1c86ef0627c68c9362fc9fac00000000", + "P2SH" + ], + [ + [ + [ + "ebbcf4bfce13292bd791d6a65a2a858d59adbf737e387e40370d4e64cc70efb0", + 0, + "2 0x21 0x033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194 0x21 0x03a88b326f8767f4f192ce252afe33c94d25ab1d24f27f159b3cb3aa691ffe1423 2 CHECKMULTISIG NOT" + ] + ], + "0100000001b0ef70cc644e0d37407e387e73bfad598d852a5aa6d691d72b2913cebff4bceb000000004a00473044022068cd4851fc7f9a892ab910df7a24e616f293bcb5c5fbdfbc304a194b26b60fba022078e6da13d8cb881a22939b952c24f88b97afd06b4c47a47d7f804c9a352a6d6d0100ffffffff0101000000000000002321033bcaa0a602f0d44cc9d5637c6e515b0471db514c020883830b7cefd73af04194ac00000000", + "P2SH" + ], + [ + [ + [ + "ba4cd7ae2ad4d4d13ebfc8ab1d93a63e4a6563f25089a18bf0fc68f282aa88c1", + 0, + "2 0x21 0x037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1 0x21 0x02edc823cd634f2c4033d94f5755207cb6b60c4b1f1f056ad7471c47de5f2e4d50 2 CHECKMULTISIG NOT" + ] + ], + "0100000001c188aa82f268fcf08ba18950f263654a3ea6931dabc8bf3ed1d4d42aaed74cba000000004b0000483045022100940378576e069aca261a6b26fb38344e4497ca6751bb10905c76bb689f4222b002204833806b014c26fd801727b792b1260003c55710f87c5adbd7a9cb57446dbc9801ffffffff0101000000000000002321037c615d761e71d38903609bf4f46847266edc2fb37532047d747ba47eaae5ffe1ac00000000", + "P2SH" + ], + [ + "OP_CODESEPARATOR tests" + ], + [ + "Test that SignatureHash() removes OP_CODESEPARATOR with FindAndDelete()" + ], + [ + [ + [ + "bc7fd132fcf817918334822ee6d9bd95c889099c96e07ca2c1eb2cc70db63224", + 0, + "CODESEPARATOR 0x21 0x038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041 CHECKSIG" + ] + ], + "01000000012432b60dc72cebc1a27ce0969c0989c895bdd9e62e8234839117f8fc32d17fbc000000004a493046022100a576b52051962c25e642c0fd3d77ee6c92487048e5d90818bcf5b51abaccd7900221008204f8fb121be4ec3b24483b1f92d89b1b0548513a134e345c5442e86e8617a501ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + [ + [ + "83e194f90b6ef21fa2e3a365b63794fb5daa844bdc9b25de30899fcfe7b01047", + 0, + "CODESEPARATOR CODESEPARATOR 0x21 0x038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041 CHECKSIG" + ] + ], + "01000000014710b0e7cf9f8930de259bdc4b84aa5dfb9437b665a3e3a21ff26e0bf994e183000000004a493046022100a166121a61b4eeb19d8f922b978ff6ab58ead8a5a5552bf9be73dc9c156873ea02210092ad9bc43ee647da4f6652c320800debcf08ec20a094a0aaf085f63ecb37a17201ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "Hashed data starts at the CODESEPARATOR" + ], + [ + [ + [ + "326882a7f22b5191f1a0cc9962ca4b878cd969cf3b3a70887aece4d801a0ba5e", + 0, + "0x21 0x038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041 CODESEPARATOR CHECKSIG" + ] + ], + "01000000015ebaa001d8e4ec7a88703a3bcf69d98c874bca6299cca0f191512bf2a7826832000000004948304502203bf754d1c6732fbf87c5dcd81258aefd30f2060d7bd8ac4a5696f7927091dad1022100f5bcb726c4cf5ed0ed34cc13dadeedf628ae1045b7cb34421bc60b89f4cecae701ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "But only if execution has reached it" + ], + [ + [ + [ + "a955032f4d6b0c9bfe8cad8f00a8933790b9c1dc28c82e0f48e75b35da0e4944", + 0, + "0x21 0x038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041 CHECKSIGVERIFY CODESEPARATOR 0x21 0x038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041 CHECKSIGVERIFY CODESEPARATOR 1" + ] + ], + "010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a900000000924830450221009c0a27f886a1d8cb87f6f595fbc3163d28f7a81ec3c4b252ee7f3ac77fd13ffa02203caa8dfa09713c8c4d7ef575c75ed97812072405d932bd11e6a1593a98b679370148304502201e3861ef39a526406bad1e20ecad06be7375ad40ddb582c9be42d26c3a0d7b240221009d0a3985e96522e59635d19cc4448547477396ce0ef17a58e7d74c3ef464292301ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "CODESEPARATOR in an unexecuted IF block does not change what is hashed" + ], + [ + [ + [ + "a955032f4d6b0c9bfe8cad8f00a8933790b9c1dc28c82e0f48e75b35da0e4944", + 0, + "IF CODESEPARATOR ENDIF 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 CHECKSIGVERIFY CODESEPARATOR 1" + ] + ], + "010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a48304502207a6974a77c591fa13dff60cabbb85a0de9e025c09c65a4b2285e47ce8e22f761022100f0efaac9ff8ac36b10721e0aae1fb975c90500b50c56e8a0cc52b0403f0425dd0100ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "As above, with the IF block executed" + ], + [ + [ + [ + "a955032f4d6b0c9bfe8cad8f00a8933790b9c1dc28c82e0f48e75b35da0e4944", + 0, + "IF CODESEPARATOR ENDIF 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 CHECKSIGVERIFY CODESEPARATOR 1" + ] + ], + "010000000144490eda355be7480f2ec828dcc1b9903793a8008fad8cfe9b0c6b4d2f0355a9000000004a483045022100fa4a74ba9fd59c59f46c3960cf90cbe0d2b743c471d24a3d5d6db6002af5eebb02204d70ec490fd0f7055a7c45f86514336e3a7f03503dacecabb247fc23f15c83510151ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "CHECKSIG is legal in scriptSigs" + ], + [ + [ + [ + "ccf7f4053a02e653c36ac75c891b7496d0dc5ce5214f6c913d9cf8f1329ebee0", + 0, + "DUP HASH160 0x14 0xee5a6aa40facefb2655ac23c0c28c57c65c41f9b EQUALVERIFY CHECKSIG" + ] + ], + "0100000001e0be9e32f1f89c3d916c4f21e55cdcd096741b895cc76ac353e6023a05f4f7cc00000000d86149304602210086e5f736a2c3622ebb62bd9d93d8e5d76508b98be922b97160edc3dcca6d8c47022100b23c312ac232a4473f19d2aeb95ab7bdf2b65518911a0d72d50e38b5dd31dc820121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac4730440220508fa761865c8abd81244a168392876ee1d94e8ed83897066b5e2df2400dad24022043f5ee7538e87e9c6aef7ef55133d3e51da7cc522830a9c4d736977a76ef755c0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "Same semantics for OP_CODESEPARATOR" + ], + [ + [ + [ + "10c9f0effe83e97f80f067de2b11c6a00c3088a4bce42c5ae761519af9306f3c", + 1, + "DUP HASH160 0x14 0xee5a6aa40facefb2655ac23c0c28c57c65c41f9b EQUALVERIFY CHECKSIG" + ] + ], + "01000000013c6f30f99a5161e75a2ce4bca488300ca0c6112bde67f0807fe983feeff0c91001000000e608646561646265656675ab61493046022100ce18d384221a731c993939015e3d1bcebafb16e8c0b5b5d14097ec8177ae6f28022100bcab227af90bab33c3fe0a9abfee03ba976ee25dc6ce542526e9b2e56e14b7f10121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac493046022100c3b93edcc0fd6250eb32f2dd8a0bba1754b0f6c3be8ed4100ed582f3db73eba2022100bf75b5bd2eff4d6bf2bda2e34a40fcc07d4aa3cf862ceaa77b47b81eff829f9a01ab21038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "Signatures are removed from the script they are in by FindAndDelete() in the CHECKSIG code; even multiple instances of one signature can be removed." + ], + [ + [ + [ + "6056ebd549003b10cbbd915cea0d82209fe40b8617104be917a26fa92cbe3d6f", + 0, + "DUP HASH160 0x14 0xee5a6aa40facefb2655ac23c0c28c57c65c41f9b EQUALVERIFY CHECKSIG" + ] + ], + "01000000016f3dbe2ca96fa217e94b1017860be49f20820dea5c91bdcb103b0049d5eb566000000000fd1d0147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140147304402203989ac8f9ad36b5d0919d97fa0a7f70c5272abee3b14477dc646288a8b976df5022027d19da84a066af9053ad3d1d7459d171b7e3a80bc6c4ef7a330677a6be548140121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ac47304402203757e937ba807e4a5da8534c17f9d121176056406a6465054bdd260457515c1a02200f02eccf1bec0f3a0d65df37889143c2e88ab7acec61a7b6f5aa264139141a2b0121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "That also includes ahead of the opcode being executed." + ], + [ + [ + [ + "5a6b0021a6042a686b6b94abc36b387bef9109847774e8b1e51eb8cc55c53921", + 1, + "DUP HASH160 0x14 0xee5a6aa40facefb2655ac23c0c28c57c65c41f9b EQUALVERIFY CHECKSIG" + ] + ], + "01000000012139c555ccb81ee5b1e87477840991ef7b386bc3ab946b6b682a04a621006b5a01000000fdb40148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390121038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f2204148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a5800390175ac4830450220646b72c35beeec51f4d5bc1cbae01863825750d7f490864af354e6ea4f625e9c022100f04b98432df3a9641719dbced53393022e7249fb59db993af1118539830aab870148304502201723e692e5f409a7151db386291b63524c5eb2030df652b1f53022fd8207349f022100b90d9bbf2f3366ce176e5e780a00433da67d9e5c79312c6388312a296a580039017521038479a0fa998cd35259a2ef0a7a5c68662c1474f88ccb6d08a7677bbec7f22041ffffffff010000000000000000016a00000000", + "P2SH" + ], + [ + "Finally CHECKMULTISIG removes all signatures prior to hashing the script containing those signatures. In conjunction with the SIGHASH_SINGLE bug this lets us test whether or not FindAndDelete() is actually present in scriptPubKey/redeemScript evaluation by including a signature of the digest 0x01 We can compute in advance for our pubkey, embed it it in the scriptPubKey, and then also using a normal SIGHASH_ALL signature. If FindAndDelete() wasn't run, the 'bugged' signature would still be in the hashed script, and the normal signature would fail." + ], + [ + "Here's an example on mainnet within a P2SH redeemScript. Remarkably it's a standard transaction in <0.9" + ], + [ + [ + [ + "b5b598de91787439afd5938116654e0b16b7a0d0f82742ba37564219c5afcbf9", + 0, + "DUP HASH160 0x14 0xf6f365c40f0739b61de827a44751e5e99032ed8f EQUALVERIFY CHECKSIG" + ], + [ + "ab9805c6d57d7070d9a42c5176e47bb705023e6b67249fb6760880548298e742", + 0, + "HASH160 0x14 0xd8dacdadb7462ae15cd906f1878706d0da8660e6 EQUAL" + ] + ], + "0100000002f9cbafc519425637ba4227f8d0a0b7160b4e65168193d5af39747891de98b5b5000000006b4830450221008dd619c563e527c47d9bd53534a770b102e40faa87f61433580e04e271ef2f960220029886434e18122b53d5decd25f1f4acb2480659fea20aabd856987ba3c3907e0121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffff42e7988254800876b69f24676b3e0205b77be476512ca4d970707dd5c60598ab00000000fd260100483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a53034930460221008431bdfa72bc67f9d41fe72e94c88fb8f359ffa30b33c72c121c5a877d922e1002210089ef5fc22dd8bfc6bf9ffdb01a9862d27687d424d1fefbab9e9c7176844a187a014c9052483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71210378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c7153aeffffffff01a08601000000000017a914d8dacdadb7462ae15cd906f1878706d0da8660e68700000000", + "P2SH" + ], + [ + "Same idea, but with bare CHECKMULTISIG" + ], + [ + [ + [ + "ceafe58e0f6e7d67c0409fbbf673c84c166e3c5d3c24af58f7175b18df3bb3db", + 0, + "DUP HASH160 0x14 0xf6f365c40f0739b61de827a44751e5e99032ed8f EQUALVERIFY CHECKSIG" + ], + [ + "ceafe58e0f6e7d67c0409fbbf673c84c166e3c5d3c24af58f7175b18df3bb3db", + 1, + "2 0x48 0x3045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 0x21 0x0378d430274f8c5ec1321338151e9f27f4c676a008bdf8638d07c0b6be9ab35c71 3 CHECKMULTISIG" + ] + ], + "0100000002dbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce000000006b4830450221009627444320dc5ef8d7f68f35010b4c050a6ed0d96b67a84db99fda9c9de58b1e02203e4b4aaa019e012e65d69b487fdf8719df72f488fa91506a80c49a33929f1fd50121022b78b756e2258af13779c1a1f37ea6800259716ca4b7f0b87610e0bf3ab52a01ffffffffdbb33bdf185b17f758af243c5d3c6e164cc873f6bb9f40c0677d6e0f8ee5afce010000009300483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303483045022015bd0139bcccf990a6af6ec5c1c52ed8222e03a0d51c334df139968525d2fcd20221009f9efe325476eb64c3958e4713e9eefe49bf1d820ed58d2112721b134e2a1a5303ffffffff01a0860100000000001976a9149bc0bbdd3024da4d0c38ed1aecf5c68dd1d3fa1288ac00000000", + "P2SH" + ], + [ + "CHECKLOCKTIMEVERIFY tests" + ], + [ + "By-height locks, with argument == 0 and == tx nLockTime" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "499999999 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ff64cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "By-time locks, with argument just beyond tx nLockTime (but within numerical boundaries)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "500000000 CHECKLOCKTIMEVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "500000000 CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000ffffffff", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Any non-maxint nSequence is fine" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "The argument can be calculated rather than created directly by a PUSHDATA" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "499999999 1ADD CHECKLOCKTIMEVERIFY 1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000065cd1d", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Perhaps even by an ADD producing a 5-byte result that is out of bounds for other opcodes" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483647 2147483647 ADD CHECKLOCKTIMEVERIFY 1" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000feffffff", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "5 byte non-minimally-encoded arguments are valid" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x05 0x0000000000 CHECKLOCKTIMEVERIFY 1" + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Valid CHECKLOCKTIMEVERIFY in scriptSig" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1" + ] + ], + "01000000010001000000000000000000000000000000000000000000000000000000000000000000000251b1000000000100000000000000000001000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "Valid CHECKLOCKTIMEVERIFY in redeemScript" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0xc5b93064159b3b2d6ab506a41b1f50463771b988 EQUAL" + ] + ], + "0100000001000100000000000000000000000000000000000000000000000000000000000000000000030251b1000000000100000000000000000001000000", + "P2SH,CHECKLOCKTIMEVERIFY" + ], + [ + "A transaction with a non-standard DER signature." + ], + [ + [ + [ + "b1dbc81696c8a9c0fccd0693ab66d7c368dbc38c0def4e800685560ddd1b2132", + 0, + "DUP HASH160 0x14 0x4b3bd7eba3bc0284fd3007be7f3be275e94f5826 EQUALVERIFY CHECKSIG" + ] + ], + "010000000132211bdd0d568506804eef0d8cc3db68c3d766ab9306cdfcc0a9c89616c8dbb1000000006c493045022100c7bb0faea0522e74ff220c20c022d2cb6033f8d167fb89e75a50e237a35fd6d202203064713491b1f8ad5f79e623d0219ad32510bfaa1009ab30cbee77b59317d6e30001210237af13eb2d84e4545af287b919c2282019c9691cc509e78e196a9d8274ed1be0ffffffff0100000000000000001976a914f1b3ed2eda9a2ebe5a9374f692877cdf87c0f95b88ac00000000", + "P2SH" + ], + [ + "CHECKSEQUENCEVERIFY tests" + ], + [ + "By-height locks, with argument == 0 and == txin.nSequence" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "65535 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "65535 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "By-time locks, with argument == 0 and == txin.nSequence" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194304 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4259839 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4259839 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194304 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Upper sequence with upper sequence is fine" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument 2^31 with various nSequence" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483648 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument 2^32-1 with various nSequence" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4294967295 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Argument 3<<31 with various nSequence" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "6442450944 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "6442450944 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "6442450944 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "5 byte non-minimally-encoded operandss are valid" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x05 0x0000000000 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "The argument can be calculated rather than created directly by a PUSHDATA" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194303 1ADD CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "4194304 1SUB CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "An ADD producing a 5-byte result that sets CTxIn::SEQUENCE_LOCKTIME_DISABLE_FLAG" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483647 65536 CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "2147483647 4259840 ADD CHECKSEQUENCEVERIFY 1" + ] + ], + "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Valid CHECKSEQUENCEVERIFY in scriptSig" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "1" + ] + ], + "02000000010001000000000000000000000000000000000000000000000000000000000000000000000251b2010000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Valid CHECKSEQUENCEVERIFY in redeemScript" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x7c17aff532f22beb54069942f9bf567a66133eaf EQUAL" + ] + ], + "0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000", + "P2SH,CHECKSEQUENCEVERIFY" + ], + [ + "Valid P2WPKH (Private key of segwit tests is L5AQtV2HDm4xGsseLokK2VAT2EtYKcTm3c7HwqnJBFt9LdaQULsM)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000", + "P2SH,WITNESS" + ], + [ + "Valid P2WSH" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x20 0xff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3db", + 1000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000", + "P2SH,WITNESS" + ], + [ + "Valid P2SH(P2WPKH)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0xfe9c7dacc9fcfbf7e3b7d5ad06aa2b28c5a7b7e3 EQUAL", + 1000 + ] + ], + "01000000000101000100000000000000000000000000000000000000000000000000000000000000000000171600144c9c3dfac4207d5d8cb89df5722cb3d712385e3fffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000", + "P2SH,WITNESS" + ], + [ + "Valid P2SH(P2WSH)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "HASH160 0x14 0x2135ab4f0981830311e35600eebc7376dce3a914 EQUAL", + 1000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000023220020ff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100aa5d8aa40a90f23ce2c3d11bc845ca4a12acd99cbea37de6b9f6d86edebba8cb022022dedc2aa0a255f74d04c0b76ece2d7c691f9dd11a64a8ac49f62a99c3a05f9d01232103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ac00000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash Single|AnyoneCanPay" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3100 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1100 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 3, + "0x51", + 4100 + ] + ], + "0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff05540b0000000000000151d0070000000000000151840300000000000001513c0f00000000000001512c010000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71000000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash Single|AnyoneCanPay (same signature as previous)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210092f4777a0f17bf5aeb8ae768dec5f2c14feabf9d1fe2c89c78dfed0f13fdb86902206da90a86042e252bcd1e80a168c719e4a1ddcc3cebea24b9812c5453c79107e9832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash Single" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff0484030000000000000151d0070000000000000151540b0000000000000151c800000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash Single (same signature as previous)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b000000000000015100024730440220699e6b0cfe015b64ca3283e6551440a34f901ba62dd4c72fe1cb815afb2e6761022021cc5e84db498b1479de14efda49093219441adc6c543e5534979605e273d80b032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash None|AnyoneCanPay" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3100 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1100 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 3, + "0x51", + 4100 + ] + ], + "0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff04b60300000000000001519e070000000000000151860b00000000000001009600000000000000015100000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash None|AnyoneCanPay (same signature as previous)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000248304502210091b32274295c2a3fa02f5bce92fb2789e3fc6ea947fbe1a76e52ea3f4ef2381a022079ad72aefa3837a2e0c033a8652a59731da05fa4a813f4fc48e87c075037256b822103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash None" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff04b60300000000000001519e070000000000000151860b0000000000000100960000000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash None (same signature as previous)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash None (same signature, only sequences changed)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "01000000000103000100000000000000000000000000000000000000000000000000000000000000000000000200000000010000000000000000000000000000000000000000000000000000000000000100000000ffffffff000100000000000000000000000000000000000000000000000000000000000002000000000200000003e8030000000000000151d0070000000000000151b80b00000000000001510002473044022022fceb54f62f8feea77faac7083c3b56c4676a78f93745adc8a35800bc36adfa022026927df9abcf0a8777829bcfcce3ff0a385fa54c3f9df577405e3ef24ee56479022103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash All|AnyoneCanPay" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3100 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1100 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 3, + "0x51", + 4100 + ] + ], + "0100000000010400010000000000000000000000000000000000000000000000000000000000000200000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000300000000ffffffff03e8030000000000000151d0070000000000000151b80b0000000000000151000002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with SigHash All|AnyoneCanPay (same signature as previous)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623eeef89e0ba1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Unknown witness program version (without DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x60 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 2000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "0x51", + 3000 + ] + ], + "0100000000010300010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000200000000ffffffff03e8030000000000000151d0070000000000000151b80b00000000000001510002483045022100a3cec69b52cba2d2de623ffffffffff1606184ea55476c0f8189fda231bc9cbb022003181ad597f7c380a7d1c740286b1d022b8b04ded028b833282e055e03b8efef812103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Witness with a push of 520 bytes" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x20 0x33198a9bfef674ebddb9ffaa52928017b8472791e54c609cb95f278ac6b1e349", + 1000 + ] + ], + "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff010000000000000000015102fd08020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002755100000000", + "P2SH,WITNESS" + ], + [ + "Transaction mixing all SigHash, segwit and normal inputs" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1001 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 2, + "DUP HASH160 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f EQUALVERIFY CHECKSIG", + 1002 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 3, + "DUP HASH160 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f EQUALVERIFY CHECKSIG", + 1003 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 4, + "DUP HASH160 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f EQUALVERIFY CHECKSIG", + 1004 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 5, + "DUP HASH160 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f EQUALVERIFY CHECKSIG", + 1005 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 6, + "DUP HASH160 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f EQUALVERIFY CHECKSIG", + 1006 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 7, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1007 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 8, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1008 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 9, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1009 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 10, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1010 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 11, + "DUP HASH160 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f EQUALVERIFY CHECKSIG", + 1011 + ] + ], + "0100000000010c00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff0001000000000000000000000000000000000000000000000000000000000000020000006a473044022026c2e65b33fcd03b2a3b0f25030f0244bd23cc45ae4dec0f48ae62255b1998a00220463aa3982b718d593a6b9e0044513fd67a5009c2fdccc59992cffc2b167889f4012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000030000006a4730440220008bd8382911218dcb4c9f2e75bf5c5c3635f2f2df49b36994fde85b0be21a1a02205a539ef10fb4c778b522c1be852352ea06c67ab74200977c722b0bc68972575a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000040000006b483045022100d9436c32ff065127d71e1a20e319e4fe0a103ba0272743dbd8580be4659ab5d302203fd62571ee1fe790b182d078ecfd092a509eac112bea558d122974ef9cc012c7012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000050000006a47304402200e2c149b114ec546015c13b2b464bbcb0cdc5872e6775787527af6cbc4830b6c02207e9396c6979fb15a9a2b96ca08a633866eaf20dc0ff3c03e512c1d5a1654f148012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0001000000000000000000000000000000000000000000000000000000000000060000006b483045022100b20e70d897dc15420bccb5e0d3e208d27bdd676af109abbd3f88dbdb7721e6d6022005836e663173fbdfe069f54cde3c2decd3d0ea84378092a5d9d85ec8642e8a41012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff00010000000000000000000000000000000000000000000000000000000000000700000000ffffffff00010000000000000000000000000000000000000000000000000000000000000800000000ffffffff00010000000000000000000000000000000000000000000000000000000000000900000000ffffffff00010000000000000000000000000000000000000000000000000000000000000a00000000ffffffff00010000000000000000000000000000000000000000000000000000000000000b0000006a47304402206639c6e05e3b9d2675a7f3876286bdf7584fe2bbd15e0ce52dd4e02c0092cdc60220757d60b0a61fc95ada79d23746744c72bac1545a75ff6c2c7cdb6ae04e7e9592012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71ffffffff0ce8030000000000000151e9030000000000000151ea030000000000000151eb030000000000000151ec030000000000000151ed030000000000000151ee030000000000000151ef030000000000000151f0030000000000000151f1030000000000000151f2030000000000000151f30300000000000001510248304502210082219a54f61bf126bfc3fa068c6e33831222d1d7138c6faa9d33ca87fd4202d6022063f9902519624254d7c2c8ea7ba2d66ae975e4e229ae38043973ec707d5d4a83012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022017fb58502475848c1b09f162cb1688d0920ff7f142bed0ef904da2ccc88b168f02201798afa61850c65e77889cbcd648a5703b487895517c88f85cdd18b021ee246a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000000247304402202830b7926e488da75782c81a54cd281720890d1af064629ebf2e31bf9f5435f30220089afaa8b455bbeb7d9b9c3fe1ed37d07685ade8455c76472cda424d93e4074a012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7102473044022026326fcdae9207b596c2b05921dbac11d81040c4d40378513670f19d9f4af893022034ecd7a282c0163b89aaa62c22ec202cef4736c58cd251649bad0d8139bcbf55012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71024730440220214978daeb2f38cd426ee6e2f44131a33d6b191af1c216247f1dd7d74c16d84a02205fdc05529b0bc0c430b4d5987264d9d075351c4f4484c16e91662e90a72aab24012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402204a6e9f199dc9672cf2ff8094aaa784363be1eb62b679f7ff2df361124f1dca3302205eeb11f70fab5355c9c8ad1a0700ea355d315e334822fa182227e9815308ee8f012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710000000000", + "P2SH,WITNESS" + ], + [ + "Unknown version witness program with empty witness" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1000 + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", + "P2SH,WITNESS" + ], + [ + "Witness SIGHASH_SINGLE with output out of bound" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x51", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x20 0x4d6c2a32c87821d68fc016fca70797abdb80df6cd84651d40a9300c6bad79e62", + 1000 + ] + ], + "0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff01d00700000000000001510003483045022100e078de4e96a0e05dcdc0a414124dd8475782b5f3f0ed3f607919e9a5eeeb22bf02201de309b3a3109adb3de8074b3610d4cf454c49b61247a2779a0bcbf31c889333032103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc711976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac00000000", + "P2SH,WITNESS" + ], + [ + "1 byte push should not be considered a witness scriptPubKey" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x01 0x01", + 1000 + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", + "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" + ], + [ + "41 bytes push should not be considered a witness scriptPubKey" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x29 0xff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbff0000000000000000", + 1000 + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", + "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" + ], + [ + "The witness version must use OP_1 to OP_16 only" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x01 0x10 0x02 0x0001", + 1000 + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", + "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" + ], + [ + "The witness program push must be canonical" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x60 0x4c02 0x0001", + 1000 + ] + ], + "010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", + "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM" + ], + [ + "Witness Single|AnyoneCanPay does not hash input's position" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1001 + ] + ], + "0100000000010200010000000000000000000000000000000000000000000000000000000000000000000000ffffffff00010000000000000000000000000000000000000000000000000000000000000100000000ffffffff02e8030000000000000151e90300000000000001510247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000", + "P2SH,WITNESS" + ], + [ + "Witness Single|AnyoneCanPay does not hash input's position (permutation)" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1001 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", + 1000 + ] + ], + "0100000000010200010000000000000000000000000000000000000000000000000000000000000100000000ffffffff00010000000000000000000000000000000000000000000000000000000000000000000000ffffffff02e9030000000000000151e80300000000000001510248304502210085001a820bfcbc9f9de0298af714493f8a37b3b354bfd21a7097c3e009f2018c022050a8b4dbc8155d4d04da2f5cdd575dcf8dd0108de8bec759bd897ea01ecb3af7832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc710247304402206d59682663faab5e4cb733c562e22cdae59294895929ec38d7c016621ff90da0022063ef0af5f970afe8a45ea836e3509b8847ed39463253106ac17d19c437d3d56b832103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000", + "P2SH,WITNESS" + ], + [ + "Non witness Single|AnyoneCanPay hash input's position" + ], + [ + [ + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 0, + "0x21 0x03596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71 CHECKSIG", + 1000 + ], + [ + "0000000000000000000000000000000000000000000000000000000000000100", + 1, + "0x21 0x03596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71 CHECKSIG", + 1001 + ] + ], + "01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000", + "P2SH,WITNESS" + ], + [ + "BIP143 examples: details and private keys are available in BIP143" + ], + [ + "BIP143 example: P2WSH with OP_CODESEPARATOR and out-of-range SIGHASH_SINGLE." + ], + [ + [ + [ + "6eb316926b1c5d567cd6f5e6a84fec606fc53d7b474526d1fff3948020c93dfe", + 0, + "0x21 0x036d5c20fa14fb2f635474c1dc4ef5909d4568e5569b79fc94d3448486e14685f8 CHECKSIG", + 156250000 + ], + [ + "f825690aee1b3dc247da796cacb12687a5e802429fd291cfd63e010f02cf1508", + 0, + "0x00 0x20 0x5d1b56b63d714eebe542309525f484b7e9d6f686b3781b6f61ef925d66d6f6a0", + 4900000000 + ] + ], + "01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000", + "P2SH,WITNESS" + ], + [ + "BIP143 example: P2WSH with unexecuted OP_CODESEPARATOR and SINGLE|ANYONECANPAY" + ], + [ + [ + [ + "01c0cf7fba650638e55eb91261b183251fbb466f90dff17f10086817c542b5e9", + 0, + "0x00 0x20 0xba468eea561b26301e4cf69fa34bde4ad60c81e70f059f045ca9a79931004a4d", + 16777215 + ], + [ + "1b2a9a426ba603ba357ce7773cb5805cb9c7c2b386d100d1fc9263513188e680", + 0, + "0x00 0x20 0xd9bbfbe56af7c4b7f960a70d7ea107156913d9e5a26b0a71429df5e097ca6537", + 16777215 + ] + ], + "01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000", + "P2SH,WITNESS" + ], + [ + "BIP143 example: Same as the previous example with input-output pairs swapped" + ], + [ + [ + [ + "1b2a9a426ba603ba357ce7773cb5805cb9c7c2b386d100d1fc9263513188e680", + 0, + "0x00 0x20 0xd9bbfbe56af7c4b7f960a70d7ea107156913d9e5a26b0a71429df5e097ca6537", + 16777215 + ], + [ + "01c0cf7fba650638e55eb91261b183251fbb466f90dff17f10086817c542b5e9", + 0, + "0x00 0x20 0xba468eea561b26301e4cf69fa34bde4ad60c81e70f059f045ca9a79931004a4d", + 16777215 + ] + ], + "0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000", + "P2SH,WITNESS" + ], + [ + "BIP143 example: P2SH-P2WSH 6-of-6 multisig signed with 6 different SIGHASH types" + ], + [ + [ + [ + "6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436", + 1, + "HASH160 0x14 0x9993a429037b5d912407a71c252019287b8d27a5 EQUAL", + 987654321 + ] + ], + "0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000", + "P2SH,WITNESS" + ], + [ + "FindAndDelete tests" + ], + [ + "This is a test of FindAndDelete. The first tx is a spend of normal P2SH and the second tx is a spend of bare P2WSH." + ], + [ + "The redeemScript/witnessScript is CHECKSIGVERIFY <0x30450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01>." + ], + [ + "The signature is <0x30450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01> ," + ], + [ + "where the pubkey is obtained through key recovery with sig and correct sighash." + ], + [ + "This is to show that FindAndDelete is applied only to non-segwit scripts" + ], + [ + "Non-segwit: correct sighash (with FindAndDelete) = 1ba1fe3bc90c5d1265460e684ce6774e324f0fabdf67619eda729e64e8b6bc08" + ], + [ + [ + [ + "f18783ace138abac5d3a7a5cf08e88fe6912f267ef936452e0c27d090621c169", + 7000, + "HASH160 0x14 0x0c746489e2d83cdbb5b90b432773342ba809c134 EQUAL", + 200000 + ] + ], + "010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000", + "P2SH,WITNESS" + ], + [ + "BIP143: correct sighash (without FindAndDelete) = 71c9cd9b2869b9c70b01b1f0360c148f42dee72297db312638df136f43311f23" + ], + [ + [ + [ + "f18783ace138abac5d3a7a5cf08e88fe6912f267ef936452e0c27d090621c169", + 7500, + "0x00 0x20 0x9e1be07558ea5cc8e02ed1d80c0911048afad949affa36d5c3951e3159dbea19", + 200000 + ] + ], + "0100000000010169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f14c1d000000ffffffff01010000000000000000034830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e012102a9781d66b61fb5a7ef00ac5ad5bc6ffc78be7b44a566e3c87870e1079368df4c4aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0100000000", + "P2SH,WITNESS" + ], + [ + "This is multisig version of the FindAndDelete tests" + ], + [ + "Script is 2 CHECKMULTISIGVERIFY DROP" + ], + [ + "52af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175" + ], + [ + "Signature is 0 2 " + ], + [ + "Non-segwit: correct sighash (with FindAndDelete) = 1d50f00ba4db2917b903b0ec5002e017343bb38876398c9510570f5dce099295" + ], + [ + [ + [ + "9628667ad48219a169b41b020800162287d2c0f713c04157e95c484a8dcb7592", + 7000, + "HASH160 0x14 0x5748407f5ca5cdca53ba30b79040260770c9ee1b EQUAL", + 200000 + ] + ], + "01000000019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a662896581b0000fd6f01004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c03959601522102cd74a2809ffeeed0092bc124fd79836706e41f048db3f6ae9df8708cefb83a1c2102e615999372426e46fd107b76eaf007156a507584aa2cc21de9eee3bdbd26d36c4c9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960175ffffffff0101000000000000000000000000", + "P2SH,WITNESS" + ], + [ + "BIP143: correct sighash (without FindAndDelete) = c1628a1e7c67f14ca0c27c06e4fdeec2e6d1a73c7a91d7c046ff83e835aebb72" + ], + [ + [ + [ + "9628667ad48219a169b41b020800162287d2c0f713c04157e95c484a8dcb7592", + 7500, + "0x00 0x20 0x9b66c15b4e0b4eb49fa877982cafded24859fe5b0e2dbfbe4f0df1de7743fd52", + 200000 + ] + ], + "010000000001019275cb8d4a485ce95741c013f7c0d28722160008021bb469a11982d47a6628964c1d000000ffffffff0101000000000000000007004830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c0395960101022102966f109c54e85d3aee8321301136cedeb9fc710fdef58a9de8a73942f8e567c021034ffc99dd9a79dd3cb31e2ab3e0b09e0e67db41ac068c625cd1f491576016c84e9552af4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0148304502205286f726690b2e9b0207f0345711e63fa7012045b9eb0f19c2458ce1db90cf43022100e89f17f86abc5b149eba4115d4f128bcf45d77fb3ecdd34f594091340c039596017500000000", + "P2SH,WITNESS" + ], + [ + "Make diffs cleaner by leaving a comment here without comma at the end" + ] +] diff --git a/pkg/txscript/doc.go b/pkg/txscript/doc.go new file mode 100644 index 0000000..3d5e5de --- /dev/null +++ b/pkg/txscript/doc.go @@ -0,0 +1,26 @@ +/*Package txscript implements the bitcoin transaction script language. + +A complete description of the script language used by bitcoin can be found at https://en.bitcoin.it/wiki/Script. The +following only serves as a quick overview to provide information on how to use the package. This package provides data +structures and functions to parse and execute bitcoin transaction scripts. + +Script Overview + +Bitcoin transaction scripts are written in a stack-base, FORTH-like language. The bitcoin script language consists of a +number of opcodes which fall into several categories such pushing and popping data to and from the stack, performing +basic and bitwise arithmetic, conditional branching, comparing hashes, and checking cryptographic signatures. Scripts +are processed from left to right and intentionally do not provide loops. + +The vast majority of Bitcoin scripts at the time of this writing are of several standard forms which consist of a +spender providing a public key and a signature which proves the spender owns the associated private key. This +information is used to prove the the spender is authorized to perform the transaction. One benefit of using a scripting +language is added flexibility in specifying what conditions must be met in order to spend bitcoins. + +Errors + +Errors returned by this package are of type txscript.ScriptError. This allows the caller to programmatically determine +the specific error by examining the ErrorCode field of the type asserted txscript.ScriptError while still providing rich +error messages with contextual information. A convenience function named IsErrorCode is also provided to allow callers +to easily check for a specific error code. See ErrorCode in the package documentation for a full list. +*/ +package txscript diff --git a/pkg/txscript/engine.go b/pkg/txscript/engine.go new file mode 100644 index 0000000..2108c47 --- /dev/null +++ b/pkg/txscript/engine.go @@ -0,0 +1,925 @@ +package txscript + +import ( + "bytes" + "crypto/sha256" + "fmt" + "math/big" + + "github.com/p9c/p9/pkg/wire" + + "go.uber.org/atomic" + + ec "github.com/p9c/p9/pkg/ecc" +) + +// ScriptFlags is a bitmask defining additional operations or tests that will be done when executing a script pair. +type ScriptFlags uint32 + +const ( + // ScriptBip16 defines whether the bip16 threshold has passed and thus pay-to-script hash transactions will be fully + // validated. + ScriptBip16 ScriptFlags = 1 << iota + // ScriptStrictMultiSig defines whether to verify the stack item used by CHECKMULTISIG is zero length. + ScriptStrictMultiSig + // ScriptDiscourageUpgradableNops defines whether to verify that NOP1 through NOP10 are reserved for future + // soft-fork upgrades. This flag must not be used for consensus critical code nor applied to blocks as this flag is + // only for stricter standard transaction checks. This flag is only applied when the above opcodes are executed. + ScriptDiscourageUpgradableNops + // ScriptVerifyCheckLockTimeVerify defines whether to verify that a transaction output is spendable based on the + // locktime. This is BIP0065. + ScriptVerifyCheckLockTimeVerify + // ScriptVerifyCheckSequenceVerify defines whether to allow execution pathways of a script to be restricted based on + // the age of the output being spent. This is BIP0112. + ScriptVerifyCheckSequenceVerify + // ScriptVerifyCleanStack defines that the stack must contain only one stack + // element after evaluation and that the element must be true if interpreted as + // a boolean. This is rule 6 of BIP0062. This flag should never be used without + // the ScriptBip16 flag nor the ScriptVerifyWitness flag. + ScriptVerifyCleanStack + // ScriptVerifyDERSignatures defines that signatures are required to compily with the DER format. + ScriptVerifyDERSignatures + // ScriptVerifyLowS defines that signtures are required to comply with the DER format and whose S value is <= order + // / 2. This is rule 5 of BIP0062. + ScriptVerifyLowS + // ScriptVerifyMinimalData defines that signatures must use the smallest push operator. This is both rules 3 and 4 + // of BIP0062. + ScriptVerifyMinimalData + // ScriptVerifyNullFail defines that signatures must be empty if a CHECKSIG or CHECKMULTISIG operation fails. + ScriptVerifyNullFail + // ScriptVerifySigPushOnly defines that signature scripts must contain only pushed data. This is rule 2 of BIP0062. + ScriptVerifySigPushOnly + // ScriptVerifyStrictEncoding defines that signature scripts and public keys must follow the strict encoding + // requirements. + ScriptVerifyStrictEncoding + // ScriptVerifyWitness defines whether or not to verify a transaction output + // using a witness program template. + ScriptVerifyWitness + // ScriptVerifyDiscourageUpgradeableWitnessProgram makes witness program with + // versions 2-16 non-standard. + ScriptVerifyDiscourageUpgradeableWitnessProgram + // ScriptVerifyMinimalIf makes a script with an OP_IF/OP_NOTIF whose operand is anything other than empty vector or + // [0x01] non-standard. + ScriptVerifyMinimalIf + // ScriptVerifyWitnessPubKeyType makes a script within a check-sig operation + // whose public key isn't serialized in a compressed format non-standard. + ScriptVerifyWitnessPubKeyType + // MaxStackSize is the maximum combined height of stack and alt stack during execution. + MaxStackSize = 1000 + // MaxScriptSize is the maximum allowed length of a raw script. + MaxScriptSize = 10000 + // payToWitnessPubKeyHashDataSize is the size of the witness program's data push + // for a pay-to-witness-pub-key-hash output. + payToWitnessPubKeyHashDataSize = 20 + // payToWitnessScriptHashDataSize is the size of the witness program's data push + // for a pay-to-witness-script-hash output. + payToWitnessScriptHashDataSize = 32 +) + +// halforder is used to tame ECDSA malleability (see BIP0062). +var halfOrder = new(big.Int).Rsh(ec.S256().N, 1) + +// Engine is the virtual machine that executes scripts. +type Engine struct { + scripts [][]parsedOpcode + scriptIdx atomic.Int64 + scriptOff atomic.Int64 + lastCodeSep int + dstack stack // data stack + astack stack // alt stack + tx wire.MsgTx + txIdx int + condStack []int + numOps int + flags ScriptFlags + sigCache *SigCache + hashCache *TxSigHashes + bip16 bool // treat execution as pay-to-script-hash + savedFirstStack [][]byte // stack from first script for bip16 scripts + witnessVersion int + witnessProgram []byte + inputAmount int64 +} + +// hasFlag returns whether the script engine instance has the passed flag set. +func (vm *Engine) hasFlag(flag ScriptFlags) bool { + return vm.flags&flag == flag +} + +// isBranchExecuting returns whether or not the current conditional branch is actively executing. For example, when the +// data stack has an OP_FALSE on it and an OP_IF is encountered, the branch is inactive until an OP_ELSE or OP_ENDIF is +// encountered. It properly handles nested conditionals. +func (vm *Engine) isBranchExecuting() bool { + if len(vm.condStack) == 0 { + return true + } + return vm.condStack[len(vm.condStack)-1] == OpCondTrue +} + +// executeOpcode peforms execution on the passed opcode. It takes into account whether or not it is hidden by +// conditionals, but some rules still must be tested in this case. +func (vm *Engine) executeOpcode(pop *parsedOpcode) (e error) { + // Disabled opcodes are fail on program counter. + if pop.isDisabled() { + str := fmt.Sprintf( + "attempt to execute disabled opcode %s", + pop.opcode.name, + ) + return scriptError(ErrDisabledOpcode, str) + } + // Always-illegal opcodes are fail on program counter. + if pop.alwaysIllegal() { + str := fmt.Sprintf( + "attempt to execute reserved opcode %s", + pop.opcode.name, + ) + return scriptError(ErrReservedOpcode, str) + } + // Note that this includes OP_RESERVED which counts as a push operation. + if pop.opcode.value > OP_16 { + vm.numOps++ + if vm.numOps > MaxOpsPerScript { + str := fmt.Sprintf( + "exceeded max operation limit of %d", + MaxOpsPerScript, + ) + return scriptError(ErrTooManyOperations, str) + } + } else if len(pop.data) > MaxScriptElementSize { + str := fmt.Sprintf( + "element size %d exceeds max allowed size %d", + len(pop.data), MaxScriptElementSize, + ) + return scriptError(ErrElementTooBig, str) + } + // Nothing left to do when this is not a conditional opcode and it is not in an executing branch. + if !vm.isBranchExecuting() && !pop.isConditional() { + return nil + } + // Ensure all executed data push opcodes use the minimal encoding when the minimal data verification flag is set. + if vm.dstack.verifyMinimalData && vm.isBranchExecuting() && + // pop.opcode.value >= 0 && + pop.opcode.value <= OP_PUSHDATA4 { + if e = pop.checkMinimalDataPush(); E.Chk(e) { + return e + } + } + return pop.opcode.opfunc(pop, vm) +} + +// disasm is a helper function to produce the output for DisasmPC and DisasmScript. It produces the opcode prefixed by +// the program counter at the provided position in the script. It does no error checking and leaves that to the caller +// to provide a valid offset. +func (vm *Engine) disasm(scriptIdx int, scriptOff int) string { + if scriptIdx >= len(vm.scripts) { + return fmt.Sprintf("disasm array index out of bounds ERR: %02x:%04x", scriptIdx, scriptOff) + } + if scriptOff >= len(vm.scripts[scriptIdx]) { + return fmt.Sprintf( + "disasm scriptoff array index out of bounds ERR: %02x:%04x", scriptIdx, scriptOff, + ) + } + return fmt.Sprintf( + "%02x:%04x: %s", scriptIdx, scriptOff, + vm.scripts[scriptIdx][scriptOff].print(false), + ) +} + +// validPC returns an error if the current script position is valid for execution, nil otherwise. +func (vm *Engine) validPC() (E error) { + if int(vm.scriptIdx.Load()) >= len(vm.scripts) { + str := fmt.Sprintf( + "past input scripts %v:%v %v:xxxx", + vm.scriptIdx.Load(), vm.scriptOff.Load(), len(vm.scripts), + ) + E = scriptError(ErrInvalidProgramCounter, str) + } + if len(vm.scripts) < int(vm.scriptIdx.Load()) && + int(vm.scriptOff.Load()) >= len(vm.scripts[vm.scriptIdx.Load()]) { + str := fmt.Sprintf( + "past input scripts %v:%v %v:%04d", + vm.scriptIdx.Load(), vm.scriptOff.Load(), vm.scriptIdx.Load(), + len(vm.scripts[vm.scriptIdx.Load()]), + ) + return scriptError(ErrInvalidProgramCounter, str) + } + return nil +} + +// curPC returns either the current script and offset, or an error if the position isn't valid. +func (vm *Engine) curPC() (script int, off int, e error) { + e = vm.validPC() + if e != nil { + return 0, 0, e + } + return int(vm.scriptIdx.Load()), int(vm.scriptOff.Load()), nil +} + +// isWitnessVersionActive returns true if a witness program was extracted during +// the initialization of the Engine, and the program's version matches the +// specified version. +func (vm *Engine) isWitnessVersionActive(version uint) bool { + return vm.witnessProgram != nil && uint(vm.witnessVersion) == version +} + +// verifyWitnessProgram validates the stored witness program using the passed +// witness as input. +func (vm *Engine) verifyWitnessProgram(witness [][]byte) (e error) { + if vm.isWitnessVersionActive(0) { + switch len(vm.witnessProgram) { + case payToWitnessPubKeyHashDataSize: // P2WKH + // The witness stack should consist of exactly two items: the signature, and the + // pubkey. + if len(witness) != 2 { + e := fmt.Sprintf( + "should have exactly two items in witness, instead have %v", len(witness), + ) + return scriptError(ErrWitnessProgramMismatch, e) + } + // Now we'll resume execution as if it were a regular p2pkh transaction. + pkScript, e := payToPubKeyHashScript(vm.witnessProgram) + if e != nil { + return e + } + pops, e := parseScript(pkScript) + if e != nil { + return e + } + // Set the stack to the provided witness stack, then append the pkScript + // generated above as the next script to execute. + vm.scripts = append(vm.scripts, pops) + vm.SetStack(witness) + case payToWitnessScriptHashDataSize: // P2WSH + // Additionally, The witness stack MUST NOT be empty at this point. + if len(witness) == 0 { + return scriptError( + ErrWitnessProgramEmpty, "witness program empty passed empty witness", + ) + } + // Obtain the witness script which should be the last element in the passed + // stack. The size of the script MUST NOT exceed the max script size. + witnessScript := witness[len(witness)-1] + if len(witnessScript) > MaxScriptSize { + str := fmt.Sprintf( + "witnessScript size %d "+ + "is larger than max allowed size %d", + len(witnessScript), MaxScriptSize, + ) + return scriptError(ErrScriptTooBig, str) + } + // Ensure that the serialized pkScript at the end of the witness stack matches + // the witness program. + witnessHash := sha256.Sum256(witnessScript) + if !bytes.Equal(witnessHash[:], vm.witnessProgram) { + return scriptError( + ErrWitnessProgramMismatch, + "witness program hash mismatch", + ) + } + // With all the validity checks passed, parse the script into individual op-codes so w can execute it as the + // next script. + pops, e := parseScript(witnessScript) + if e != nil { + return e + } + // The hash matched successfully, so use the witness as the stack, and set the + // witnessScript to be the next script executed. + vm.scripts = append(vm.scripts, pops) + vm.SetStack(witness[:len(witness)-1]) + default: + errStr := fmt.Sprintf( + "length of witness program "+ + "must either be %v or %v bytes, instead is %v bytes", + payToWitnessPubKeyHashDataSize, + payToWitnessScriptHashDataSize, + len(vm.witnessProgram), + ) + return scriptError(ErrWitnessProgramWrongLength, errStr) + } + } else if vm.hasFlag(ScriptVerifyDiscourageUpgradeableWitnessProgram) { + errStr := fmt.Sprintf( + "new witness program versions invalid: %v", vm.witnessProgram, + ) + return scriptError(ErrDiscourageUpgradableWitnessProgram, errStr) + } else { + // If we encounter an unknown witness program version and we aren't discouraging + // future unknown witness based soft-forks, then we de-activate the segwit + // behavior within the VM for the remainder of execution. + vm.witnessProgram = nil + } + if vm.isWitnessVersionActive(0) { + // All elements within the witness stack must not be greater than the maximum + // bytes which are allowed to be pushed onto the stack. + for _, witElement := range vm.GetStack() { + if len(witElement) > MaxScriptElementSize { + str := fmt.Sprintf( + "element size %d exceeds "+ + "max allowed size %d", len(witElement), + MaxScriptElementSize, + ) + return scriptError(ErrElementTooBig, str) + } + } + } + return nil +} + +// DisasmPC returns the string for the disassembly of the opcode that will be next to execute when Step() is called. +func (vm *Engine) DisasmPC() (string, error) { + scriptIdx, scriptOff, e := vm.curPC() + if e != nil { + return "", e + } + return vm.disasm(scriptIdx, scriptOff), nil +} + +// DisasmScript returns the disassembly string for the script at the requested offset index. Index 0 is the signature +// script and 1 is the public key script. +func (vm *Engine) DisasmScript(idx int) (string, error) { + if idx >= len(vm.scripts) { + str := fmt.Sprintf( + "script index %d >= total scripts %d", idx, + len(vm.scripts), + ) + return "", scriptError(ErrInvalidIndex, str) + } + var disstr string + for i := range vm.scripts[idx] { + disstr = disstr + vm.disasm(idx, i) + "\n" + } + return disstr, nil +} + +// CheckErrorCondition returns nil if the running script has ended and was successful, leaving a a true boolean on the +// stack. An error otherwise, including if the script has not finished. +func (vm *Engine) CheckErrorCondition(finalScript bool) (e error) { + // Chk execution is actually done. When pc is past the end of script array there are no more scripts to run. + if int(vm.scriptIdx.Load()) < len(vm.scripts) { + return scriptError( + ErrScriptUnfinished, + "error check when script unfinished", + ) + } + // If we're in version zero witness execution mode, and this was the final + // script, then the stack MUST be clean in order to maintain compatibility with + // BIP16. + if finalScript && vm.isWitnessVersionActive(0) && vm.dstack.Depth() != 1 { + return scriptError( + ErrEvalFalse, "witness program must have clean stack", + ) + } + if finalScript && vm.hasFlag(ScriptVerifyCleanStack) && + vm.dstack.Depth() != 1 { + str := fmt.Sprintf( + "stack contains %d unexpected items", + vm.dstack.Depth()-1, + ) + return scriptError(ErrCleanStack, str) + } else if vm.dstack.Depth() < 1 { + return scriptError( + ErrEmptyStack, + "stack empty at end of script execution", + ) + } + v, e := vm.dstack.PopBool() + if e != nil { + return e + } + if !v { + // Log interesting data. + T.C( + func() string { + dis0, _ := vm.DisasmScript(0) + dis1, _ := vm.DisasmScript(1) + return fmt.Sprintf( + "scripts failed: script0: %s\n"+ + "script1: %s", dis0, dis1, + ) + }, + ) + return scriptError( + ErrEvalFalse, + "false stack entry at end of script execution", + ) + } + return nil +} + +// Step will execute the next instruction and move the program counter to the next opcode in the script, or the next +// script if the current has ended. Step will return true in the case that the last opcode was successfully executed. +// The result of calling Step or any other method is undefined if an error is returned. +func (vm *Engine) Step() (done bool, e error) { + // Verify that it is pointing to a valid script address. + e = vm.validPC() + if e != nil { + return true, e + } + opcode := &vm.scripts[vm.scriptIdx.Load()][vm.scriptOff.Load()] + vm.scriptOff.Inc() + // Execute the opcode while taking into account several things such as disabled opcodes, illegal opcodes, maximum + // allowed operations per script, maximum script element txsizes, and conditionals. + e = vm.executeOpcode(opcode) + if e != nil { + return true, e + } + // The number of elements in the combination of the data and alt stacks must not exceed the maximum number of stack + // elements allowed. + combinedStackSize := vm.dstack.Depth() + vm.astack.Depth() + if combinedStackSize > MaxStackSize { + str := fmt.Sprintf( + "combined stack size %d > max allowed %d", + combinedStackSize, MaxStackSize, + ) + done, e = false, scriptError(ErrStackOverflow, str) + } + if e != nil { + return + } + // Prepare for next instruction. + if int(vm.scriptOff.Load()) >= len(vm.scripts[vm.scriptIdx.Load()]) { + // Illegal to have an `if' that straddles two scripts. + if len(vm.condStack) != 0 { + done, e = + false, + scriptError( + ErrUnbalancedConditional, + "end of script reached in conditional execution", + ) + return + } + // Alt stack doesn't persist. + _ = vm.astack.DropN(vm.astack.Depth()) + vm.numOps = 0 // number of ops is per script. + vm.scriptOff.Store(0) + if vm.scriptIdx.Load() == 0 && vm.bip16 { + vm.scriptIdx.Inc() + vm.savedFirstStack = vm.GetStack() + } else if vm.scriptIdx.Load() == 1 && vm.bip16 { + // Put us past the end for CheckErrorCondition() + vm.scriptIdx.Inc() + // Check script ran successfully and pull the script out of the first stack and execute that. + ee := vm.CheckErrorCondition(false) + if ee != nil { + E.Ln(e) + done, e = false, ee + return + } + script := vm.savedFirstStack[len(vm.savedFirstStack)-1] + pops, er := parseScript(script) + if er != nil { + E.Ln(e) + done, e = false, er + return + } + vm.scripts = append(vm.scripts, pops) + // Set stack to be the stack from first script minus the script itself + vm.SetStack(vm.savedFirstStack[:len(vm.savedFirstStack)-1]) + // } else if (vm.scriptIdx.Load() == 1 && vm.witnessProgram != nil) || + // (vm.scriptIdx.Load() == 2 && vm.witnessProgram != nil && vm.bip16) { + // // Nested P2SH. + // vm.scriptIdx.Inc() + // witness := vm.tx.TxIn[vm.txIdx].Witness + // if er := vm.verifyWitnessProgram(witness); E.Chk(e) { + // done, e = false, er + // return + // } + } else { + vm.scriptIdx.Inc() + } + // there are zero length scripts in the wild + if int(vm.scriptIdx.Load()) < len(vm.scripts) && + int(vm.scriptOff.Load()) >= len(vm.scripts[vm.scriptIdx.Load()]) { + vm.scriptIdx.Inc() + } + vm.lastCodeSep = 0 + if int(vm.scriptIdx.Load()) >= len(vm.scripts) { + done, e = true, nil + return + } + } + return +} + +// Execute will execute all scripts in the script engine and return either nil for successful validation or an error if +// one occurred. +func (vm *Engine) Execute() (e error) { + done := false + for !done { + done, e = vm.Step() + if e != nil { + return e + } + T.C( + func() string { + var o string + dis, e := vm.DisasmPC() + if e != nil { + o += "c stepping (" + e.Error() + ")" + } + o += "oo stepping " + dis + var dstr, astr string + // if we're tracing, dump the stacks. + if vm.dstack.Depth() != 0 { + dstr = "\nStack:\n" + vm.dstack.String() + } + if vm.astack.Depth() != 0 { + astr = "\nAltStack:\n" + vm.astack.String() + } + return o + dstr + astr + }, + ) + } + return vm.CheckErrorCondition(true) +} + +// subScript returns the script since the last OP_CODESEPARATOR. +func (vm *Engine) subScript() []parsedOpcode { + return vm.scripts[vm.scriptIdx.Load()][vm.lastCodeSep:] +} + +// checkHashTypeEncoding returns whether or not the passed hashtype adheres to the strict encoding requirements if +// enabled. +func (vm *Engine) checkHashTypeEncoding(hashType SigHashType) (e error) { + if !vm.hasFlag(ScriptVerifyStrictEncoding) { + return nil + } + sigHashType := hashType & ^SigHashAnyOneCanPay + if sigHashType < SigHashAll || sigHashType > SigHashSingle { + str := fmt.Sprintf("invalid hash type 0x%x", hashType) + return scriptError(ErrInvalidSigHashType, str) + } + return nil +} + +// checkPubKeyEncoding returns whether or not the passed public key adheres to the strict encoding requirements if +// enabled. +func (vm *Engine) checkPubKeyEncoding(pubKey []byte) (e error) { + if vm.hasFlag(ScriptVerifyWitnessPubKeyType) && + vm.isWitnessVersionActive(0) && !ec.IsCompressedPubKey(pubKey) { + str := "only uncompressed keys are accepted post-segwit" + return scriptError(ErrWitnessPubKeyType, str) + } + if !vm.hasFlag(ScriptVerifyStrictEncoding) { + return nil + } + if len(pubKey) == 33 && (pubKey[0] == 0x02 || pubKey[0] == 0x03) { + // Compressed + return nil + } + if len(pubKey) == 65 && pubKey[0] == 0x04 { + // Uncompressed + return nil + } + return scriptError(ErrPubKeyType, "unsupported public key type") +} + +// checkSignatureEncoding returns whether or not the passed signature adheres to the strict encoding requirements if +// enabled. +func (vm *Engine) checkSignatureEncoding(sig []byte) (e error) { + if !vm.hasFlag(ScriptVerifyDERSignatures) && + !vm.hasFlag(ScriptVerifyLowS) && + !vm.hasFlag(ScriptVerifyStrictEncoding) { + return nil + } + // The format of a DER encoded signature is as follows: + // + // 0x30 0x02 0x02 + // - 0x30 is the ASN.1 identifier for a sequence - Total length is 1 byte and specifies length of all remaining data + // - 0x02 is the ASN.1 identifier that specifies an integer follows + // - Length of R is 1 byte and specifies how many bytes R occupies + // - R is the arbitrary length big-endian encoded number which represents the R value of the + // signature. DER encoding dictates that the value must be encoded using the minimum possible number of bytes. This + // implies the first byte can only be null if the highest bit of the next byte is set in order to prevent it from + // being interpreted as a negative number. + // - 0x02 is once again the ASN.1 integer identifier - Length of S is 1 byte + // and specifies how many bytes S occupies + // - S is the arbitrary length big-endian encoded number which represents + // the S value of the signature. The encoding rules are identical as those for R. + const ( + asn1SequenceID = 0x30 + asn1IntegerID = 0x02 + // minSigLen is the minimum length of a DER encoded signature and is when both R and S are 1 byte each. + // 0x30 + <1-byte> + 0x02 + 0x01 + + 0x2 + 0x01 + + minSigLen = 8 + // maxSigLen is the maximum length of a DER encoded signature and is when both R and S are 33 bytes each. It is + // 33 bytes because a 256-bit integer requires 32 bytes and an additional leading null byte might required if + // the high bit is set in the value. + // + // 0x30 + <1-byte> + 0x02 + 0x21 + <33 bytes> + 0x2 + 0x21 + <33 bytes> + maxSigLen = 72 + // sequenceOffset is the byte offset within the signature of the expected ASN.1 sequence identifier. + sequenceOffset = 0 + // dataLenOffset is the byte offset within the signature of the expected total length of all remaining data in + // the signature. + dataLenOffset = 1 + // rTypeOffset is the byte offset within the signature of the ASN.1 identifier for R and is expected to indicate + // an ASN.1 integer. + rTypeOffset = 2 + // rLenOffset is the byte offset within the signature of the length of R. + rLenOffset = 3 + // rOffset is the byte offset within the signature of R. + rOffset = 4 + ) + // The signature must adhere to the minimum and maximum allowed length. + sigLen := len(sig) + if sigLen < minSigLen { + str := fmt.Sprintf( + "malformed signature: too short: %d < %d", sigLen, + minSigLen, + ) + return scriptError(ErrSigTooShort, str) + } + if sigLen > maxSigLen { + str := fmt.Sprintf( + "malformed signature: too long: %d > %d", sigLen, + maxSigLen, + ) + return scriptError(ErrSigTooLong, str) + } + // The signature must start with the ASN.1 sequence identifier. + if sig[sequenceOffset] != asn1SequenceID { + str := fmt.Sprintf( + "malformed signature: format has wrong type: %#x", + sig[sequenceOffset], + ) + return scriptError(ErrSigInvalidSeqID, str) + } + // The signature must indicate the correct amount of data for all elements related to R and S. + if int(sig[dataLenOffset]) != sigLen-2 { + str := fmt.Sprintf( + "malformed signature: bad length: %d != %d", + sig[dataLenOffset], sigLen-2, + ) + return scriptError(ErrSigInvalidDataLen, str) + } + // Calculate the offsets of the elements related to S and ensure S is inside the signature. rLen specifies the + // length of the big-endian encoded number which represents the R value of the signature. sTypeOffset is the offset + // of the ASN.1 identifier for S and, like its R counterpart, is expected to indicate an ASN.1 integer. sLenOffset + // and sOffset are the byte offsets within the signature of the length of S and S itself, respectively. + rLen := int(sig[rLenOffset]) + sTypeOffset := rOffset + rLen + sLenOffset := sTypeOffset + 1 + if sTypeOffset >= sigLen { + str := "malformed signature: S type indicator missing" + return scriptError(ErrSigMissingSTypeID, str) + } + if sLenOffset >= sigLen { + str := "malformed signature: S length missing" + return scriptError(ErrSigMissingSLen, str) + } + // The lengths of R and S must match the overall length of the signature. sLen specifies the length of the + // big-endian encoded number which represents the S value of the signature. + sOffset := sLenOffset + 1 + sLen := int(sig[sLenOffset]) + if sOffset+sLen != sigLen { + str := "malformed signature: invalid S length" + return scriptError(ErrSigInvalidSLen, str) + } + // R elements must be ASN.1 integers. + if sig[rTypeOffset] != asn1IntegerID { + str := fmt.Sprintf( + "malformed signature: R integer marker: %#x != %#x", + sig[rTypeOffset], asn1IntegerID, + ) + return scriptError(ErrSigInvalidRIntID, str) + } + // Zero-length integers are not allowed for R. + if rLen == 0 { + str := "malformed signature: R length is zero" + return scriptError(ErrSigZeroRLen, str) + } + // R must not be negative. + if sig[rOffset]&0x80 != 0 { + str := "malformed signature: R is negative" + return scriptError(ErrSigNegativeR, str) + } + // Null bytes at the start of R are not allowed, unless R would otherwise be interpreted as a negative number. + if rLen > 1 && sig[rOffset] == 0x00 && sig[rOffset+1]&0x80 == 0 { + str := "malformed signature: R value has too much padding" + return scriptError(ErrSigTooMuchRPadding, str) + } + // S elements must be ASN.1 integers. + if sig[sTypeOffset] != asn1IntegerID { + str := fmt.Sprintf( + "malformed signature: S integer marker: %#x != %#x", + sig[sTypeOffset], asn1IntegerID, + ) + return scriptError(ErrSigInvalidSIntID, str) + } + // Zero-length integers are not allowed for S. + if sLen == 0 { + str := "malformed signature: S length is zero" + return scriptError(ErrSigZeroSLen, str) + } + // S must not be negative. + if sig[sOffset]&0x80 != 0 { + str := "malformed signature: S is negative" + return scriptError(ErrSigNegativeS, str) + } + // Null bytes at the start of S are not allowed, unless S would otherwise be interpreted as a negative number. + if sLen > 1 && sig[sOffset] == 0x00 && sig[sOffset+1]&0x80 == 0 { + str := "malformed signature: S value has too much padding" + return scriptError(ErrSigTooMuchSPadding, str) + } + // Verify the S value is <= half the order of the curve. This check is done because when it is higher, the + // complement modulo the order can be used instead which is a shorter encoding by 1 byte. Further, without enforcing + // this, it is possible to replace a signature in a valid transaction with the complement while still being a valid + // signature that verifies. This would result in changing the transaction hash and thus is a source of malleability. + if vm.hasFlag(ScriptVerifyLowS) { + sValue := new(big.Int).SetBytes(sig[sOffset : sOffset+sLen]) + if sValue.Cmp(halfOrder) > 0 { + return scriptError( + ErrSigHighS, "signature is not canonical due "+ + "to unnecessarily high S value", + ) + } + } + return nil +} + +// getStack returns the contents of stack as a byte array bottom up +func getStack(stack *stack) [][]byte { + array := make([][]byte, stack.Depth()) + for i := range array { + // PeekByteArry can't fail due to overflow, already checked + array[len(array)-i-1], _ = stack.PeekByteArray(int32(i)) + } + return array +} + +// setStack sets the stack to the contents of the array where the last item in the array is the top item in the stack. +func setStack(stack *stack, data [][]byte) { + // This can not error. Only errors are for invalid arguments. + _ = stack.DropN(stack.Depth()) + for i := range data { + stack.PushByteArray(data[i]) + } +} + +// GetStack returns the contents of the primary stack as an array. where the last item in the array is the top of the +// stack. +func (vm *Engine) GetStack() [][]byte { + return getStack(&vm.dstack) +} + +// SetStack sets the contents of the primary stack to the contents of the provided array where the last item in the +// array will be the top of the stack. +func (vm *Engine) SetStack(data [][]byte) { + setStack(&vm.dstack, data) +} + +// GetAltStack returns the contents of the alternate stack as an array where the last item in the array is the top of +// the stack. +func (vm *Engine) GetAltStack() [][]byte { + return getStack(&vm.astack) +} + +// SetAltStack sets the contents of the alternate stack to the contents of the provided array where the last item in the +// array will be the top of the stack. +func (vm *Engine) SetAltStack(data [][]byte) { + setStack(&vm.astack, data) +} + +// NewEngine returns a new script engine for the provided public key script, transaction, and input index. The flags +// modify the behavior of the script engine according to the description provided by each flag. +func NewEngine( + scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags, + sigCache *SigCache, hashCache *TxSigHashes, inputAmount int64, +) (*Engine, error) { + // The provided transaction input index must refer to a valid input. + if txIdx < 0 || txIdx >= len(tx.TxIn) { + str := fmt.Sprintf( + "transaction input index %d is negative or "+ + ">= %d", txIdx, len(tx.TxIn), + ) + return nil, scriptError(ErrInvalidIndex, str) + } + scriptSig := tx.TxIn[txIdx].SignatureScript + // When both the signature script and public key script are empty the result is necessarily an error since the stack + // would end up being empty which is equivalent to a false top element. Thus, just return the relevant error now as + // an optimization. + if len(scriptSig) == 0 && len(scriptPubKey) == 0 { + return nil, scriptError( + ErrEvalFalse, + "false stack entry at end of script execution", + ) + } + // The clean stack flag (ScriptVerifyCleanStack) is not allowed without either + // the pay-to-script-hash (P2SH) evaluation (ScriptBip16) flag or the Segregated + // Witness (ScriptVerifyWitness) flag. Recall that evaluating a P2SH script + // without the flag set results in non-P2SH evaluation which leaves the P2SH + // inputs on the stack. Thus, allowing the clean stack flag without the P2SH + // flag would make it possible to have a situation where P2SH would not be a + // soft fork when it should be. The same goes for segwit which will pull in + // additional scripts for execution from the witness stack. + vm := Engine{ + flags: flags, + sigCache: sigCache, + hashCache: hashCache, + inputAmount: inputAmount, + } + if vm.hasFlag(ScriptVerifyCleanStack) && (!vm.hasFlag(ScriptBip16) && + !vm.hasFlag(ScriptVerifyWitness)) { + return nil, scriptError( + ErrInvalidFlags, + "invalid flags combination", + ) + } + // The signature script must only contain data pushes when the associated flag is set. + if vm.hasFlag(ScriptVerifySigPushOnly) && !IsPushOnlyScript(scriptSig) { + return nil, scriptError( + ErrNotPushOnly, + "signature script is not push only", + ) + } + // The engine stores the scripts in parsed form using a slice. This allows multiple scripts to be executed in + // sequence. For example, with a pay-to-script-hash transaction, there will be ultimately be a third script to + // execute. + scripts := [][]byte{scriptSig, scriptPubKey} + vm.scripts = make([][]parsedOpcode, len(scripts)) + for i, scr := range scripts { + if len(scr) > MaxScriptSize { + str := fmt.Sprintf( + "script size %d is larger than max "+ + "allowed size %d", len(scr), MaxScriptSize, + ) + return nil, scriptError(ErrScriptTooBig, str) + } + var e error + vm.scripts[i], e = parseScript(scr) + if e != nil { + return nil, e + } + } + // Advance the program counter to the public key script if the signature script is empty since there is nothing to + // execute for it in that case. + if len(scripts[0]) == 0 { + vm.scriptIdx.Inc() + } + if vm.hasFlag(ScriptBip16) && isScriptHash(vm.scripts[1]) { + // Only accept input scripts that push data for P2SH. + if !isPushOnly(vm.scripts[0]) { + return nil, scriptError( + ErrNotPushOnly, + "pay to script hash is not push only", + ) + } + vm.bip16 = true + } + if vm.hasFlag(ScriptVerifyMinimalData) { + vm.dstack.verifyMinimalData = true + vm.astack.verifyMinimalData = true + } + // // Chk to see if we should execute in witness verification mode according to + // // the set flags. We check both the pkScript, and sigScript here since in the + // // case of nested p2sh, the scriptSig will be a valid witness program. For + // // nested p2sh, all the bytes after the first data push should *exactly* match + // // the witness program template. + // if vm.hasFlag(ScriptVerifyWitness) { + // // If witness evaluation is enabled, then P2SH MUST also be active. + // if !vm.hasFlag(ScriptBip16) { + // errStr := "P2SH must be enabled to do witness verification" + // return nil, scriptError(ErrInvalidFlags, errStr) + // } + // var witProgram []byte + // switch { + // case isWitnessProgram(vm.scripts[1]): + // // The scriptSig must be *empty* for all native witness programs, otherwise we + // // introduce malleability. + // if len(scriptSig) != 0 { + // errStr := "native witness program cannot also have a signature script" + // return nil, scriptError(ErrWitnessMalleated, errStr) + // } + // witProgram = scriptPubKey + // case len(tx.TxIn[txIdx].Witness) != 0 && vm.bip16: + // // The sigScript MUST be *exactly* a single canonical data push of the witness + // // program, otherwise we reintroduce malleability. + // sigPops := vm.scripts[0] + // if len(sigPops) == 1 && canonicalPush(sigPops[0]) && + // IsWitnessProgram(sigPops[0].data) { + // witProgram = sigPops[0].data + // } else { + // errStr := "signature script for witness nested p2sh is not canonical" + // return nil, scriptError(ErrWitnessMalleatedP2SH, errStr) + // } + // } + // if witProgram != nil { + // var e error + // vm.witnessVersion, vm.witnessProgram, e = ExtractWitnessProgramI.Ln(witProgram) + // if e != nil { + // return nil, e + // } + // } else { + // // If we didn't find a witness program in either the pkScript or as a datapush + // // within the sigScript, then there MUST NOT be any witness data associated with + // // the input being validated. + // if vm.witnessProgram == nil && len(tx.TxIn[txIdx].Witness) != 0 { + // errStr := "non-witness inputs cannot have a witness" + // return nil, scriptError(ErrWitnessUnexpected, errStr) + // } + // } + // } + vm.tx = *tx + vm.txIdx = txIdx + return &vm, nil +} diff --git a/pkg/txscript/engine_test.go b/pkg/txscript/engine_test.go new file mode 100644 index 0000000..bc63fad --- /dev/null +++ b/pkg/txscript/engine_test.go @@ -0,0 +1,474 @@ +package txscript + +import ( + "testing" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// TestBadPC sets the pc to a deliberately bad result then confirms that Step() and Disasm fail correctly. +func TestBadPC(t *testing.T) { + t.Parallel() + tests := []struct { + script, off int + }{ + {script: 2, off: 0}, + {script: 0, off: 2}, + } + // tx with almost empty scripts. + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash( + [32]byte{ + 0xc9, 0x97, 0xa5, 0xe5, + 0x6e, 0x10, 0x41, 0x02, + 0xfa, 0x20, 0x9c, 0x6a, + 0x85, 0x2d, 0xd9, 0x06, + 0x60, 0xa2, 0x0b, 0x2d, + 0x9c, 0x35, 0x24, 0x23, + 0xed, 0xce, 0x25, 0x85, + 0x7f, 0xcd, 0x37, 0x04, + }, + ), + Index: 0, + }, + SignatureScript: mustParseShortForm("NOP"), + Sequence: 4294967295, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1000000000, + PkScript: nil, + }, + }, + LockTime: 0, + } + pkScript := mustParseShortForm("NOP") + for _, test := range tests { + vm, e := NewEngine(pkScript, tx, 0, 0, nil, nil, -1) + if e != nil { + t.Errorf("Failed to create script: %v", e) + } + // set to after all scripts + vm.scriptIdx.Store(int64(test.script)) + vm.scriptOff.Store(int64(test.off)) + _, e = vm.Step() + if e == nil { + t.Errorf("Step with invalid pc (%v) succeeds!", test) + continue + } + _, e = vm.DisasmPC() + if e == nil { + t.Errorf( + "DisasmPC with invalid pc (%v) succeeds!", + test, + ) + } + } +} + +// TestCheckErrorCondition tests the execute early test in CheckErrorCondition() since most code paths are tested +// elsewhere. +func TestCheckErrorCondition(t *testing.T) { + t.Parallel() + // tx with almost empty scripts. + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash( + [32]byte{ + 0xc9, 0x97, 0xa5, 0xe5, + 0x6e, 0x10, 0x41, 0x02, + 0xfa, 0x20, 0x9c, 0x6a, + 0x85, 0x2d, 0xd9, 0x06, + 0x60, 0xa2, 0x0b, 0x2d, + 0x9c, 0x35, 0x24, 0x23, + 0xed, 0xce, 0x25, 0x85, + 0x7f, 0xcd, 0x37, 0x04, + }, + ), + Index: 0, + }, + SignatureScript: nil, + Sequence: 4294967295, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1000000000, + PkScript: nil, + }, + }, + LockTime: 0, + } + pkScript := mustParseShortForm( + "NOP NOP NOP NOP NOP NOP NOP NOP NOP" + + " NOP TRUE", + ) + vm, e := NewEngine(pkScript, tx, 0, 0, nil, nil, 0) + if e != nil { + t.Errorf("failed to create script: %v", e) + } + for i := 0; i < len(pkScript)-1; i++ { + var done bool + done, e = vm.Step() + if e != nil { + t.Fatalf("failed to step %dth time: %v", i, e) + } + if done { + t.Fatalf("finshed early on %dth time", i) + } + e = vm.CheckErrorCondition(false) + if !IsErrorCode(e, ErrScriptUnfinished) { + t.Fatalf( + "got unexepected error %v on %dth iteration", + e, i, + ) + } + } + done, e := vm.Step() + if e != nil { + t.Fatalf("final step failed %v", e) + } + if !done { + t.Fatalf("final step isn't done!") + } + e = vm.CheckErrorCondition(false) + if e != nil { + t.Errorf("unexpected error %v on final check", e) + } +} + +// TestInvalidFlagCombinations ensures the script engine returns the expected error when disallowed flag combinations +// are specified. +func TestInvalidFlagCombinations(t *testing.T) { + t.Parallel() + tests := []ScriptFlags{ + ScriptVerifyCleanStack, + } + // tx with almost empty scripts. + tx := &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash( + [32]byte{ + 0xc9, 0x97, 0xa5, 0xe5, + 0x6e, 0x10, 0x41, 0x02, + 0xfa, 0x20, 0x9c, 0x6a, + 0x85, 0x2d, 0xd9, 0x06, + 0x60, 0xa2, 0x0b, 0x2d, + 0x9c, 0x35, 0x24, 0x23, + 0xed, 0xce, 0x25, 0x85, + 0x7f, 0xcd, 0x37, 0x04, + }, + ), + Index: 0, + }, + SignatureScript: []uint8{OP_NOP}, + Sequence: 4294967295, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 1000000000, + PkScript: nil, + }, + }, + LockTime: 0, + } + pkScript := []byte{OP_NOP} + for i, test := range tests { + _, e := NewEngine(pkScript, tx, 0, test, nil, nil, -1) + if !IsErrorCode(e, ErrInvalidFlags) { + t.Fatalf( + "TestInvalidFlagCombinations #%d unexpected "+ + "error: %v", i, e, + ) + } + } +} + +// TestCheckPubKeyEncoding ensures the internal checkPubKeyEncoding function works as expected. +func TestCheckPubKeyEncoding(t *testing.T) { + t.Parallel() + tests := []struct { + name string + key []byte + isValid bool + }{ + { + name: "uncompressed ok", + key: hexToBytes( + "0411db93e1dcdb8a016b49840f8c53bc1eb68" + + "a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf" + + "9744464f82e160bfa9b8b64f9d4c03f999b8643f656b" + + "412a3", + ), + isValid: true, + }, + { + name: "compressed ok", + key: hexToBytes( + "02ce0b14fb842b1ba549fdd675c98075f12e9" + + "c510f8ef52bd021a9a1f4809d3b4d", + ), + isValid: true, + }, + { + name: "compressed ok", + key: hexToBytes( + "032689c7c2dab13309fb143e0e8fe39634252" + + "1887e976690b6b47f5b2a4b7d448e", + ), + isValid: true, + }, + { + name: "hybrid", + key: hexToBytes( + "0679be667ef9dcbbac55a06295ce870b07029" + + "bfcdb2dce28d959f2815b16f81798483ada7726a3c46" + + "55da4fbfc0e1108a8fd17b448a68554199c47d08ffb1" + + "0d4b8", + ), + isValid: false, + }, + { + name: "empty", + key: nil, + isValid: false, + }, + } + vm := Engine{flags: ScriptVerifyStrictEncoding} + for _, test := range tests { + e := vm.checkPubKeyEncoding(test.key) + if e != nil && test.isValid { + t.Errorf( + "checkSignatureEncoding test '%s' failed "+ + "when it should have succeeded: %v", test.name, + e, + ) + } else if e == nil && !test.isValid { + t.Errorf( + "checkSignatureEncooding test '%s' succeeded "+ + "when it should have failed", test.name, + ) + } + } +} + +// TestCheckSignatureEncoding ensures the internal checkSignatureEncoding function works as expected. +func TestCheckSignatureEncoding(t *testing.T) { + t.Parallel() + tests := []struct { + name string + sig []byte + isValid bool + }{ + { + name: "valid signature", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: true, + }, + { + name: "empty.", + sig: nil, + isValid: false, + }, + { + name: "bad magic", + sig: hexToBytes( + "314402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "bad 1st int marker magic", + sig: hexToBytes( + "304403204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "bad 2nd int marker", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41032018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "short len", + sig: hexToBytes( + "304302204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "long len", + sig: hexToBytes( + "304502204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "long X", + sig: hexToBytes( + "304402424e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "long Y", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022118152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "short Y", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41021918152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "trailing crap", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d0901", + ), + isValid: false, + }, + { + name: "X == N ", + sig: hexToBytes( + "30440220fffffffffffffffffffffffffffff" + + "ffebaaedce6af48a03bbfd25e8cd0364141022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "X == N ", + sig: hexToBytes( + "30440220fffffffffffffffffffffffffffff" + + "ffebaaedce6af48a03bbfd25e8cd0364142022018152" + + "2ec8eca07de4860a4acdd12909d831cc56cbbac46220" + + "82221a8768d1d09", + ), + isValid: false, + }, + { + name: "Y == N", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd410220fffff" + + "ffffffffffffffffffffffffffebaaedce6af48a03bb" + + "fd25e8cd0364141", + ), + isValid: false, + }, + { + name: "Y > N", + sig: hexToBytes( + "304402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd410220fffff" + + "ffffffffffffffffffffffffffebaaedce6af48a03bb" + + "fd25e8cd0364142", + ), + isValid: false, + }, + { + name: "0 len X", + sig: hexToBytes( + "302402000220181522ec8eca07de4860a4acd" + + "d12909d831cc56cbbac4622082221a8768d1d09", + ), + isValid: false, + }, + { + name: "0 len Y", + sig: hexToBytes( + "302402204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd410200", + ), + isValid: false, + }, + { + name: "extra R padding", + sig: hexToBytes( + "30450221004e45e16932b8af514961a1d3a1a" + + "25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181" + + "522ec8eca07de4860a4acdd12909d831cc56cbbac462" + + "2082221a8768d1d09", + ), + isValid: false, + }, + { + name: "extra S padding", + sig: hexToBytes( + "304502204e45e16932b8af514961a1d3a1a25" + + "fdf3f4f7732e9d624c6c61548ab5fb8cd41022100181" + + "522ec8eca07de4860a4acdd12909d831cc56cbbac462" + + "2082221a8768d1d09", + ), + isValid: false, + }, + } + vm := Engine{flags: ScriptVerifyStrictEncoding} + for _, test := range tests { + e := vm.checkSignatureEncoding(test.sig) + if e != nil && test.isValid { + t.Errorf( + "checkSignatureEncoding test '%s' failed "+ + "when it should have succeeded: %v", test.name, + e, + ) + } else if e == nil && !test.isValid { + t.Errorf( + "checkSignatureEncooding test '%s' succeeded "+ + "when it should have failed", test.name, + ) + } + } +} diff --git a/pkg/txscript/error.go b/pkg/txscript/error.go new file mode 100644 index 0000000..8fdd36e --- /dev/null +++ b/pkg/txscript/error.go @@ -0,0 +1,366 @@ +package txscript + +import ( + "fmt" +) + +// ErrorCode identifies a kind of script error. +type ErrorCode int + +// These constants are used to identify a specific ScriptError. +const ( + // ErrInternal is returned if internal consistency checks fail. In practice + // this error should never be seen as it would mean there is an error in the + // engine logic. + ErrInternal ErrorCode = iota + + // Failures related to improper API usage. + + // ErrInvalidFlags is returned when the passed flags to NewEngine contain an + // invalid combination. + ErrInvalidFlags + // ErrInvalidIndex is returned when an out-of-bounds index is passed to a + // function. + ErrInvalidIndex + // ErrUnsupportedAddress is returned when a concrete type that implements a + // util.Address is not a supported type. + ErrUnsupportedAddress + // ErrNotMultisigScript is returned from CalcMultiSigStats when the provided + // script is not a multisig script. + ErrNotMultisigScript + // ErrTooManyRequiredSigs is returned from MultiSigScript when the specified + // number of required signatures is larger than the number of provided public + // keys. + ErrTooManyRequiredSigs + // ErrTooMuchNullData is returned from NullDataScript when the length of the + // provided data exceeds MaxDataCarrierSize. + ErrTooMuchNullData + + // Failures related to final execution state. + + // ErrEarlyReturn is returned when OP_RETURN is executed in the script. + ErrEarlyReturn + // ErrEmptyStack is returned when the script evaluated without error, but + // terminated with an empty top stack element. + ErrEmptyStack + // ErrEvalFalse is returned when the script evaluated without error but + // terminated with a false top stack element. + ErrEvalFalse + // ErrScriptUnfinished is returned when CheckErrorCondition is called on a + // script that has not finished executing. + ErrScriptUnfinished + // ErrInvalidProgramCounter is... + ErrInvalidProgramCounter + // ErrScriptDone is returned when an attempt to execute an opcode is made + // once all of them have already been executed. This can happen due to things + // such as a second call to Execute or calling Step after all opcodes have + // already been executed. + + // Failures related to exceeding maximum allowed limits. + + // ErrScriptTooBig is returned if a script is larger than MaxScriptSize. + ErrScriptTooBig + // ErrElementTooBig is returned if the size of an element to be pushed to the + // stack is over MaxScriptElementSize. + ErrElementTooBig + // ErrTooManyOperations is returned if a script has more than MaxOpsPerScript + // opcodes that do not push data. + ErrTooManyOperations + // ErrStackOverflow is returned when stack and altstack combined depth is + // over the limit. + ErrStackOverflow + // ErrInvalidPubKeyCount is returned when the number of public keys specified + // for a multsig is either negative or greater than MaxPubKeysPerMultiSig. + ErrInvalidPubKeyCount + // ErrInvalidSignatureCount is returned when the number of signatures + // specified for a multisig is either negative or greater than the number of + // public keys. + ErrInvalidSignatureCount + // ErrNumberTooBig is returned when the argument for an opcode that expects + // numeric input is larger than the expected maximum number of bytes. For the + // most part, opcodes that deal with stack manipulation via offsets, + // arithmetic, numeric comparison, and boolean logic are those that this + // applies to. However, any opcode that expects numeric input may fail with + // this code. + ErrNumberTooBig + + // Failures related to verification operations. + + // ErrVerify is returned when OP_VERIFY is encountered in a script and + // the top item on the data stack does not evaluate to true. + ErrVerify + // ErrEqualVerify is returned when OP_EQUALVERIFY is encountered in a script + // and the top item on the data stack does not evaluate to true. + ErrEqualVerify + // ErrNumEqualVerify is returned when OP_NUMEQUALVERIFY is encountered in a + // script and the top item on the data stack does not evaluate to true. + ErrNumEqualVerify + // ErrCheckSigVerify is returned when OP_CHECKSIGVERIFY is encountered in a + // script and the top item on the data stack does not evaluate to true. + ErrCheckSigVerify + // ErrCheckMultiSigVerify is returned when OP_CHECKMULTISIGVERIFY is + // encountered in a script and the top item on the data stack does not + // evaluate to true. + ErrCheckMultiSigVerify + // Failures related to improper use of opcodes. + + // ErrDisabledOpcode is returned when a disabled opcode is encountered in a + // script. + ErrDisabledOpcode + // ErrReservedOpcode is returned when an opcode marked as reserved is + // encountered in a script. + ErrReservedOpcode + // ErrMalformedPush is returned when a data push opcode tries to push more + // bytes than are left in the script. + ErrMalformedPush + // ErrInvalidStackOperation is returned when a stack operation is attempted + // with a number that is invalid for the current stack size. + ErrInvalidStackOperation + // ErrUnbalancedConditional is returned when an OP_ELSE or OP_ENDIF is + // encountered in a script without first having an OP_IF or OP_NOTIF or the + // end of script is reached without encountering an OP_ENDIF when an OP_IF or + // OP_NOTIF was previously encountered. + ErrUnbalancedConditional + + // Failures related to malleability. + + // ErrMinimalData is returned when the ScriptVerifyMinimalData flag is set + // and the script contains push operations that do not use the minimal opcode + // required. + ErrMinimalData + // ErrInvalidSigHashType is returned when a signature hash type is not one of + // the supported types. + ErrInvalidSigHashType + // ErrSigTooShort is returned when a signature that should be a + // canonically-encoded DER signature is too short. + ErrSigTooShort + // ErrSigTooLong is returned when a signature that should be a + // canonically-encoded DER signature is too long. + ErrSigTooLong + // ErrSigInvalidSeqID is returned when a signature that should be a + // canonically-encoded DER signature does not have the expected ASN.1 + // sequence ID. + ErrSigInvalidSeqID + // ErrSigInvalidDataLen is returned a signature that should be a + // canonically-encoded DER signature does not specify the correct number of + // remaining bytes for the R and S portions. + ErrSigInvalidDataLen + // ErrSigMissingSTypeID is returned a signature that should be a + // canonically-encoded DER signature does not provide the ASN.1 type ID for + // S. + ErrSigMissingSTypeID + // ErrSigMissingSLen is returned when a signature that should be a + // canonically-encoded DER signature does not provide the length of S. + ErrSigMissingSLen + // ErrSigInvalidSLen is returned a signature that should be a + // canonically-encoded DER signature does not specify the correct number of + // bytes for the S portion. + ErrSigInvalidSLen + // ErrSigInvalidRIntID is returned when a signature that should be a + // canonically-encoded DER signature does not have the expected ASN.1 integer + // ID for R. + ErrSigInvalidRIntID + // ErrSigZeroRLen is returned when a signature that should be a + // canonically-encoded DER signature has an R length of zero. + ErrSigZeroRLen + // ErrSigNegativeR is returned when a signature that should be a + // canonically-encoded DER signature has a negative value for R. + ErrSigNegativeR + // ErrSigTooMuchRPadding is returned when a signature that should be a + // canonically-encoded DER signature has too much padding for R. + ErrSigTooMuchRPadding + // ErrSigInvalidSIntID is returned when a signature that should be a + // canonically-encoded DER signature does not have the expected ASN.1 integer + // ID for S. + ErrSigInvalidSIntID + // ErrSigZeroSLen is returned when a signature that should be a + // canonically-encoded DER signature has an S length of zero. + ErrSigZeroSLen + // ErrSigNegativeS is returned when a signature that should be a + // canonically-encoded DER signature has a negative value for S. + ErrSigNegativeS + // ErrSigTooMuchSPadding is returned when a signature that should be a + // canonically-encoded DER signature has too much padding for S. + ErrSigTooMuchSPadding + // ErrSigHighS is returned when the ScriptVerifyLowS flag is set and the + // script contains any signatures whose S values are higher than the half + // order. + ErrSigHighS + // ErrNotPushOnly is returned when a script that is required to only push + // data to the stack performs other operations. A couple of cases where this + // applies is for a pay-to-script-hash signature script when bip16 is active + // and when the ScriptVerifySigPushOnly flag is set. + ErrNotPushOnly + // ErrSigNullDummy is returned when the ScriptStrictMultiSig flag is set and + // a multisig script has anything other than 0 for the extra dummy argument. + ErrSigNullDummy + // ErrPubKeyType is returned when the ScriptVerifyStrictEncoding flag is set + // and the script contains invalid public keys. + ErrPubKeyType + // ErrCleanStack is returned when the ScriptVerifyCleanStack flag is set, and + // after evalution, the stack does not contain only a single element. + ErrCleanStack + // ErrNullFail is returned when the ScriptVerifyNullFail flag is set and + // signatures are not empty on failed checksig or checkmultisig operations. + ErrNullFail + // ErrWitnessMalleated is returned if ScriptVerifyWitness is set and a native + // p2wsh program is encountered which has a non-empty sigScript. + ErrWitnessMalleated + // ErrWitnessMalleatedP2SH is returned if ScriptVerifyWitness if set and the + // validation logic for nested p2sh encounters a sigScript which isn't *exactyl* + // a datapush of the witness program. + ErrWitnessMalleatedP2SH + // Failures related to soft forks. + + // ErrDiscourageUpgradableNOPs is returned when the + // ScriptDiscourageUpgradableNops flag is set and a NOP opcode is encountered + // in a script. + ErrDiscourageUpgradableNOPs + // ErrNegativeLockTime is returned when a script contains an opcode that + // interprets a negative lock time. + ErrNegativeLockTime + // ErrUnsatisfiedLockTime is returned when a script contains an opcode that + // involves a lock time and the required lock time has not been reached. + ErrUnsatisfiedLockTime + // ErrMinimalIf is returned if ScriptVerifyWitness is set and the operand of an + // OP_IF/OP_NOF_IF are not either an empty vector or [0x01]. + ErrMinimalIf + // ErrDiscourageUpgradableWitnessProgram is returned if ScriptVerifyWitness is + // set and the versino of an executing witness program is outside the set of + // currently defined witness program vesions. + ErrDiscourageUpgradableWitnessProgram + // Failures related to segregated witness. + + // ErrWitnessProgramEmpty is returned if ScriptVerifyWitness is set and the + // witness stack itself is empty. + ErrWitnessProgramEmpty + // ErrWitnessProgramMismatch is returned if ScriptVerifyWitness is set and the + // witness itself for a p2wkh witness program isn't *exactly* 2 items or if the + // witness for a p2wsh isn't the sha255 of the witness script. + ErrWitnessProgramMismatch + // ErrWitnessProgramWrongLength is returned if ScriptVerifyWitness is set and + // the length of the witness program violates the length as dictated by the + // current witness version. + ErrWitnessProgramWrongLength + // ErrWitnessUnexpected is returned if ScriptVerifyWitness is set and a + // transaction includes witness data but doesn't spend an which is a witness + // program (nested or native). + ErrWitnessUnexpected + // ErrWitnessPubKeyType is returned if ScriptVerifyWitness is set and the public + // key used in either a check-sig or check-multi-sig isn't serialized in a + // compressed format. + ErrWitnessPubKeyType + // numErrorCodes is the maximum error code number used in tests. This entry MUST + // be the last entry in the enum. + numErrorCodes +) + +// Map of ErrorCode values back to their constant names for pretty printing. +var errorCodeStrings = map[ErrorCode]string{ + ErrInternal: "ErrInternal", + ErrInvalidFlags: "ErrInvalidFlags", + ErrInvalidIndex: "ErrInvalidIndex", + ErrUnsupportedAddress: "ErrUnsupportedAddress", + ErrNotMultisigScript: "ErrNotMultisigScript", + ErrTooManyRequiredSigs: "ErrTooManyRequiredSigs", + ErrTooMuchNullData: "ErrTooMuchNullData", + ErrEarlyReturn: "ErrEarlyReturn", + ErrEmptyStack: "ErrEmptyStack", + ErrEvalFalse: "ErrEvalFalse", + ErrScriptUnfinished: "ErrScriptUnfinished", + ErrInvalidProgramCounter: "ErrInvalidProgramCounter", + ErrScriptTooBig: "ErrScriptTooBig", + ErrElementTooBig: "ErrElementTooBig", + ErrTooManyOperations: "ErrTooManyOperations", + ErrStackOverflow: "ErrStackOverflow", + ErrInvalidPubKeyCount: "ErrInvalidPubKeyCount", + ErrInvalidSignatureCount: "ErrInvalidSignatureCount", + ErrNumberTooBig: "ErrNumberTooBig", + ErrVerify: "ErrVerify", + ErrEqualVerify: "ErrEqualVerify", + ErrNumEqualVerify: "ErrNumEqualVerify", + ErrCheckSigVerify: "ErrCheckSigVerify", + ErrCheckMultiSigVerify: "ErrCheckMultiSigVerify", + ErrDisabledOpcode: "ErrDisabledOpcode", + ErrReservedOpcode: "ErrReservedOpcode", + ErrMalformedPush: "ErrMalformedPush", + ErrInvalidStackOperation: "ErrInvalidStackOperation", + ErrUnbalancedConditional: "ErrUnbalancedConditional", + ErrMinimalData: "ErrMinimalData", + ErrInvalidSigHashType: "ErrInvalidSigHashType", + ErrSigTooShort: "ErrSigTooShort", + ErrSigTooLong: "ErrSigTooLong", + ErrSigInvalidSeqID: "ErrSigInvalidSeqID", + ErrSigInvalidDataLen: "ErrSigInvalidDataLen", + ErrSigMissingSTypeID: "ErrSigMissingSTypeID", + ErrSigMissingSLen: "ErrSigMissingSLen", + ErrSigInvalidSLen: "ErrSigInvalidSLen", + ErrSigInvalidRIntID: "ErrSigInvalidRIntID", + ErrSigZeroRLen: "ErrSigZeroRLen", + ErrSigNegativeR: "ErrSigNegativeR", + ErrSigTooMuchRPadding: "ErrSigTooMuchRPadding", + ErrSigInvalidSIntID: "ErrSigInvalidSIntID", + ErrSigZeroSLen: "ErrSigZeroSLen", + ErrSigNegativeS: "ErrSigNegativeS", + ErrSigTooMuchSPadding: "ErrSigTooMuchSPadding", + ErrSigHighS: "ErrSigHighS", + ErrNotPushOnly: "ErrNotPushOnly", + ErrSigNullDummy: "ErrSigNullDummy", + ErrPubKeyType: "ErrPubKeyType", + ErrCleanStack: "ErrCleanStack", + ErrNullFail: "ErrNullFail", + ErrDiscourageUpgradableNOPs: "ErrDiscourageUpgradableNOPs", + ErrNegativeLockTime: "ErrNegativeLockTime", + ErrUnsatisfiedLockTime: "ErrUnsatisfiedLockTime", + ErrWitnessProgramEmpty: "ErrWitnessProgramEmpty", + ErrWitnessProgramMismatch: "ErrWitnessProgramMismatch", + ErrWitnessProgramWrongLength: "ErrWitnessProgramWrongLength", + ErrWitnessMalleated: "ErrWitnessMalleated", + ErrWitnessMalleatedP2SH: "ErrWitnessMalleatedP2SH", + ErrWitnessUnexpected: "ErrWitnessUnexpected", + ErrMinimalIf: "ErrMinimalIf", + ErrWitnessPubKeyType: "ErrWitnessPubKeyType", + ErrDiscourageUpgradableWitnessProgram: "ErrDiscourageUpgradableWitnessProgram", +} + +// String returns the ErrorCode as a human-readable name. +func (e ErrorCode) String() string { + if s := errorCodeStrings[e]; s != "" { + return s + } + return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) +} + +// ScriptError identifies a script-related error. It is used to indicate three +// classes of errors: +// +// 1) Script execution failures due to violating one of the many requirements +// imposed by the script engine or evaluating to false +// 2) Improper API usage by callers +// 3) Internal consistency check failures +// +// The caller can use type assertions on the returned errors to access the +// ErrorCode field to ascertain the specific reason for the error. As an +// additional convenience, the caller may make use of the IsErrorCode function +// to check for a specific error code. +type ScriptError struct { + ErrorCode ErrorCode + Description string +} + +// ScriptError satisfies the error interface and prints human-readable errors. +func (e ScriptError) Error() string { + return e.Description +} + +// scriptError creates an ScriptError given a set of arguments. +func scriptError(c ErrorCode, desc string) ScriptError { + return ScriptError{ErrorCode: c, Description: desc} +} + +// IsErrorCode returns whether or not the provided error is a script error with +// the provided error code. +func IsErrorCode(e error, c ErrorCode) bool { + serr, ok := e.(ScriptError) + return ok && serr.ErrorCode == c +} diff --git a/pkg/txscript/error_test.go b/pkg/txscript/error_test.go new file mode 100644 index 0000000..fe7aeb9 --- /dev/null +++ b/pkg/txscript/error_test.go @@ -0,0 +1,124 @@ +package txscript + +import ( + "testing" +) + +// TestErrorCodeStringer tests the stringized output for the ErrorCode type. +func TestErrorCodeStringer(t *testing.T) { + t.Parallel() + tests := []struct { + in ErrorCode + want string + }{ + {ErrInternal, "ErrInternal"}, + {ErrInvalidFlags, "ErrInvalidFlags"}, + {ErrInvalidIndex, "ErrInvalidIndex"}, + {ErrUnsupportedAddress, "ErrUnsupportedAddress"}, + {ErrTooManyRequiredSigs, "ErrTooManyRequiredSigs"}, + {ErrTooMuchNullData, "ErrTooMuchNullData"}, + {ErrNotMultisigScript, "ErrNotMultisigScript"}, + {ErrEarlyReturn, "ErrEarlyReturn"}, + {ErrEmptyStack, "ErrEmptyStack"}, + {ErrEvalFalse, "ErrEvalFalse"}, + {ErrScriptUnfinished, "ErrScriptUnfinished"}, + {ErrInvalidProgramCounter, "ErrInvalidProgramCounter"}, + {ErrScriptTooBig, "ErrScriptTooBig"}, + {ErrElementTooBig, "ErrElementTooBig"}, + {ErrTooManyOperations, "ErrTooManyOperations"}, + {ErrStackOverflow, "ErrStackOverflow"}, + {ErrInvalidPubKeyCount, "ErrInvalidPubKeyCount"}, + {ErrInvalidSignatureCount, "ErrInvalidSignatureCount"}, + {ErrNumberTooBig, "ErrNumberTooBig"}, + {ErrVerify, "ErrVerify"}, + {ErrEqualVerify, "ErrEqualVerify"}, + {ErrNumEqualVerify, "ErrNumEqualVerify"}, + {ErrCheckSigVerify, "ErrCheckSigVerify"}, + {ErrCheckMultiSigVerify, "ErrCheckMultiSigVerify"}, + {ErrDisabledOpcode, "ErrDisabledOpcode"}, + {ErrReservedOpcode, "ErrReservedOpcode"}, + {ErrMalformedPush, "ErrMalformedPush"}, + {ErrInvalidStackOperation, "ErrInvalidStackOperation"}, + {ErrUnbalancedConditional, "ErrUnbalancedConditional"}, + {ErrMinimalData, "ErrMinimalData"}, + {ErrInvalidSigHashType, "ErrInvalidSigHashType"}, + {ErrSigTooShort, "ErrSigTooShort"}, + {ErrSigTooLong, "ErrSigTooLong"}, + {ErrSigInvalidSeqID, "ErrSigInvalidSeqID"}, + {ErrSigInvalidDataLen, "ErrSigInvalidDataLen"}, + {ErrSigMissingSTypeID, "ErrSigMissingSTypeID"}, + {ErrSigMissingSLen, "ErrSigMissingSLen"}, + {ErrSigInvalidSLen, "ErrSigInvalidSLen"}, + {ErrSigInvalidRIntID, "ErrSigInvalidRIntID"}, + {ErrSigZeroRLen, "ErrSigZeroRLen"}, + {ErrSigNegativeR, "ErrSigNegativeR"}, + {ErrSigTooMuchRPadding, "ErrSigTooMuchRPadding"}, + {ErrSigInvalidSIntID, "ErrSigInvalidSIntID"}, + {ErrSigZeroSLen, "ErrSigZeroSLen"}, + {ErrSigNegativeS, "ErrSigNegativeS"}, + {ErrSigTooMuchSPadding, "ErrSigTooMuchSPadding"}, + {ErrSigHighS, "ErrSigHighS"}, + {ErrNotPushOnly, "ErrNotPushOnly"}, + {ErrSigNullDummy, "ErrSigNullDummy"}, + {ErrPubKeyType, "ErrPubKeyType"}, + {ErrCleanStack, "ErrCleanStack"}, + {ErrNullFail, "ErrNullFail"}, + {ErrDiscourageUpgradableNOPs, "ErrDiscourageUpgradableNOPs"}, + {ErrNegativeLockTime, "ErrNegativeLockTime"}, + {ErrUnsatisfiedLockTime, "ErrUnsatisfiedLockTime"}, + {ErrWitnessProgramEmpty, "ErrWitnessProgramEmpty"}, + {ErrWitnessProgramMismatch, "ErrWitnessProgramMismatch"}, + {ErrWitnessProgramWrongLength, "ErrWitnessProgramWrongLength"}, + {ErrWitnessMalleated, "ErrWitnessMalleated"}, + {ErrWitnessMalleatedP2SH, "ErrWitnessMalleatedP2SH"}, + {ErrWitnessUnexpected, "ErrWitnessUnexpected"}, + {ErrMinimalIf, "ErrMinimalIf"}, + {ErrWitnessPubKeyType, "ErrWitnessPubKeyType"}, + {ErrDiscourageUpgradableWitnessProgram, "ErrDiscourageUpgradableWitnessProgram"}, + {0xffff, "Unknown ErrorCode (65535)"}, + } + // Detect additional error codes that don't have the stringer added. + if len(tests)-1 != int(numErrorCodes) { + t.Errorf("It appears an error code was added without adding an " + + "associated stringer test", + ) + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.String() + if result != test.want { + t.Errorf("String #%d\n got: %s want: %s", i, result, + test.want, + ) + continue + } + } +} + +// TestError tests the error output for the ScriptError type. +func TestError(t *testing.T) { + t.Parallel() + tests := []struct { + in ScriptError + want string + }{ + { + ScriptError{Description: "some error"}, + "some error", + }, + { + ScriptError{Description: "human-readable error"}, + "human-readable error", + }, + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("ScriptError #%d\n got: %s want: %s", i, result, + test.want, + ) + continue + } + } +} diff --git a/pkg/txscript/hashcache.go b/pkg/txscript/hashcache.go new file mode 100644 index 0000000..1375b87 --- /dev/null +++ b/pkg/txscript/hashcache.go @@ -0,0 +1,76 @@ +package txscript + +import ( + "sync" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// TxSigHashes houses the partial set of sighashes introduced within BIP0143. This partial set of sighashes may be +// re-used within each input across a transaction when validating all inputs. As a result, validation complexity for +// SigHashAll can be reduced by a polynomial factor. +type TxSigHashes struct { + HashPrevOuts chainhash.Hash + HashSequence chainhash.Hash + HashOutputs chainhash.Hash +} + +// NewTxSigHashes computes, and returns the cached sighashes of the given transaction. +func NewTxSigHashes(tx *wire.MsgTx) *TxSigHashes { + return &TxSigHashes{ + HashPrevOuts: calcHashPrevOuts(tx), + HashSequence: calcHashSequence(tx), + HashOutputs: calcHashOutputs(tx), + } +} + +// HashCache houses a set of partial sighashes keyed by txid. The set of partial sighashes are those introduced within +// BIP0143 by the new more efficient sighash digest calculation algorithm. Using this threadsafe shared cache, multiple +// goroutines can safely re-use the pre-computed partial sighashes speeding up validation time amongst all inputs found +// within a block. +type HashCache struct { + sigHashes map[chainhash.Hash]*TxSigHashes + sync.RWMutex +} + +// NewHashCache returns a new instance of the HashCache given a maximum number of entries which may exist within it at +// anytime. +func NewHashCache(maxSize uint) *HashCache { + return &HashCache{ + sigHashes: make(map[chainhash.Hash]*TxSigHashes, maxSize), + } +} + +// AddSigHashes computes, then adds the partial sighashes for the passed transaction. +func (h *HashCache) AddSigHashes(tx *wire.MsgTx) { + h.Lock() + h.sigHashes[tx.TxHash()] = NewTxSigHashes(tx) + h.Unlock() +} + +// ContainsHashes returns true if the partial sighashes for the passed transaction currently exist within the HashCache, +// and false otherwise. +func (h *HashCache) ContainsHashes(txid *chainhash.Hash) bool { + h.RLock() + _, found := h.sigHashes[*txid] + h.RUnlock() + return found +} + +// GetSigHashes possibly returns the previously cached partial sighashes for the passed transaction. This function also +// returns an additional boolean value indicating if the sighashes for the passed transaction were found to be present +// within the HashCache. +func (h *HashCache) GetSigHashes(txid *chainhash.Hash) (*TxSigHashes, bool) { + h.RLock() + item, found := h.sigHashes[*txid] + h.RUnlock() + return item, found +} + +// PurgeSigHashes removes all partial sighashes from the HashCache belonging to the passed transaction. +func (h *HashCache) PurgeSigHashes(txid *chainhash.Hash) { + h.Lock() + delete(h.sigHashes, *txid) + h.Unlock() +} diff --git a/pkg/txscript/hashcache_test.go b/pkg/txscript/hashcache_test.go new file mode 100644 index 0000000..d2b95cf --- /dev/null +++ b/pkg/txscript/hashcache_test.go @@ -0,0 +1,151 @@ +package txscript + +import ( + "math/rand" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + + "github.com/p9c/p9/pkg/wire" +) + +// genTestTx creates a random transaction for uses within test cases. +func genTestTx() (*wire.MsgTx, error) { + tx := wire.NewMsgTx(2) + tx.Version = rand.Int31() + numTxins := rand.Intn(11) + for i := 0; i < numTxins; i++ { + randTxIn := wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Index: uint32(rand.Int31()), + }, + Sequence: uint32(rand.Int31()), + } + _, e := rand.Read(randTxIn.PreviousOutPoint.Hash[:]) + if e != nil { + return nil, e + } + tx.TxIn = append(tx.TxIn, &randTxIn) + } + numTxouts := rand.Intn(11) + for i := 0; i < numTxouts; i++ { + randTxOut := wire.TxOut{ + Value: rand.Int63(), + PkScript: make([]byte, rand.Intn(30)), + } + var e error + if _, e = rand.Read(randTxOut.PkScript); E.Chk(e) { + return nil, e + } + tx.TxOut = append(tx.TxOut, &randTxOut) + } + return tx, nil +} + +// TestHashCacheAddContainsHashes tests that after items have been added to the hash cache, the ContainsHashes method +// returns true for all the items inserted. Conversely, ContainsHashes should return false for any items _not_ in the +// hash cache. +func TestHashCacheAddContainsHashes(t *testing.T) { + t.Parallel() + rand.Seed(time.Now().Unix()) + cache := NewHashCache(10) + var e error + // First, we'll generate 10 random transactions for use within our tests. + const numTxns = 10 + txns := make([]*wire.MsgTx, numTxns) + for i := 0; i < numTxns; i++ { + txns[i], e = genTestTx() + if e != nil { + t.Fatalf("unable to generate test tx: %v", e) + } + } + // With the transactions generated, we'll add each of them to the hash cache. + for _, tx := range txns { + cache.AddSigHashes(tx) + } + // Next, we'll ensure that each of the transactions inserted into the cache are properly located by the + // ContainsHashes method. + for _, tx := range txns { + txid := tx.TxHash() + if ok := cache.ContainsHashes(&txid); !ok { + t.Fatalf("txid %v not found in cache but should be: ", + txid, + ) + } + } + randTx, e := genTestTx() + if e != nil { + t.Fatalf("unable to generate tx: %v", e) + } + // Finally, we'll assert that a transaction that wasn't added to the cache won't be reported as being present by the + // ContainsHashes method. + randTxid := randTx.TxHash() + if ok := cache.ContainsHashes(&randTxid); ok { + t.Fatalf("txid %v wasn't inserted into cache but was found", + randTxid, + ) + } +} + +// TestHashCacheAddGet tests that the sighashes for a particular transaction are properly retrieved by the GetSigHashes +// function. +func TestHashCacheAddGet(t *testing.T) { + t.Parallel() + rand.Seed(time.Now().Unix()) + cache := NewHashCache(10) + // To start, we'll generate a random transaction and compute the set of sighashes for the transaction. + randTx, e := genTestTx() + if e != nil { + t.Fatalf("unable to generate tx: %v", e) + } + sigHashes := NewTxSigHashes(randTx) + // Next, add the transaction to the hash cache. + cache.AddSigHashes(randTx) + // The transaction inserted into the cache above should be found. + txid := randTx.TxHash() + cacheHashes, ok := cache.GetSigHashes(&txid) + if !ok { + t.Fatalf("tx %v wasn't found in cache", txid) + } + // Finally, the sighashes retrieved should exactly match the sighash originally inserted into the cache. + if *sigHashes != *cacheHashes { + t.Fatalf("sighashes don't match: expected %v, got %v", + spew.Sdump(sigHashes), spew.Sdump(cacheHashes), + ) + } +} + +// TestHashCachePurge tests that items are able to be properly removed from the hash cache. +func TestHashCachePurge(t *testing.T) { + t.Parallel() + rand.Seed(time.Now().Unix()) + cache := NewHashCache(10) + var e error + // First we'll start by inserting numTxns transactions into the hash cache. + const numTxns = 10 + txns := make([]*wire.MsgTx, numTxns) + for i := 0; i < numTxns; i++ { + txns[i], e = genTestTx() + if e != nil { + t.Fatalf("unable to generate test tx: %v", e) + } + } + for _, tx := range txns { + cache.AddSigHashes(tx) + } + // Once all the transactions have been inserted, we'll purge them from the hash cache. + for _, tx := range txns { + txid := tx.TxHash() + cache.PurgeSigHashes(&txid) + } + // At this point, none of the transactions inserted into the hash cache should be found within the cache. + for _, tx := range txns { + txid := tx.TxHash() + if ok := cache.ContainsHashes(&txid); ok { + t.Fatalf("tx %v found in cache but should have "+ + "been purged: ", txid, + ) + } + } +} diff --git a/pkg/txscript/log.go b/pkg/txscript/log.go new file mode 100644 index 0000000..c4cc1f2 --- /dev/null +++ b/pkg/txscript/log.go @@ -0,0 +1,43 @@ +package txscript + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/txscript/opcode.go b/pkg/txscript/opcode.go new file mode 100644 index 0000000..7879eb6 --- /dev/null +++ b/pkg/txscript/opcode.go @@ -0,0 +1,2312 @@ +package txscript + +import ( + "bytes" + "crypto/sha1" + "crypto/sha256" + "encoding/binary" + "fmt" + "hash" + + "golang.org/x/crypto/ripemd160" + + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/wire" +) + +// An opcode defines the information related to a txscript opcode. opfunc, if present, is the function to call to +// perform the opcode on the script. The current script is passed in as a slice with the first member being the opcode +// itself. +type opcode struct { + value byte + name string + length int + opfunc func(*parsedOpcode, *Engine) error +} + +// These constants are the values of the official opcodes used on the btc +// wiki, in bitcoin core and in most if not all other references and software +// related to handling DUO scripts. + +const ( + OP_0 = 0x00 // 0 + OP_FALSE = 0x00 // 0 - AKA OP_0 + OP_DATA_1 = 0x01 // 1 + OP_DATA_2 = 0x02 // 2 + OP_DATA_3 = 0x03 // 3 + OP_DATA_4 = 0x04 // 4 + OP_DATA_5 = 0x05 // 5 + OP_DATA_6 = 0x06 // 6 + OP_DATA_7 = 0x07 // 7 + OP_DATA_8 = 0x08 // 8 + OP_DATA_9 = 0x09 // 9 + OP_DATA_10 = 0x0a // 10 + OP_DATA_11 = 0x0b // 11 + OP_DATA_12 = 0x0c // 12 + OP_DATA_13 = 0x0d // 13 + OP_DATA_14 = 0x0e // 14 + OP_DATA_15 = 0x0f // 15 + OP_DATA_16 = 0x10 // 16 + OP_DATA_17 = 0x11 // 17 + OP_DATA_18 = 0x12 // 18 + OP_DATA_19 = 0x13 // 19 + OP_DATA_20 = 0x14 // 20 + OP_DATA_21 = 0x15 // 21 + OP_DATA_22 = 0x16 // 22 + OP_DATA_23 = 0x17 // 23 + OP_DATA_24 = 0x18 // 24 + OP_DATA_25 = 0x19 // 25 + OP_DATA_26 = 0x1a // 26 + OP_DATA_27 = 0x1b // 27 + OP_DATA_28 = 0x1c // 28 + OP_DATA_29 = 0x1d // 29 + OP_DATA_30 = 0x1e // 30 + OP_DATA_31 = 0x1f // 31 + OP_DATA_32 = 0x20 // 32 + OP_DATA_33 = 0x21 // 33 + OP_DATA_34 = 0x22 // 34 + OP_DATA_35 = 0x23 // 35 + OP_DATA_36 = 0x24 // 36 + OP_DATA_37 = 0x25 // 37 + OP_DATA_38 = 0x26 // 38 + OP_DATA_39 = 0x27 // 39 + OP_DATA_40 = 0x28 // 40 + OP_DATA_41 = 0x29 // 41 + OP_DATA_42 = 0x2a // 42 + OP_DATA_43 = 0x2b // 43 + OP_DATA_44 = 0x2c // 44 + OP_DATA_45 = 0x2d // 45 + OP_DATA_46 = 0x2e // 46 + OP_DATA_47 = 0x2f // 47 + OP_DATA_48 = 0x30 // 48 + OP_DATA_49 = 0x31 // 49 + OP_DATA_50 = 0x32 // 50 + OP_DATA_51 = 0x33 // 51 + OP_DATA_52 = 0x34 // 52 + OP_DATA_53 = 0x35 // 53 + OP_DATA_54 = 0x36 // 54 + OP_DATA_55 = 0x37 // 55 + OP_DATA_56 = 0x38 // 56 + OP_DATA_57 = 0x39 // 57 + OP_DATA_58 = 0x3a // 58 + OP_DATA_59 = 0x3b // 59 + OP_DATA_60 = 0x3c // 60 + OP_DATA_61 = 0x3d // 61 + OP_DATA_62 = 0x3e // 62 + OP_DATA_63 = 0x3f // 63 + OP_DATA_64 = 0x40 // 64 + OP_DATA_65 = 0x41 // 65 + OP_DATA_66 = 0x42 // 66 + OP_DATA_67 = 0x43 // 67 + OP_DATA_68 = 0x44 // 68 + OP_DATA_69 = 0x45 // 69 + OP_DATA_70 = 0x46 // 70 + OP_DATA_71 = 0x47 // 71 + OP_DATA_72 = 0x48 // 72 + OP_DATA_73 = 0x49 // 73 + OP_DATA_74 = 0x4a // 74 + OP_DATA_75 = 0x4b // 75 + OP_PUSHDATA1 = 0x4c // 76 + OP_PUSHDATA2 = 0x4d // 77 + OP_PUSHDATA4 = 0x4e // 78 + OP_1NEGATE = 0x4f // 79 + OP_RESERVED = 0x50 // 80 + OP_1 = 0x51 // 81 - AKA OP_TRUE + OP_TRUE = 0x51 // 81 + OP_2 = 0x52 // 82 + OP_3 = 0x53 // 83 + OP_4 = 0x54 // 84 + OP_5 = 0x55 // 85 + OP_6 = 0x56 // 86 + OP_7 = 0x57 // 87 + OP_8 = 0x58 // 88 + OP_9 = 0x59 // 89 + OP_10 = 0x5a // 90 + OP_11 = 0x5b // 91 + OP_12 = 0x5c // 92 + OP_13 = 0x5d // 93 + OP_14 = 0x5e // 94 + OP_15 = 0x5f // 95 + OP_16 = 0x60 // 96 + OP_NOP = 0x61 // 97 + OP_VER = 0x62 // 98 + OP_IF = 0x63 // 99 + OP_NOTIF = 0x64 // 100 + OP_VERIF = 0x65 // 101 + OP_VERNOTIF = 0x66 // 102 + OP_ELSE = 0x67 // 103 + OP_ENDIF = 0x68 // 104 + OP_VERIFY = 0x69 // 105 + OP_RETURN = 0x6a // 106 + OP_TOALTSTACK = 0x6b // 107 + OP_FROMALTSTACK = 0x6c // 108 + OP_2DROP = 0x6d // 109 + OP_2DUP = 0x6e // 110 + OP_3DUP = 0x6f // 111 + OP_2OVER = 0x70 // 112 + OP_2ROT = 0x71 // 113 + OP_2SWAP = 0x72 // 114 + OP_IFDUP = 0x73 // 115 + OP_DEPTH = 0x74 // 116 + OP_DROP = 0x75 // 117 + OP_DUP = 0x76 // 118 + OP_NIP = 0x77 // 119 + OP_OVER = 0x78 // 120 + OP_PICK = 0x79 // 121 + OP_ROLL = 0x7a // 122 + OP_ROT = 0x7b // 123 + OP_SWAP = 0x7c // 124 + OP_TUCK = 0x7d // 125 + OP_CAT = 0x7e // 126 + OP_SUBSTR = 0x7f // 127 + OP_LEFT = 0x80 // 128 + OP_RIGHT = 0x81 // 129 + OP_SIZE = 0x82 // 130 + OP_INVERT = 0x83 // 131 + OP_AND = 0x84 // 132 + OP_OR = 0x85 // 133 + OP_XOR = 0x86 // 134 + OP_EQUAL = 0x87 // 135 + OP_EQUALVERIFY = 0x88 // 136 + OP_RESERVED1 = 0x89 // 137 + OP_RESERVED2 = 0x8a // 138 + OP_1ADD = 0x8b // 139 + OP_1SUB = 0x8c // 140 + OP_2MUL = 0x8d // 141 + OP_2DIV = 0x8e // 142 + OP_NEGATE = 0x8f // 143 + OP_ABS = 0x90 // 144 + OP_NOT = 0x91 // 145 + OP_0NOTEQUAL = 0x92 // 146 + OP_ADD = 0x93 // 147 + OP_SUB = 0x94 // 148 + OP_MUL = 0x95 // 149 + OP_DIV = 0x96 // 150 + OP_MOD = 0x97 // 151 + OP_LSHIFT = 0x98 // 152 + OP_RSHIFT = 0x99 // 153 + OP_BOOLAND = 0x9a // 154 + OP_BOOLOR = 0x9b // 155 + OP_NUMEQUAL = 0x9c // 156 + OP_NUMEQUALVERIFY = 0x9d // 157 + OP_NUMNOTEQUAL = 0x9e // 158 + OP_LESSTHAN = 0x9f // 159 + OP_GREATERTHAN = 0xa0 // 160 + OP_LESSTHANOREQUAL = 0xa1 // 161 + OP_GREATERTHANOREQUAL = 0xa2 // 162 + OP_MIN = 0xa3 // 163 + OP_MAX = 0xa4 // 164 + OP_WITHIN = 0xa5 // 165 + OP_RIPEMD160 = 0xa6 // 166 + OP_SHA1 = 0xa7 // 167 + OP_SHA256 = 0xa8 // 168 + OP_HASH160 = 0xa9 // 169 + OP_HASH256 = 0xaa // 170 + OP_CODESEPARATOR = 0xab // 171 + OP_CHECKSIG = 0xac // 172 + OP_CHECKSIGVERIFY = 0xad // 173 + OP_CHECKMULTISIG = 0xae // 174 + OP_CHECKMULTISIGVERIFY = 0xaf // 175 + OP_NOP1 = 0xb0 // 176 + OP_NOP2 = 0xb1 // 177 + OP_CHECKLOCKTIMEVERIFY = 0xb1 // 177 - AKA OP_NOP2 + OP_NOP3 = 0xb2 // 178 + OP_CHECKSEQUENCEVERIFY = 0xb2 // 178 - AKA OP_NOP3 + OP_NOP4 = 0xb3 // 179 + OP_NOP5 = 0xb4 // 180 + OP_NOP6 = 0xb5 // 181 + OP_NOP7 = 0xb6 // 182 + OP_NOP8 = 0xb7 // 183 + OP_NOP9 = 0xb8 // 184 + OP_NOP10 = 0xb9 // 185 + OP_UNKNOWN186 = 0xba // 186 + OP_UNKNOWN187 = 0xbb // 187 + OP_UNKNOWN188 = 0xbc // 188 + OP_UNKNOWN189 = 0xbd // 189 + OP_UNKNOWN190 = 0xbe // 190 + OP_UNKNOWN191 = 0xbf // 191 + OP_UNKNOWN192 = 0xc0 // 192 + OP_UNKNOWN193 = 0xc1 // 193 + OP_UNKNOWN194 = 0xc2 // 194 + OP_UNKNOWN195 = 0xc3 // 195 + OP_UNKNOWN196 = 0xc4 // 196 + OP_UNKNOWN197 = 0xc5 // 197 + OP_UNKNOWN198 = 0xc6 // 198 + OP_UNKNOWN199 = 0xc7 // 199 + OP_UNKNOWN200 = 0xc8 // 200 + OP_UNKNOWN201 = 0xc9 // 201 + OP_UNKNOWN202 = 0xca // 202 + OP_UNKNOWN203 = 0xcb // 203 + OP_UNKNOWN204 = 0xcc // 204 + OP_UNKNOWN205 = 0xcd // 205 + OP_UNKNOWN206 = 0xce // 206 + OP_UNKNOWN207 = 0xcf // 207 + OP_UNKNOWN208 = 0xd0 // 208 + OP_UNKNOWN209 = 0xd1 // 209 + OP_UNKNOWN210 = 0xd2 // 210 + OP_UNKNOWN211 = 0xd3 // 211 + OP_UNKNOWN212 = 0xd4 // 212 + OP_UNKNOWN213 = 0xd5 // 213 + OP_UNKNOWN214 = 0xd6 // 214 + OP_UNKNOWN215 = 0xd7 // 215 + OP_UNKNOWN216 = 0xd8 // 216 + OP_UNKNOWN217 = 0xd9 // 217 + OP_UNKNOWN218 = 0xda // 218 + OP_UNKNOWN219 = 0xdb // 219 + OP_UNKNOWN220 = 0xdc // 220 + OP_UNKNOWN221 = 0xdd // 221 + OP_UNKNOWN222 = 0xde // 222 + OP_UNKNOWN223 = 0xdf // 223 + OP_UNKNOWN224 = 0xe0 // 224 + OP_UNKNOWN225 = 0xe1 // 225 + OP_UNKNOWN226 = 0xe2 // 226 + OP_UNKNOWN227 = 0xe3 // 227 + OP_UNKNOWN228 = 0xe4 // 228 + OP_UNKNOWN229 = 0xe5 // 229 + OP_UNKNOWN230 = 0xe6 // 230 + OP_UNKNOWN231 = 0xe7 // 231 + OP_UNKNOWN232 = 0xe8 // 232 + OP_UNKNOWN233 = 0xe9 // 233 + OP_UNKNOWN234 = 0xea // 234 + OP_UNKNOWN235 = 0xeb // 235 + OP_UNKNOWN236 = 0xec // 236 + OP_UNKNOWN237 = 0xed // 237 + OP_UNKNOWN238 = 0xee // 238 + OP_UNKNOWN239 = 0xef // 239 + OP_UNKNOWN240 = 0xf0 // 240 + OP_UNKNOWN241 = 0xf1 // 241 + OP_UNKNOWN242 = 0xf2 // 242 + OP_UNKNOWN243 = 0xf3 // 243 + OP_UNKNOWN244 = 0xf4 // 244 + OP_UNKNOWN245 = 0xf5 // 245 + OP_UNKNOWN246 = 0xf6 // 246 + OP_UNKNOWN247 = 0xf7 // 247 + OP_UNKNOWN248 = 0xf8 // 248 + OP_UNKNOWN249 = 0xf9 // 249 + OP_SMALLINTEGER = 0xfa // 250 - bitcoin core internal + OP_PUBKEYS = 0xfb // 251 - bitcoin core internal + OP_UNKNOWN252 = 0xfc // 252 + OP_PUBKEYHASH = 0xfd // 253 - bitcoin core internal + OP_PUBKEY = 0xfe // 254 - bitcoin core internal + OP_INVALIDOPCODE = 0xff // 255 - bitcoin core internal +) + +const ( // Conditional execution constants. + OpCondFalse = 0 + OpCondTrue = 1 + OpCondSkip = 2 +) + +// OpcodeArray holds details about all possible opcodes such as how many bytes the opcode and any associated data should +// take, its human-readable name, and the handler function. +var OpcodeArray = [256]opcode{ + // Data push opcodes. + OP_FALSE: {OP_FALSE, "OP_0", 1, opcodeFalse}, + OP_DATA_1: {OP_DATA_1, "OP_DATA_1", 2, opcodePushData}, + OP_DATA_2: {OP_DATA_2, "OP_DATA_2", 3, opcodePushData}, + OP_DATA_3: {OP_DATA_3, "OP_DATA_3", 4, opcodePushData}, + OP_DATA_4: {OP_DATA_4, "OP_DATA_4", 5, opcodePushData}, + OP_DATA_5: {OP_DATA_5, "OP_DATA_5", 6, opcodePushData}, + OP_DATA_6: {OP_DATA_6, "OP_DATA_6", 7, opcodePushData}, + OP_DATA_7: {OP_DATA_7, "OP_DATA_7", 8, opcodePushData}, + OP_DATA_8: {OP_DATA_8, "OP_DATA_8", 9, opcodePushData}, + OP_DATA_9: {OP_DATA_9, "OP_DATA_9", 10, opcodePushData}, + OP_DATA_10: {OP_DATA_10, "OP_DATA_10", 11, opcodePushData}, + OP_DATA_11: {OP_DATA_11, "OP_DATA_11", 12, opcodePushData}, + OP_DATA_12: {OP_DATA_12, "OP_DATA_12", 13, opcodePushData}, + OP_DATA_13: {OP_DATA_13, "OP_DATA_13", 14, opcodePushData}, + OP_DATA_14: {OP_DATA_14, "OP_DATA_14", 15, opcodePushData}, + OP_DATA_15: {OP_DATA_15, "OP_DATA_15", 16, opcodePushData}, + OP_DATA_16: {OP_DATA_16, "OP_DATA_16", 17, opcodePushData}, + OP_DATA_17: {OP_DATA_17, "OP_DATA_17", 18, opcodePushData}, + OP_DATA_18: {OP_DATA_18, "OP_DATA_18", 19, opcodePushData}, + OP_DATA_19: {OP_DATA_19, "OP_DATA_19", 20, opcodePushData}, + OP_DATA_20: {OP_DATA_20, "OP_DATA_20", 21, opcodePushData}, + OP_DATA_21: {OP_DATA_21, "OP_DATA_21", 22, opcodePushData}, + OP_DATA_22: {OP_DATA_22, "OP_DATA_22", 23, opcodePushData}, + OP_DATA_23: {OP_DATA_23, "OP_DATA_23", 24, opcodePushData}, + OP_DATA_24: {OP_DATA_24, "OP_DATA_24", 25, opcodePushData}, + OP_DATA_25: {OP_DATA_25, "OP_DATA_25", 26, opcodePushData}, + OP_DATA_26: {OP_DATA_26, "OP_DATA_26", 27, opcodePushData}, + OP_DATA_27: {OP_DATA_27, "OP_DATA_27", 28, opcodePushData}, + OP_DATA_28: {OP_DATA_28, "OP_DATA_28", 29, opcodePushData}, + OP_DATA_29: {OP_DATA_29, "OP_DATA_29", 30, opcodePushData}, + OP_DATA_30: {OP_DATA_30, "OP_DATA_30", 31, opcodePushData}, + OP_DATA_31: {OP_DATA_31, "OP_DATA_31", 32, opcodePushData}, + OP_DATA_32: {OP_DATA_32, "OP_DATA_32", 33, opcodePushData}, + OP_DATA_33: {OP_DATA_33, "OP_DATA_33", 34, opcodePushData}, + OP_DATA_34: {OP_DATA_34, "OP_DATA_34", 35, opcodePushData}, + OP_DATA_35: {OP_DATA_35, "OP_DATA_35", 36, opcodePushData}, + OP_DATA_36: {OP_DATA_36, "OP_DATA_36", 37, opcodePushData}, + OP_DATA_37: {OP_DATA_37, "OP_DATA_37", 38, opcodePushData}, + OP_DATA_38: {OP_DATA_38, "OP_DATA_38", 39, opcodePushData}, + OP_DATA_39: {OP_DATA_39, "OP_DATA_39", 40, opcodePushData}, + OP_DATA_40: {OP_DATA_40, "OP_DATA_40", 41, opcodePushData}, + OP_DATA_41: {OP_DATA_41, "OP_DATA_41", 42, opcodePushData}, + OP_DATA_42: {OP_DATA_42, "OP_DATA_42", 43, opcodePushData}, + OP_DATA_43: {OP_DATA_43, "OP_DATA_43", 44, opcodePushData}, + OP_DATA_44: {OP_DATA_44, "OP_DATA_44", 45, opcodePushData}, + OP_DATA_45: {OP_DATA_45, "OP_DATA_45", 46, opcodePushData}, + OP_DATA_46: {OP_DATA_46, "OP_DATA_46", 47, opcodePushData}, + OP_DATA_47: {OP_DATA_47, "OP_DATA_47", 48, opcodePushData}, + OP_DATA_48: {OP_DATA_48, "OP_DATA_48", 49, opcodePushData}, + OP_DATA_49: {OP_DATA_49, "OP_DATA_49", 50, opcodePushData}, + OP_DATA_50: {OP_DATA_50, "OP_DATA_50", 51, opcodePushData}, + OP_DATA_51: {OP_DATA_51, "OP_DATA_51", 52, opcodePushData}, + OP_DATA_52: {OP_DATA_52, "OP_DATA_52", 53, opcodePushData}, + OP_DATA_53: {OP_DATA_53, "OP_DATA_53", 54, opcodePushData}, + OP_DATA_54: {OP_DATA_54, "OP_DATA_54", 55, opcodePushData}, + OP_DATA_55: {OP_DATA_55, "OP_DATA_55", 56, opcodePushData}, + OP_DATA_56: {OP_DATA_56, "OP_DATA_56", 57, opcodePushData}, + OP_DATA_57: {OP_DATA_57, "OP_DATA_57", 58, opcodePushData}, + OP_DATA_58: {OP_DATA_58, "OP_DATA_58", 59, opcodePushData}, + OP_DATA_59: {OP_DATA_59, "OP_DATA_59", 60, opcodePushData}, + OP_DATA_60: {OP_DATA_60, "OP_DATA_60", 61, opcodePushData}, + OP_DATA_61: {OP_DATA_61, "OP_DATA_61", 62, opcodePushData}, + OP_DATA_62: {OP_DATA_62, "OP_DATA_62", 63, opcodePushData}, + OP_DATA_63: {OP_DATA_63, "OP_DATA_63", 64, opcodePushData}, + OP_DATA_64: {OP_DATA_64, "OP_DATA_64", 65, opcodePushData}, + OP_DATA_65: {OP_DATA_65, "OP_DATA_65", 66, opcodePushData}, + OP_DATA_66: {OP_DATA_66, "OP_DATA_66", 67, opcodePushData}, + OP_DATA_67: {OP_DATA_67, "OP_DATA_67", 68, opcodePushData}, + OP_DATA_68: {OP_DATA_68, "OP_DATA_68", 69, opcodePushData}, + OP_DATA_69: {OP_DATA_69, "OP_DATA_69", 70, opcodePushData}, + OP_DATA_70: {OP_DATA_70, "OP_DATA_70", 71, opcodePushData}, + OP_DATA_71: {OP_DATA_71, "OP_DATA_71", 72, opcodePushData}, + OP_DATA_72: {OP_DATA_72, "OP_DATA_72", 73, opcodePushData}, + OP_DATA_73: {OP_DATA_73, "OP_DATA_73", 74, opcodePushData}, + OP_DATA_74: {OP_DATA_74, "OP_DATA_74", 75, opcodePushData}, + OP_DATA_75: {OP_DATA_75, "OP_DATA_75", 76, opcodePushData}, + OP_PUSHDATA1: {OP_PUSHDATA1, "OP_PUSHDATA1", -1, opcodePushData}, + OP_PUSHDATA2: {OP_PUSHDATA2, "OP_PUSHDATA2", -2, opcodePushData}, + OP_PUSHDATA4: {OP_PUSHDATA4, "OP_PUSHDATA4", -4, opcodePushData}, + OP_1NEGATE: {OP_1NEGATE, "OP_1NEGATE", 1, opcode1Negate}, + OP_RESERVED: {OP_RESERVED, "OP_RESERVED", 1, opcodeReserved}, + OP_TRUE: {OP_TRUE, "OP_1", 1, opcodeN}, + OP_2: {OP_2, "OP_2", 1, opcodeN}, + OP_3: {OP_3, "OP_3", 1, opcodeN}, + OP_4: {OP_4, "OP_4", 1, opcodeN}, + OP_5: {OP_5, "OP_5", 1, opcodeN}, + OP_6: {OP_6, "OP_6", 1, opcodeN}, + OP_7: {OP_7, "OP_7", 1, opcodeN}, + OP_8: {OP_8, "OP_8", 1, opcodeN}, + OP_9: {OP_9, "OP_9", 1, opcodeN}, + OP_10: {OP_10, "OP_10", 1, opcodeN}, + OP_11: {OP_11, "OP_11", 1, opcodeN}, + OP_12: {OP_12, "OP_12", 1, opcodeN}, + OP_13: {OP_13, "OP_13", 1, opcodeN}, + OP_14: {OP_14, "OP_14", 1, opcodeN}, + OP_15: {OP_15, "OP_15", 1, opcodeN}, + OP_16: {OP_16, "OP_16", 1, opcodeN}, + // Control opcodes. + OP_NOP: {OP_NOP, "OP_NOP", 1, opcodeNop}, + OP_VER: {OP_VER, "OP_VER", 1, opcodeReserved}, + OP_IF: {OP_IF, "OP_IF", 1, opcodeIf}, + OP_NOTIF: {OP_NOTIF, "OP_NOTIF", 1, opcodeNotIf}, + OP_VERIF: {OP_VERIF, "OP_VERIF", 1, opcodeReserved}, + OP_VERNOTIF: {OP_VERNOTIF, "OP_VERNOTIF", 1, opcodeReserved}, + OP_ELSE: {OP_ELSE, "OP_ELSE", 1, opcodeElse}, + OP_ENDIF: {OP_ENDIF, "OP_ENDIF", 1, opcodeEndif}, + OP_VERIFY: {OP_VERIFY, "OP_VERIFY", 1, opcodeVerify}, + OP_RETURN: {OP_RETURN, "OP_RETURN", 1, opcodeReturn}, + OP_CHECKLOCKTIMEVERIFY: {OP_CHECKLOCKTIMEVERIFY, "OP_CHECKLOCKTIMEVERIFY", 1, opcodeCheckLockTimeVerify}, + OP_CHECKSEQUENCEVERIFY: {OP_CHECKSEQUENCEVERIFY, "OP_CHECKSEQUENCEVERIFY", 1, opcodeCheckSequenceVerify}, + // Stack opcodes. + OP_TOALTSTACK: {OP_TOALTSTACK, "OP_TOALTSTACK", 1, opcodeToAltStack}, + OP_FROMALTSTACK: {OP_FROMALTSTACK, "OP_FROMALTSTACK", 1, opcodeFromAltStack}, + OP_2DROP: {OP_2DROP, "OP_2DROP", 1, opcode2Drop}, + OP_2DUP: {OP_2DUP, "OP_2DUP", 1, opcode2Dup}, + OP_3DUP: {OP_3DUP, "OP_3DUP", 1, opcode3Dup}, + OP_2OVER: {OP_2OVER, "OP_2OVER", 1, opcode2Over}, + OP_2ROT: {OP_2ROT, "OP_2ROT", 1, opcode2Rot}, + OP_2SWAP: {OP_2SWAP, "OP_2SWAP", 1, opcode2Swap}, + OP_IFDUP: {OP_IFDUP, "OP_IFDUP", 1, opcodeIfDup}, + OP_DEPTH: {OP_DEPTH, "OP_DEPTH", 1, opcodeDepth}, + OP_DROP: {OP_DROP, "OP_DROP", 1, opcodeDrop}, + OP_DUP: {OP_DUP, "OP_DUP", 1, opcodeDup}, + OP_NIP: {OP_NIP, "OP_NIP", 1, opcodeNip}, + OP_OVER: {OP_OVER, "OP_OVER", 1, opcodeOver}, + OP_PICK: {OP_PICK, "OP_PICK", 1, opcodePick}, + OP_ROLL: {OP_ROLL, "OP_ROLL", 1, opcodeRoll}, + OP_ROT: {OP_ROT, "OP_ROT", 1, opcodeRot}, + OP_SWAP: {OP_SWAP, "OP_SWAP", 1, opcodeSwap}, + OP_TUCK: {OP_TUCK, "OP_TUCK", 1, opcodeTuck}, + // Splice opcodes. + OP_CAT: {OP_CAT, "OP_CAT", 1, opcodeDisabled}, + OP_SUBSTR: {OP_SUBSTR, "OP_SUBSTR", 1, opcodeDisabled}, + OP_LEFT: {OP_LEFT, "OP_LEFT", 1, opcodeDisabled}, + OP_RIGHT: {OP_RIGHT, "OP_RIGHT", 1, opcodeDisabled}, + OP_SIZE: {OP_SIZE, "OP_SIZE", 1, opcodeSize}, + // Bitwise logic opcodes. + OP_INVERT: {OP_INVERT, "OP_INVERT", 1, opcodeDisabled}, + OP_AND: {OP_AND, "OP_AND", 1, opcodeDisabled}, + OP_OR: {OP_OR, "OP_OR", 1, opcodeDisabled}, + OP_XOR: {OP_XOR, "OP_XOR", 1, opcodeDisabled}, + OP_EQUAL: {OP_EQUAL, "OP_EQUAL", 1, opcodeEqual}, + OP_EQUALVERIFY: {OP_EQUALVERIFY, "OP_EQUALVERIFY", 1, opcodeEqualVerify}, + OP_RESERVED1: {OP_RESERVED1, "OP_RESERVED1", 1, opcodeReserved}, + OP_RESERVED2: {OP_RESERVED2, "OP_RESERVED2", 1, opcodeReserved}, + // Numeric related opcodes. + OP_1ADD: {OP_1ADD, "OP_1ADD", 1, opcode1Add}, + OP_1SUB: {OP_1SUB, "OP_1SUB", 1, opcode1Sub}, + OP_2MUL: {OP_2MUL, "OP_2MUL", 1, opcodeDisabled}, + OP_2DIV: {OP_2DIV, "OP_2DIV", 1, opcodeDisabled}, + OP_NEGATE: {OP_NEGATE, "OP_NEGATE", 1, opcodeNegate}, + OP_ABS: {OP_ABS, "OP_ABS", 1, opcodeAbs}, + OP_NOT: {OP_NOT, "OP_NOT", 1, opcodeNot}, + OP_0NOTEQUAL: {OP_0NOTEQUAL, "OP_0NOTEQUAL", 1, opcode0NotEqual}, + OP_ADD: {OP_ADD, "OP_ADD", 1, opcodeAdd}, + OP_SUB: {OP_SUB, "OP_SUB", 1, opcodeSub}, + OP_MUL: {OP_MUL, "OP_MUL", 1, opcodeDisabled}, + OP_DIV: {OP_DIV, "OP_DIV", 1, opcodeDisabled}, + OP_MOD: {OP_MOD, "OP_MOD", 1, opcodeDisabled}, + OP_LSHIFT: {OP_LSHIFT, "OP_LSHIFT", 1, opcodeDisabled}, + OP_RSHIFT: {OP_RSHIFT, "OP_RSHIFT", 1, opcodeDisabled}, + OP_BOOLAND: {OP_BOOLAND, "OP_BOOLAND", 1, opcodeBoolAnd}, + OP_BOOLOR: {OP_BOOLOR, "OP_BOOLOR", 1, opcodeBoolOr}, + OP_NUMEQUAL: {OP_NUMEQUAL, "OP_NUMEQUAL", 1, opcodeNumEqual}, + OP_NUMEQUALVERIFY: {OP_NUMEQUALVERIFY, "OP_NUMEQUALVERIFY", 1, opcodeNumEqualVerify}, + OP_NUMNOTEQUAL: {OP_NUMNOTEQUAL, "OP_NUMNOTEQUAL", 1, opcodeNumNotEqual}, + OP_LESSTHAN: {OP_LESSTHAN, "OP_LESSTHAN", 1, opcodeLessThan}, + OP_GREATERTHAN: {OP_GREATERTHAN, "OP_GREATERTHAN", 1, opcodeGreaterThan}, + OP_LESSTHANOREQUAL: {OP_LESSTHANOREQUAL, "OP_LESSTHANOREQUAL", 1, opcodeLessThanOrEqual}, + OP_GREATERTHANOREQUAL: {OP_GREATERTHANOREQUAL, "OP_GREATERTHANOREQUAL", 1, opcodeGreaterThanOrEqual}, + OP_MIN: {OP_MIN, "OP_MIN", 1, opcodeMin}, + OP_MAX: {OP_MAX, "OP_MAX", 1, opcodeMax}, + OP_WITHIN: {OP_WITHIN, "OP_WITHIN", 1, opcodeWithin}, + // Crypto opcodes. + OP_RIPEMD160: {OP_RIPEMD160, "OP_RIPEMD160", 1, opcodeRipeMD160}, + OP_SHA1: {OP_SHA1, "OP_SHA1", 1, opcodeSHA1}, + OP_SHA256: {OP_SHA256, "OP_SHA256", 1, opcodeSHA256}, + OP_HASH160: {OP_HASH160, "OP_HASH160", 1, opcodeHash160}, + OP_HASH256: {OP_HASH256, "OP_HASH256", 1, opcodeHash256}, + OP_CODESEPARATOR: {OP_CODESEPARATOR, "OP_CODESEPARATOR", 1, opcodeCodeSeparator}, + OP_CHECKSIG: {OP_CHECKSIG, "OP_CHECKSIG", 1, opcodeCheckSig}, + OP_CHECKSIGVERIFY: {OP_CHECKSIGVERIFY, "OP_CHECKSIGVERIFY", 1, opcodeCheckSigVerify}, + OP_CHECKMULTISIG: {OP_CHECKMULTISIG, "OP_CHECKMULTISIG", 1, opcodeCheckMultiSig}, + OP_CHECKMULTISIGVERIFY: {OP_CHECKMULTISIGVERIFY, "OP_CHECKMULTISIGVERIFY", 1, opcodeCheckMultiSigVerify}, + // Reserved opcodes. + OP_NOP1: {OP_NOP1, "OP_NOP1", 1, opcodeNop}, + OP_NOP4: {OP_NOP4, "OP_NOP4", 1, opcodeNop}, + OP_NOP5: {OP_NOP5, "OP_NOP5", 1, opcodeNop}, + OP_NOP6: {OP_NOP6, "OP_NOP6", 1, opcodeNop}, + OP_NOP7: {OP_NOP7, "OP_NOP7", 1, opcodeNop}, + OP_NOP8: {OP_NOP8, "OP_NOP8", 1, opcodeNop}, + OP_NOP9: {OP_NOP9, "OP_NOP9", 1, opcodeNop}, + OP_NOP10: {OP_NOP10, "OP_NOP10", 1, opcodeNop}, + // Undefined opcodes. + OP_UNKNOWN186: {OP_UNKNOWN186, "OP_UNKNOWN186", 1, opcodeInvalid}, + OP_UNKNOWN187: {OP_UNKNOWN187, "OP_UNKNOWN187", 1, opcodeInvalid}, + OP_UNKNOWN188: {OP_UNKNOWN188, "OP_UNKNOWN188", 1, opcodeInvalid}, + OP_UNKNOWN189: {OP_UNKNOWN189, "OP_UNKNOWN189", 1, opcodeInvalid}, + OP_UNKNOWN190: {OP_UNKNOWN190, "OP_UNKNOWN190", 1, opcodeInvalid}, + OP_UNKNOWN191: {OP_UNKNOWN191, "OP_UNKNOWN191", 1, opcodeInvalid}, + OP_UNKNOWN192: {OP_UNKNOWN192, "OP_UNKNOWN192", 1, opcodeInvalid}, + OP_UNKNOWN193: {OP_UNKNOWN193, "OP_UNKNOWN193", 1, opcodeInvalid}, + OP_UNKNOWN194: {OP_UNKNOWN194, "OP_UNKNOWN194", 1, opcodeInvalid}, + OP_UNKNOWN195: {OP_UNKNOWN195, "OP_UNKNOWN195", 1, opcodeInvalid}, + OP_UNKNOWN196: {OP_UNKNOWN196, "OP_UNKNOWN196", 1, opcodeInvalid}, + OP_UNKNOWN197: {OP_UNKNOWN197, "OP_UNKNOWN197", 1, opcodeInvalid}, + OP_UNKNOWN198: {OP_UNKNOWN198, "OP_UNKNOWN198", 1, opcodeInvalid}, + OP_UNKNOWN199: {OP_UNKNOWN199, "OP_UNKNOWN199", 1, opcodeInvalid}, + OP_UNKNOWN200: {OP_UNKNOWN200, "OP_UNKNOWN200", 1, opcodeInvalid}, + OP_UNKNOWN201: {OP_UNKNOWN201, "OP_UNKNOWN201", 1, opcodeInvalid}, + OP_UNKNOWN202: {OP_UNKNOWN202, "OP_UNKNOWN202", 1, opcodeInvalid}, + OP_UNKNOWN203: {OP_UNKNOWN203, "OP_UNKNOWN203", 1, opcodeInvalid}, + OP_UNKNOWN204: {OP_UNKNOWN204, "OP_UNKNOWN204", 1, opcodeInvalid}, + OP_UNKNOWN205: {OP_UNKNOWN205, "OP_UNKNOWN205", 1, opcodeInvalid}, + OP_UNKNOWN206: {OP_UNKNOWN206, "OP_UNKNOWN206", 1, opcodeInvalid}, + OP_UNKNOWN207: {OP_UNKNOWN207, "OP_UNKNOWN207", 1, opcodeInvalid}, + OP_UNKNOWN208: {OP_UNKNOWN208, "OP_UNKNOWN208", 1, opcodeInvalid}, + OP_UNKNOWN209: {OP_UNKNOWN209, "OP_UNKNOWN209", 1, opcodeInvalid}, + OP_UNKNOWN210: {OP_UNKNOWN210, "OP_UNKNOWN210", 1, opcodeInvalid}, + OP_UNKNOWN211: {OP_UNKNOWN211, "OP_UNKNOWN211", 1, opcodeInvalid}, + OP_UNKNOWN212: {OP_UNKNOWN212, "OP_UNKNOWN212", 1, opcodeInvalid}, + OP_UNKNOWN213: {OP_UNKNOWN213, "OP_UNKNOWN213", 1, opcodeInvalid}, + OP_UNKNOWN214: {OP_UNKNOWN214, "OP_UNKNOWN214", 1, opcodeInvalid}, + OP_UNKNOWN215: {OP_UNKNOWN215, "OP_UNKNOWN215", 1, opcodeInvalid}, + OP_UNKNOWN216: {OP_UNKNOWN216, "OP_UNKNOWN216", 1, opcodeInvalid}, + OP_UNKNOWN217: {OP_UNKNOWN217, "OP_UNKNOWN217", 1, opcodeInvalid}, + OP_UNKNOWN218: {OP_UNKNOWN218, "OP_UNKNOWN218", 1, opcodeInvalid}, + OP_UNKNOWN219: {OP_UNKNOWN219, "OP_UNKNOWN219", 1, opcodeInvalid}, + OP_UNKNOWN220: {OP_UNKNOWN220, "OP_UNKNOWN220", 1, opcodeInvalid}, + OP_UNKNOWN221: {OP_UNKNOWN221, "OP_UNKNOWN221", 1, opcodeInvalid}, + OP_UNKNOWN222: {OP_UNKNOWN222, "OP_UNKNOWN222", 1, opcodeInvalid}, + OP_UNKNOWN223: {OP_UNKNOWN223, "OP_UNKNOWN223", 1, opcodeInvalid}, + OP_UNKNOWN224: {OP_UNKNOWN224, "OP_UNKNOWN224", 1, opcodeInvalid}, + OP_UNKNOWN225: {OP_UNKNOWN225, "OP_UNKNOWN225", 1, opcodeInvalid}, + OP_UNKNOWN226: {OP_UNKNOWN226, "OP_UNKNOWN226", 1, opcodeInvalid}, + OP_UNKNOWN227: {OP_UNKNOWN227, "OP_UNKNOWN227", 1, opcodeInvalid}, + OP_UNKNOWN228: {OP_UNKNOWN228, "OP_UNKNOWN228", 1, opcodeInvalid}, + OP_UNKNOWN229: {OP_UNKNOWN229, "OP_UNKNOWN229", 1, opcodeInvalid}, + OP_UNKNOWN230: {OP_UNKNOWN230, "OP_UNKNOWN230", 1, opcodeInvalid}, + OP_UNKNOWN231: {OP_UNKNOWN231, "OP_UNKNOWN231", 1, opcodeInvalid}, + OP_UNKNOWN232: {OP_UNKNOWN232, "OP_UNKNOWN232", 1, opcodeInvalid}, + OP_UNKNOWN233: {OP_UNKNOWN233, "OP_UNKNOWN233", 1, opcodeInvalid}, + OP_UNKNOWN234: {OP_UNKNOWN234, "OP_UNKNOWN234", 1, opcodeInvalid}, + OP_UNKNOWN235: {OP_UNKNOWN235, "OP_UNKNOWN235", 1, opcodeInvalid}, + OP_UNKNOWN236: {OP_UNKNOWN236, "OP_UNKNOWN236", 1, opcodeInvalid}, + OP_UNKNOWN237: {OP_UNKNOWN237, "OP_UNKNOWN237", 1, opcodeInvalid}, + OP_UNKNOWN238: {OP_UNKNOWN238, "OP_UNKNOWN238", 1, opcodeInvalid}, + OP_UNKNOWN239: {OP_UNKNOWN239, "OP_UNKNOWN239", 1, opcodeInvalid}, + OP_UNKNOWN240: {OP_UNKNOWN240, "OP_UNKNOWN240", 1, opcodeInvalid}, + OP_UNKNOWN241: {OP_UNKNOWN241, "OP_UNKNOWN241", 1, opcodeInvalid}, + OP_UNKNOWN242: {OP_UNKNOWN242, "OP_UNKNOWN242", 1, opcodeInvalid}, + OP_UNKNOWN243: {OP_UNKNOWN243, "OP_UNKNOWN243", 1, opcodeInvalid}, + OP_UNKNOWN244: {OP_UNKNOWN244, "OP_UNKNOWN244", 1, opcodeInvalid}, + OP_UNKNOWN245: {OP_UNKNOWN245, "OP_UNKNOWN245", 1, opcodeInvalid}, + OP_UNKNOWN246: {OP_UNKNOWN246, "OP_UNKNOWN246", 1, opcodeInvalid}, + OP_UNKNOWN247: {OP_UNKNOWN247, "OP_UNKNOWN247", 1, opcodeInvalid}, + OP_UNKNOWN248: {OP_UNKNOWN248, "OP_UNKNOWN248", 1, opcodeInvalid}, + OP_UNKNOWN249: {OP_UNKNOWN249, "OP_UNKNOWN249", 1, opcodeInvalid}, + // Bitcoin Core internal use opcode. Defined here for completeness. + OP_SMALLINTEGER: {OP_SMALLINTEGER, "OP_SMALLINTEGER", 1, opcodeInvalid}, + OP_PUBKEYS: {OP_PUBKEYS, "OP_PUBKEYS", 1, opcodeInvalid}, + OP_UNKNOWN252: {OP_UNKNOWN252, "OP_UNKNOWN252", 1, opcodeInvalid}, + OP_PUBKEYHASH: {OP_PUBKEYHASH, "OP_PUBKEYHASH", 1, opcodeInvalid}, + OP_PUBKEY: {OP_PUBKEY, "OP_PUBKEY", 1, opcodeInvalid}, + OP_INVALIDOPCODE: {OP_INVALIDOPCODE, "OP_INVALIDOPCODE", 1, opcodeInvalid}, +} + +// opcodeOnelineRepls defines opcode names which are replaced when doing a one-line disassembly. This is done to match +// the output of the reference implementation while not changing the opcode names in the nicer full disassembly. +var opcodeOnelineRepls = map[string]string{ + "OP_1NEGATE": "-1", + "OP_0": "0", + "OP_1": "1", + "OP_2": "2", + "OP_3": "3", + "OP_4": "4", + "OP_5": "5", + "OP_6": "6", + "OP_7": "7", + "OP_8": "8", + "OP_9": "9", + "OP_10": "10", + "OP_11": "11", + "OP_12": "12", + "OP_13": "13", + "OP_14": "14", + "OP_15": "15", + "OP_16": "16", +} + +// parsedOpcode represents an opcode that has been parsed and includes any potential data associated with it. +type parsedOpcode struct { + opcode *opcode + data []byte +} + +// isDisabled returns whether or not the opcode is disabled and thus is always bad to see in the instruction stream ( +// even if turned off by a conditional). +func (pop *parsedOpcode) isDisabled() bool { + switch pop.opcode.value { + case OP_CAT: + return true + case OP_SUBSTR: + return true + case OP_LEFT: + return true + case OP_RIGHT: + return true + case OP_INVERT: + return true + case OP_AND: + return true + case OP_OR: + return true + case OP_XOR: + return true + case OP_2MUL: + return true + case OP_2DIV: + return true + case OP_MUL: + return true + case OP_DIV: + return true + case OP_MOD: + return true + case OP_LSHIFT: + return true + case OP_RSHIFT: + return true + default: + return false + } +} + +// alwaysIllegal returns whether or not the opcode is always illegal when passed over by the program counter even if in +// a non-executed branch ( it isn't a coincidence that they are conditionals). +func (pop *parsedOpcode) alwaysIllegal() bool { + switch pop.opcode.value { + case OP_VERIF: + return true + case OP_VERNOTIF: + return true + default: + return false + } +} + +// isConditional returns whether or not the opcode is a conditional opcode which changes the conditional execution stack +// when executed. +func (pop *parsedOpcode) isConditional() bool { + switch pop.opcode.value { + case OP_IF: + return true + case OP_NOTIF: + return true + case OP_ELSE: + return true + case OP_ENDIF: + return true + default: + return false + } +} + +// checkMinimalDataPush returns whether or not the current data push uses the smallest possible opcode to represent it. +// For example, the value 15 could be pushed with OP_DATA_1 15 ( among other variations); however, OP_15 is a single +// opcode that represents the same value and is only a single byte versus two bytes. +func (pop *parsedOpcode) checkMinimalDataPush() (e error) { + data := pop.data + dataLen := len(data) + opcode := pop.opcode.value + if dataLen == 0 && opcode != OP_0 { + str := fmt.Sprintf( + "zero length data push is encoded with "+ + "opcode %s instead of OP_0", pop.opcode.name, + ) + return scriptError(ErrMinimalData, str) + } else if dataLen == 1 && data[0] >= 1 && data[0] <= 16 { + if opcode != OP_1+data[0]-1 { + // Should have used OP_1 .. OP_16 + str := fmt.Sprintf( + "data push of the value %d encoded "+ + "with opcode %s instead of OP_%d", data[0], + pop.opcode.name, data[0], + ) + return scriptError(ErrMinimalData, str) + } + } else if dataLen == 1 && data[0] == 0x81 { + if opcode != OP_1NEGATE { + str := fmt.Sprintf( + "data push of the value -1 encoded "+ + "with opcode %s instead of OP_1NEGATE", + pop.opcode.name, + ) + return scriptError(ErrMinimalData, str) + } + } else if dataLen <= 75 { + if int(opcode) != dataLen { + // Should have used a direct push + str := fmt.Sprintf( + "data push of %d bytes encoded "+ + "with opcode %s instead of OP_DATA_%d", dataLen, + pop.opcode.name, dataLen, + ) + return scriptError(ErrMinimalData, str) + } + } else if dataLen <= 255 { + if opcode != OP_PUSHDATA1 { + str := fmt.Sprintf( + "data push of %d bytes encoded "+ + "with opcode %s instead of OP_PUSHDATA1", + dataLen, pop.opcode.name, + ) + return scriptError(ErrMinimalData, str) + } + } else if dataLen <= 65535 { + if opcode != OP_PUSHDATA2 { + str := fmt.Sprintf( + "data push of %d bytes encoded "+ + "with opcode %s instead of OP_PUSHDATA2", + dataLen, pop.opcode.name, + ) + return scriptError(ErrMinimalData, str) + } + } + return nil +} + +// print returns a human-readable string representation of the opcode for use in script disassembly. +func (pop *parsedOpcode) print(oneline bool) string { + // The reference implementation one-line disassembly replaces opcodes which represent values (e.g. OP_0 through + // OP_16 and OP_1NEGATE) with the raw value. However, when not doing a one-line dissassembly, we prefer to show the + // actual opcode names. Thus, only replace the opcodes in question when the oneline flag is set. + opcodeName := pop.opcode.name + if oneline { + if replName, ok := opcodeOnelineRepls[opcodeName]; ok { + opcodeName = replName + } + // Nothing more to do for non-data push opcodes. + if pop.opcode.length == 1 { + return opcodeName + } + return fmt.Sprintf("%x", pop.data) + } + // Nothing more to do for non-data push opcodes. + if pop.opcode.length == 1 { + return opcodeName + } + // Add length for the OP_PUSHDATA# opcodes. + retString := opcodeName + switch pop.opcode.length { + case -1: + retString += fmt.Sprintf(" 0x%02x", len(pop.data)) + case -2: + retString += fmt.Sprintf(" 0x%04x", len(pop.data)) + case -4: + retString += fmt.Sprintf(" 0x%08x", len(pop.data)) + } + return fmt.Sprintf("%s 0x%02x", retString, pop.data) +} + +// bytes returns any data associated with the opcode encoded as it would be in a script. This is used for unparsing +// scripts from parsed opcodes. +func (pop *parsedOpcode) bytes() ([]byte, error) { + var retbytes []byte + if pop.opcode.length > 0 { + retbytes = make([]byte, 1, pop.opcode.length) + } else { + retbytes = make( + []byte, 1, 1+len(pop.data)- + pop.opcode.length, + ) + } + retbytes[0] = pop.opcode.value + if pop.opcode.length == 1 { + if len(pop.data) != 0 { + str := fmt.Sprintf( + "internal consistency error - "+ + "parsed opcode %s has data length %d when %d "+ + "was expected", pop.opcode.name, len(pop.data), + 0, + ) + return nil, scriptError(ErrInternal, str) + } + return retbytes, nil + } + nbytes := pop.opcode.length + if pop.opcode.length < 0 { + l := len(pop.data) + // tempting just to hardcode to avoid the complexity here. + switch pop.opcode.length { + case -1: + retbytes = append(retbytes, byte(l)) + nbytes = int(retbytes[1]) + len(retbytes) + case -2: + retbytes = append( + retbytes, byte(l&0xff), + byte(l>>8&0xff), + ) + nbytes = int(binary.LittleEndian.Uint16(retbytes[1:])) + + len(retbytes) + case -4: + retbytes = append( + retbytes, byte(l&0xff), + byte((l>>8)&0xff), byte((l>>16)&0xff), + byte((l>>24)&0xff), + ) + nbytes = int(binary.LittleEndian.Uint32(retbytes[1:])) + + len(retbytes) + } + } + retbytes = append(retbytes, pop.data...) + if len(retbytes) != nbytes { + str := fmt.Sprintf( + "internal consistency error - "+ + "parsed opcode %s has data length %d when %d was "+ + "expected", pop.opcode.name, len(retbytes), nbytes, + ) + return nil, scriptError(ErrInternal, str) + } + return retbytes, nil +} + +// Opcode implementation functions start here. + +// opcodeDisabled is a common handler for disabled opcodes. It returns an appropriate error indicating the opcode is +// disabled. While it would ordinarily make more sense to detect if the script contains any disabled opcodes before +// executing in an initial parse step, the consensus rules dictate the script doesn't fail until the program counter +// passes over a disabled opcode ( even when they appear in a branch that is not executed). +func opcodeDisabled(op *parsedOpcode, vm *Engine) (e error) { + str := fmt.Sprintf( + "attempt to execute disabled opcode %s", + op.opcode.name, + ) + return scriptError(ErrDisabledOpcode, str) +} + +// opcodeReserved is a common handler for all reserved opcodes. It returns an appropriate error indicating the opcode is +// reserved. +func opcodeReserved(op *parsedOpcode, vm *Engine) (e error) { + str := fmt.Sprintf( + "attempt to execute reserved opcode %s", + op.opcode.name, + ) + return scriptError(ErrReservedOpcode, str) +} + +// opcodeInvalid is a common handler for all invalid opcodes. It returns an appropriate error indicating the opcode is +// invalid. +func opcodeInvalid(op *parsedOpcode, vm *Engine) (e error) { + str := fmt.Sprintf( + "attempt to execute invalid opcode %s", + op.opcode.name, + ) + return scriptError(ErrReservedOpcode, str) +} + +// opcodeFalse pushes an empty array to the data stack to represent false. Note that 0, when encoded as a number +// according to the numeric encoding consensus rules, is an empty array. +func opcodeFalse(op *parsedOpcode, vm *Engine) (e error) { + vm.dstack.PushByteArray(nil) + return nil +} + +// opcodePushData is a common handler for the vast majority of opcodes that push raw data (bytes) to the data stack. +func opcodePushData(op *parsedOpcode, vm *Engine) (e error) { + vm.dstack.PushByteArray(op.data) + return nil +} + +// opcode1Negate pushes -1, encoded as a number, to the data stack. +func opcode1Negate(op *parsedOpcode, vm *Engine) (e error) { + vm.dstack.PushInt(scriptNum(-1)) + return nil +} + +// opcodeN is a common handler for the small integer data push opcodes. It pushes the numeric value the opcode +// represents ( which will be from 1 to 16) onto the data stack. +func opcodeN(op *parsedOpcode, vm *Engine) (e error) { + // The opcodes are all defined consecutively, so the numeric value is the difference. + vm.dstack.PushInt(scriptNum(op.opcode.value - (OP_1 - 1))) + return nil +} + +// opcodeNop is a common handler for the NOP family of opcodes. As the name implies it generally does nothing, however, +// it will return an error when the flag to discourage use of NOPs is set for select opcodes. +func opcodeNop(op *parsedOpcode, vm *Engine) (e error) { + switch op.opcode.value { + case OP_NOP1, OP_NOP4, OP_NOP5, + OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10: + if vm.hasFlag(ScriptDiscourageUpgradableNops) { + str := fmt.Sprintf( + "OP_NOP%d reserved for soft-fork "+ + "upgrades", op.opcode.value-(OP_NOP1-1), + ) + return scriptError(ErrDiscourageUpgradableNOPs, str) + } + } + return nil +} + +// popIfBool enforces the "minimal if" policy during script execution if the +// particular flag is set. If so, in order to eliminate an additional source of +// nuisance malleability, post-segwit for version 0 witness programs, we now +// require the following: for OP_IF and OP_NOT_IF, the top stack item MUST +// either be an empty byte slice, or [0x01]. Otherwise, the item at the top of +// the stack will be popped and interpreted as a boolean. +func popIfBool(vm *Engine) (bool, error) { + // When not in witness execution mode, not executing a v0 witness program, or + // the minimal if flag isn't set pop the top stack item as a normal bool. + if !vm.isWitnessVersionActive(0) || !vm.hasFlag(ScriptVerifyMinimalIf) { + return vm.dstack.PopBool() + } + // At this point, a v0 witness program is being executed and the minimal if flag + // is set, so enforce additional constraints on the top stack item. + so, e := vm.dstack.PopByteArray() + if e != nil { + return false, e + } + // The top element MUST have a length of at least one. + if len(so) > 1 { + str := fmt.Sprintf( + "minimal if is active, top element MUST "+ + "have a length of at least, instead length is %v", + len(so), + ) + return false, scriptError(ErrMinimalIf, str) + } + // Additionally, if the length is one, then the value MUST be 0x01. + if len(so) == 1 && so[0] != 0x01 { + str := fmt.Sprintf( + "minimal if is active, top stack item MUST "+ + "be an empty byte array or 0x01, is instead: %v", + so[0], + ) + return false, scriptError(ErrMinimalIf, str) + } + return asBool(so), nil +} + +// opcodeIf treats the top item on the data stack as a boolean and removes it. An appropriate entry is added to the +// conditional stack depending on whether the boolean is true and whether this if is on an executing branch in order to +// allow proper execution of further opcodes depending on the conditional logic. When the boolean is true, the first +// branch will be executed (unless this opcode is nested in a non-executed branch). if [statements] [else +// [statements]] endif Note that, unlike for all non-conditional opcodes, this is executed even when it is on a +// non-executing branch so proper nesting is maintained. +// +// Data stack transformation: [... bool] -> [...] +// +// Conditional stack transformation: [...] -> [... OpCondValue] +func opcodeIf(op *parsedOpcode, vm *Engine) (e error) { + condVal := OpCondFalse + if vm.isBranchExecuting() { + ok, e := popIfBool(vm) + if e != nil { + return e + } + if ok { + condVal = OpCondTrue + } + } else { + condVal = OpCondSkip + } + vm.condStack = append(vm.condStack, condVal) + return nil +} + +// opcodeNotIf treats the top item on the data stack as a boolean and removes it. An appropriate entry is added to the +// conditional stack depending on whether the boolean is true and whether this if is on an executing branch in order to +// allow proper execution of further opcodes depending on the conditional logic. When the boolean is false, the first +// branch will be executed (unless this opcode is nested in a non-executed branch). notif [statements] +// [else [statements]] endif Note that, unlike for all non-conditional opcodes, this is executed even when it is on a +// non-executing branch so proper nesting is maintained. +// +// Data stack transformation: [... bool] -> [...] +// +// Conditional stack transformation: [...] -> [... OpCondValue] +func opcodeNotIf(op *parsedOpcode, vm *Engine) (e error) { + condVal := OpCondFalse + if vm.isBranchExecuting() { + ok, e := popIfBool(vm) + if e != nil { + return e + } + if !ok { + condVal = OpCondTrue + } + } else { + condVal = OpCondSkip + } + vm.condStack = append(vm.condStack, condVal) + return nil +} + +// opcodeElse inverts conditional execution for other half of if/else/endif. +// +// An error is returned if there has not already been a matching OP_IF. +// +// Conditional stack transformation: [... OpCondValue] -> [... !OpCondValue] +func opcodeElse(op *parsedOpcode, vm *Engine) (e error) { + if len(vm.condStack) == 0 { + str := fmt.Sprintf( + "encountered opcode %s with no matching "+ + "opcode to begin conditional execution", op.opcode.name, + ) + return scriptError(ErrUnbalancedConditional, str) + } + conditionalIdx := len(vm.condStack) - 1 + switch vm.condStack[conditionalIdx] { + case OpCondTrue: + vm.condStack[conditionalIdx] = OpCondFalse + case OpCondFalse: + vm.condStack[conditionalIdx] = OpCondTrue + case OpCondSkip: + // value doesn't change in skip since it indicates this opcode is nested in a non-executed branch. + } + return nil +} + +// opcodeEndif terminates a conditional block, removing the value from the conditional execution stack. +// +// An error is returned if there has not already been a matching OP_IF. +// +// Conditional stack transformation: [... OpCondValue] -> [...] +func opcodeEndif(op *parsedOpcode, vm *Engine) (e error) { + if len(vm.condStack) == 0 { + str := fmt.Sprintf( + "encountered opcode %s with no matching "+ + "opcode to begin conditional execution", op.opcode.name, + ) + return scriptError(ErrUnbalancedConditional, str) + } + vm.condStack = vm.condStack[:len(vm.condStack)-1] + return nil +} + +// abstractVerify examines the top item on the data stack as a boolean value and verifies it evaluates to true. +// +// An error is returned either when there is no item on the stack or when that item evaluates to false. +// +// In the latter case where the verification fails specifically due to the top item evaluating to false, the returned +// error will use the passed error code. +func abstractVerify(op *parsedOpcode, vm *Engine, c ErrorCode) (e error) { + verified, e := vm.dstack.PopBool() + if e != nil { + return e + } + if !verified { + str := fmt.Sprintf("%s failed", op.opcode.name) + return scriptError(c, str) + } + return nil +} + +// opcodeVerify examines the top item on the data stack as a boolean value and verifies it evaluates to true. An error +// is returned if it does not. +func opcodeVerify(op *parsedOpcode, vm *Engine) (e error) { + return abstractVerify(op, vm, ErrVerify) +} + +// opcodeReturn returns an appropriate error since it is always an error to return early from a script. +func opcodeReturn(op *parsedOpcode, vm *Engine) (e error) { + return scriptError(ErrEarlyReturn, "script returned early") +} + +// verifyLockTime is a helper function used to validate locktimes. +func verifyLockTime(txLockTime, threshold, lockTime int64) (e error) { + // The lockTimes in both the script and transaction must be of the same type. + if !((txLockTime < threshold && lockTime < threshold) || + (txLockTime >= threshold && lockTime >= threshold)) { + str := fmt.Sprintf( + "mismatched locktime types -- tx locktime "+ + "%d, stack locktime %d", txLockTime, lockTime, + ) + return scriptError(ErrUnsatisfiedLockTime, str) + } + if lockTime > txLockTime { + str := fmt.Sprintf( + "locktime requirement not satisfied -- "+ + "locktime is greater than the transaction locktime: "+ + "%d > %d", lockTime, txLockTime, + ) + return scriptError(ErrUnsatisfiedLockTime, str) + } + return nil +} + +// opcodeCheckLockTimeVerify compares the top item on the data stack to the LockTime field of the transaction containing +// the script signature validating if the transaction outputs are spendable yet. +// +// If flag ScriptVerifyCheckLockTimeVerify is not set, the code continues as if OP_NOP2 were executed. +func opcodeCheckLockTimeVerify(op *parsedOpcode, vm *Engine) (e error) { + // If the ScriptVerifyCheckLockTimeVerify script flag is not set, treat opcode as OP_NOP2 instead. + if !vm.hasFlag(ScriptVerifyCheckLockTimeVerify) { + if vm.hasFlag(ScriptDiscourageUpgradableNops) { + return scriptError( + ErrDiscourageUpgradableNOPs, + "OP_NOP2 reserved for soft-fork upgrades", + ) + } + return nil + } + // The current transaction locktime is a uint32 resulting in a maximum locktime of 2^32-1 (the year 2106). However, + // scriptNums are signed and therefore a standard 4-byte scriptNum would only support up to a maximum of 2^31-1 (the + // year 2038). Thus, a 5-byte scriptNum is used here since it will support up to 2^39-1 which allows dates beyond + // the current locktime limit. PeekByteArray is used here instead of PeekInt because we do not want to be limited to + // a 4-byte integer for reasons specified above. + so, e := vm.dstack.PeekByteArray(0) + if e != nil { + return e + } + lockTime, e := makeScriptNum(so, vm.dstack.verifyMinimalData, 5) + if e != nil { + return e + } + // In the rare event that the argument needs to be < 0 due to some arithmetic being done first, you can always use 0 + // OP_MAX OP_CHECKLOCKTIMEVERIFY. + if lockTime < 0 { + str := fmt.Sprintf("negative lock time: %d", lockTime) + return scriptError(ErrNegativeLockTime, str) + } + // The lock time field of a transaction is either a block height at which the transaction is finalized or a + // timestamp depending on if the value is before the txscript.LockTimeThreshold. When it is under the threshold it + // is a block height. + e = verifyLockTime( + int64(vm.tx.LockTime), LockTimeThreshold, + int64(lockTime), + ) + if e != nil { + return e + } + // The lock time feature can also be disabled, thereby bypassing OP_CHECKLOCKTIMEVERIFY, if every transaction input + // has been finalized by setting its sequence to the maximum value (wire.MaxTxInSequenceNum). This condition would + // result in the transaction being allowed into the blockchain making the opcode ineffective. This condition is + // prevented by enforcing that the input being used by the opcode is unlocked (its sequence number is less than the + // max value). This is sufficient to prove correctness without having to check every input. NOTE: This implies that + // even if the transaction is not finalized due to another input being unlocked, the opcode execution will still + // fail when the input being used by the opcode is locked. + if vm.tx.TxIn[vm.txIdx].Sequence == wire.MaxTxInSequenceNum { + return scriptError( + ErrUnsatisfiedLockTime, + "transaction input is finalized", + ) + } + return nil +} + +// opcodeCheckSequenceVerify compares the top item on the data stack to the LockTime field of the transaction containing +// the script signature validating if the transaction outputs are spendable yet. +// +// If flag ScriptVerifyCheckSequenceVerify is not set, the code continues as if OP_NOP3 were executed. +func opcodeCheckSequenceVerify(op *parsedOpcode, vm *Engine) (e error) { + // If the ScriptVerifyCheckSequenceVerify script flag is not set, treat opcode as OP_NOP3 instead. + if !vm.hasFlag(ScriptVerifyCheckSequenceVerify) { + if vm.hasFlag(ScriptDiscourageUpgradableNops) { + return scriptError( + ErrDiscourageUpgradableNOPs, + "OP_NOP3 reserved for soft-fork upgrades", + ) + } + return nil + } + // The current transaction sequence is a uint32 resulting in a maximum sequence of 2^32-1. However, scriptNums are + // signed and therefore a standard 4-byte scriptNum would only support up to a maximum of 2^31-1. Thus, a 5-byte + // scriptNum is used here since it will support up to 2^39-1 which allows sequences beyond the current sequence + // limit. PeekByteArray is used here instead of PeekInt because we do not want to be limited to a 4-byte integer for + // reasons specified above. + so, e := vm.dstack.PeekByteArray(0) + if e != nil { + return e + } + stackSequence, e := makeScriptNum(so, vm.dstack.verifyMinimalData, 5) + if e != nil { + return e + } + // In the rare event that the argument needs to be < 0 due to some arithmetic being done first, you can always use 0 + // OP_MAX OP_CHECKSEQUENCEVERIFY. + if stackSequence < 0 { + str := fmt.Sprintf("negative sequence: %d", stackSequence) + return scriptError(ErrNegativeLockTime, str) + } + sequence := int64(stackSequence) + // To provide for future soft-fork extensibility, if the operand has the disabled lock-time flag set, + // CHECKSEQUENCEVERIFY behaves as a NOP. + if sequence&int64(wire.SequenceLockTimeDisabled) != 0 { + return nil + } + // Transaction version numbers not high enough to trigger CSV rules must fail. + if vm.tx.Version < 2 { + str := fmt.Sprintf( + "invalid transaction version: %d", + vm.tx.Version, + ) + return scriptError(ErrUnsatisfiedLockTime, str) + } + // Sequence numbers with their most significant bit set are not consensus constrained. Testing that the + // transaction's sequence number does not have this bit set prevents using this property to get around a + // CHECKSEQUENCEVERIFY check. + txSequence := int64(vm.tx.TxIn[vm.txIdx].Sequence) + if txSequence&int64(wire.SequenceLockTimeDisabled) != 0 { + str := fmt.Sprintf( + "transaction sequence has sequence "+ + "locktime disabled bit set: 0x%x", txSequence, + ) + return scriptError(ErrUnsatisfiedLockTime, str) + } + // Mask off non-consensus bits before doing comparisons. + lockTimeMask := int64( + wire.SequenceLockTimeIsSeconds | + wire.SequenceLockTimeMask, + ) + return verifyLockTime( + txSequence&lockTimeMask, + wire.SequenceLockTimeIsSeconds, sequence&lockTimeMask, + ) +} + +// opcodeToAltStack removes the top item from the main data stack and pushes it onto the alternate data stack. +// +// Main data stack transformation: [... x1 x2 x3] -> [... x1 x2] +// +// Alt data stack transformation: [... y1 y2 y3] -> [... y1 y2 y3 x3] +func opcodeToAltStack(op *parsedOpcode, vm *Engine) (e error) { + so, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + vm.astack.PushByteArray(so) + return nil +} + +// opcodeFromAltStack removes the top item from the alternate data stack and pushes it onto the main data stack. +// +// Main data stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 y3] +// +// Alt data stack transformation: [... y1 y2 y3] -> [... y1 y2] +func opcodeFromAltStack(op *parsedOpcode, vm *Engine) (e error) { + so, e := vm.astack.PopByteArray() + if e != nil { + return e + } + vm.dstack.PushByteArray(so) + return nil +} + +// opcode2Drop removes the top 2 items from the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1] +func opcode2Drop(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.DropN(2) +} + +// opcode2Dup duplicates the top 2 items on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x2 x3] +func opcode2Dup(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.DupN(2) +} + +// opcode3Dup duplicates the top 3 items on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x1 x2 x3] +func opcode3Dup(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.DupN(3) +} + +// opcode2Over duplicates the 2 items before the top 2 items on the data stack. +// +// Stack transformation: [... x1 x2 x3 x4] -> [... x1 x2 x3 x4 x1 x2] +func opcode2Over(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.OverN(2) +} + +// opcode2Rot rotates the top 6 items on the data stack to the left twice. +// +// Stack transformation: [... x1 x2 x3 x4 x5 x6] -> [... x3 x4 x5 x6 x1 x2] +func opcode2Rot(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.RotN(2) +} + +// opcode2Swap swaps the top 2 items on the data stack with the 2 that come before them. +// +// Stack transformation: [... x1 x2 x3 x4] -> [... x3 x4 x1 x2] +func opcode2Swap(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.SwapN(2) +} + +// opcodeIfDup duplicates the top item of the stack if it is not zero. +// +// Stack transformation (x1==0): [... x1] -> [... x1] +// +// Stack transformation (x1!=0): [... x1] -> [... x1 x1] +func opcodeIfDup(op *parsedOpcode, vm *Engine) (e error) { + so, e := vm.dstack.PeekByteArray(0) + if e != nil { + return e + } + // Push copy of data iff it isn't zero + if asBool(so) { + vm.dstack.PushByteArray(so) + } + return nil +} + +// opcodeDepth pushes the depth of the data stack prior to executing this opcode, encoded as a number, onto the data +// stack. +// +// Stack transformation: [...] -> [... ] +// +// Example with 2 items: [x1 x2] -> [x1 x2 2] +// +// Example with 3 items: [x1 x2 x3] -> [x1 x2 x3 3] +func opcodeDepth(op *parsedOpcode, vm *Engine) (e error) { + vm.dstack.PushInt(scriptNum(vm.dstack.Depth())) + return nil +} + +// opcodeDrop removes the top item from the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func opcodeDrop(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.DropN(1) +} + +// opcodeDup duplicates the top item on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x3] +func opcodeDup(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.DupN(1) +} + +// opcodeNip removes the item before the top item on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x3] +func opcodeNip(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.NipN(1) +} + +// opcodeOver duplicates the item before the top item on the data stack. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2 x3 x2] +func opcodeOver(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.OverN(1) +} + +// opcodePick treats the top item on the data stack as an integer and duplicates the item on the stack that number of +// items back to the top. +// +// Stack transformation: [xn ... x2 x1 x0 n] -> [xn ... x2 x1 x0 xn] +// +// Example with n=1: [x2 x1 x0 1] -> [x2 x1 x0 x1] +// +// Example with n=2: [x2 x1 x0 2] -> [x2 x1 x0 x2] +func opcodePick(op *parsedOpcode, vm *Engine) (e error) { + val, e := vm.dstack.PopInt() + if e != nil { + return e + } + return vm.dstack.PickN(val.Int32()) +} + +// opcodeRoll treats the top item on the data stack as an integer and moves the item on the stack that number of items +// back to the top. +// +// Stack transformation: [xn ... x2 x1 x0 n] -> [... x2 x1 x0 xn] +// +// Example with n=1: [x2 x1 x0 1] -> [x2 x0 x1] +// +// Example with n=2: [x2 x1 x0 2] -> [x1 x0 x2] +func opcodeRoll(op *parsedOpcode, vm *Engine) (e error) { + val, e := vm.dstack.PopInt() + if e != nil { + return e + } + return vm.dstack.RollN(val.Int32()) +} + +// opcodeRot rotates the top 3 items on the data stack to the left. +// +// Stack transformation: [... x1 x2 x3] -> [... x2 x3 x1] +func opcodeRot(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.RotN(1) +} + +// opcodeSwap swaps the top two items on the stack. +// +// Stack transformation: [... x1 x2] -> [... x2 x1] +func opcodeSwap(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.SwapN(1) +} + +// opcodeTuck inserts a duplicate of the top item of the data stack before the second-to-top item. +// +// Stack transformation: [... x1 x2] -> [... x2 x1 x2] +func opcodeTuck(op *parsedOpcode, vm *Engine) (e error) { + return vm.dstack.Tuck() +} + +// opcodeSize pushes the size of the top item of the data stack onto the data stack. +// +// Stack transformation: [... x1] -> [... x1 len(x1)] +func opcodeSize(op *parsedOpcode, vm *Engine) (e error) { + so, e := vm.dstack.PeekByteArray(0) + if e != nil { + return e + } + vm.dstack.PushInt(scriptNum(len(so))) + return nil +} + +// opcodeEqual removes the top 2 items of the data stack, compares them as raw bytes, and pushes the result, encoded as +// a boolean, back to the stack. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeEqual(op *parsedOpcode, vm *Engine) (e error) { + a, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + b, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + vm.dstack.PushBool(bytes.Equal(a, b)) + return nil +} + +// opcodeEqualVerify is a combination of opcodeEqual and opcodeVerify. Specifically, it removes the top 2 items of the +// data stack, compares them, and pushes the result, encoded as a boolean, back to the stack. Then, it examines the top +// item on the data stack as a boolean value and verifies it evaluates to true. An error is returned if it does not. +// +// Stack transformation: [... x1 x2] -> [... bool] -> [...] +func opcodeEqualVerify(op *parsedOpcode, vm *Engine) (e error) { + e = opcodeEqual(op, vm) + if e == nil { + e = abstractVerify(op, vm, ErrEqualVerify) + } + return e +} + +// opcode1Add treats the top item on the data stack as an integer and replaces it with its incremented value (plus 1). +// +// Stack transformation: [... x1 x2] -> [... x1 x2+1] +func opcode1Add(op *parsedOpcode, vm *Engine) (e error) { + var m scriptNum + m, e = vm.dstack.PopInt() + if e != nil { + return e + } + vm.dstack.PushInt(m + 1) + return nil +} + +// opcode1Sub treats the top item on the data stack as an integer and replaces it with its decremented value (minus 1). +// +// Stack transformation: [... x1 x2] -> [... x1 x2-1] +func opcode1Sub(op *parsedOpcode, vm *Engine) (e error) { + var m scriptNum + m, e = vm.dstack.PopInt() + if e != nil { + return e + } + vm.dstack.PushInt(m - 1) + return nil +} + +// opcodeNegate treats the top item on the data stack as an integer and replaces it with its negation. +// +// Stack transformation: [... x1 x2] -> [... x1 -x2] +func opcodeNegate(op *parsedOpcode, vm *Engine) (e error) { + var m scriptNum + m, e = vm.dstack.PopInt() + if e != nil { + return e + } + vm.dstack.PushInt(-m) + return nil +} + +// opcodeAbs treats the top item on the data stack as an integer and replaces it it with its absolute value. +// +// Stack transformation: [... x1 x2] -> [... x1 abs(x2)] +func opcodeAbs(op *parsedOpcode, vm *Engine) (e error) { + var m scriptNum + m, e = vm.dstack.PopInt() + if e != nil { + return e + } + if m < 0 { + m = -m + } + vm.dstack.PushInt(m) + return nil +} + +// opcodeNot treats the top item on the data stack as an integer and replaces it with its "inverted" value (0 becomes 1, +// non-zero becomes 0). +// +// NOTE: While it would probably make more sense to treat the top item as a boolean, and push the opposite, which is +// really what the intention of this opcode is, it is extremely important that is not done because integers are +// interpreted differently than booleans and the consensus rules for this opcode dictate the item is interpreted as an +// integer. +// +// Stack transformation (x2==0): [... x1 0] -> [... x1 1] +// +// Stack transformation (x2!=0): [... x1 1] -> [... x1 0] +// +// Stack transformation (x2!=0): [... x1 17] -> [... x1 0] +func opcodeNot(op *parsedOpcode, vm *Engine) (e error) { + var m scriptNum + m, e = vm.dstack.PopInt() + if e != nil { + return e + } + if m == 0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcode0NotEqual treats the top item on the data stack as an integer and replaces it with either a 0 if it is zero, or +// a 1 if it is not zero. +// +// Stack transformation (x2==0): [... x1 0] -> [... x1 0] +// +// Stack transformation (x2!=0): [... x1 1] -> [... x1 1] +// +// Stack transformation (x2!=0): [... x1 17] -> [... x1 1] +func opcode0NotEqual(op *parsedOpcode, vm *Engine) (e error) { + var m scriptNum + m, e = vm.dstack.PopInt() + if e != nil { + return e + } + if m != 0 { + m = 1 + } + vm.dstack.PushInt(m) + return nil +} + +// opcodeAdd treats the top two items on the data stack as integers and replaces them with their sum. +// +// Stack transformation: [... x1 x2] -> [... x1+x2] +func opcodeAdd(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + vm.dstack.PushInt(v0 + v1) + return nil +} + +// opcodeSub treats the top two items on the data stack as integers and replaces them with the result of subtracting the +// top entry from the second-to-top entry. +// +// Stack transformation: [... x1 x2] -> [... x1-x2] +func opcodeSub(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + vm.dstack.PushInt(v1 - v0) + return nil +} + +// opcodeBoolAnd treats the top two items on the data stack as integers. When both of them are not zero, they are +// replaced with a 1, otherwise a 0. +// +// Stack transformation (x1==0, x2==0): [... 0 0] -> [... 0] +// +// Stack transformation (x1!=0, x2==0): [... 5 0] -> [... 0] +// +// Stack transformation (x1==0, x2!=0): [... 0 7] -> [... 0] +// +// Stack transformation (x1!=0, x2!=0): [... 4 8] -> [... 1] +func opcodeBoolAnd(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v0 != 0 && v1 != 0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeBoolOr treats the top two items on the data stack as integers. When either of them are not zero, they are +// replaced with a 1, otherwise a 0. +// +// Stack transformation (x1==0, x2==0): [... 0 0] -> [... 0] +// +// Stack transformation (x1!=0, x2==0): [... 5 0] -> [... 1] +// +// Stack transformation (x1==0, x2!=0): [... 0 7] -> [... 1] +// +// Stack transformation (x1!=0, x2!=0): [... 4 8] -> [... 1] +func opcodeBoolOr(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v0 != 0 || v1 != 0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeNumEqual treats the top two items on the data stack as integers. When they are equal, they are replaced with a +// 1, otherwise a 0. +// +// Stack transformation (x1==x2): [... 5 5] -> [... 1] +// +// Stack transformation (x1!=x2): [... 5 7] -> [... 0] +func opcodeNumEqual(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v0 == v1 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeNumEqualVerify is a combination of opcodeNumEqual and opcodeVerify. Specifically, treats the top two items on +// the data stack as integers. +// +// When they are equal, they are replaced with a 1, otherwise a 0. Then, it examines the top item on the data stack as a +// boolean value and verifies it evaluates to true. An error is returned if it does not. +// +// Stack transformation: [... x1 x2] -> [... bool] -> [...] +func opcodeNumEqualVerify(op *parsedOpcode, vm *Engine) (e error) { + e = opcodeNumEqual(op, vm) + if e == nil { + e = abstractVerify(op, vm, ErrNumEqualVerify) + } + return e +} + +// opcodeNumNotEqual treats the top two items on the data stack as integers. When they are NOT equal, they are replaced +// with a 1, otherwise a 0. +// +// Stack transformation (x1==x2): [... 5 5] -> [... 0] +// +// Stack transformation (x1!=x2): [... 5 7] -> [... 1] +func opcodeNumNotEqual(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v0 != v1 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeLessThan treats the top two items on the data stack as integers. When the second-to-top item is less than the +// top item, they are replaced with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeLessThan(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v1 < v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeGreaterThan treats the top two items on the data stack as integers. When the second-to-top item is greater than +// the top item, they are replaced with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeGreaterThan(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v1 > v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeLessThanOrEqual treats the top two items on the data stack as integers. When the second-to-top item is less +// than or equal to the top item, they are replaced with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeLessThanOrEqual(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v1 <= v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeGreaterThanOrEqual treats the top two items on the data stack as integers. When the second-to-top item is +// greater than or equal to the top item, they are replaced with a 1, otherwise a 0. +// +// Stack transformation: [... x1 x2] -> [... bool] +func opcodeGreaterThanOrEqual(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v1 >= v0 { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// opcodeMin treats the top two items on the data stack as integers and replaces with the minimum of the two. +// +// Stack transformation: [... x1 x2] -> [... min(x1, x2)] +func opcodeMin(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v1 < v0 { + vm.dstack.PushInt(v1) + } else { + vm.dstack.PushInt(v0) + } + return nil +} + +// opcodeMax treats the top two items on the data stack as integers and replaces them with the maximum of the two. +// +// Stack transformation: [... x1 x2] -> [... max(x1, x2)] +func opcodeMax(op *parsedOpcode, vm *Engine) (e error) { + v0, e := vm.dstack.PopInt() + if e != nil { + return e + } + v1, e := vm.dstack.PopInt() + if e != nil { + return e + } + if v1 > v0 { + vm.dstack.PushInt(v1) + } else { + vm.dstack.PushInt(v0) + } + return nil +} + +// opcodeWithin treats the top 3 items on the data stack as integers. When the value to test is within the specified +// range (left inclusive), they are replaced with a 1, otherwise a 0. The top item is the max value, the second-top-item +// is the minimum value, and the third-to-top item is the value to test. +// +// Stack transformation: [... x1 min max] -> [... bool] +func opcodeWithin(op *parsedOpcode, vm *Engine) (e error) { + maxVal, e := vm.dstack.PopInt() + if e != nil { + return e + } + minVal, e := vm.dstack.PopInt() + if e != nil { + return e + } + x, e := vm.dstack.PopInt() + if e != nil { + return e + } + if x >= minVal && x < maxVal { + vm.dstack.PushInt(scriptNum(1)) + } else { + vm.dstack.PushInt(scriptNum(0)) + } + return nil +} + +// calcHash calculates the hash of hasher over buf. +func calcHash(buf []byte, hasher hash.Hash) []byte { + _, e := hasher.Write(buf) + if e != nil { + D.Ln(e) + } + return hasher.Sum(nil) +} + +// opcodeRipeMD160 treats the top item of the data stack as raw bytes and replaces it with ripemd160(data). +// +// Stack transformation: [... x1] -> [... ripemd160(x1)] +func opcodeRipeMD160(op *parsedOpcode, vm *Engine) (e error) { + buf, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + vm.dstack.PushByteArray(calcHash(buf, ripemd160.New())) + return nil +} + +// opcodeSHA1 treats the top item of the data stack as raw bytes and replaces it with sha1(data). +// +// Stack transformation: [... x1] -> [... sha1(x1)] +func opcodeSHA1(op *parsedOpcode, vm *Engine) (e error) { + buf, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + hassh := sha1.Sum(buf) + vm.dstack.PushByteArray(hassh[:]) + return nil +} + +// opcodeSHA256 treats the top item of the data stack as raw bytes and replaces it with sha256(data). +// +// Stack transformation: [... x1] -> [... sha256(x1)] +func opcodeSHA256(op *parsedOpcode, vm *Engine) (e error) { + buf, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + hassh := sha256.Sum256(buf) + vm.dstack.PushByteArray(hassh[:]) + return nil +} + +// opcodeHash160 treats the top item of the data stack as raw bytes and replaces it with ripemd160(sha256(data)). +// +// Stack transformation: [... x1] -> [... ripemd160(sha256(x1))] +func opcodeHash160(op *parsedOpcode, vm *Engine) (e error) { + buf, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + hassh := sha256.Sum256(buf) + vm.dstack.PushByteArray(calcHash(hassh[:], ripemd160.New())) + return nil +} + +// opcodeHash256 treats the top item of the data stack as raw bytes and replaces it with sha256(sha256(data)). +// +// Stack transformation: [... x1] -> [... sha256(sha256(x1))] +func opcodeHash256(op *parsedOpcode, vm *Engine) (e error) { + buf, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + vm.dstack.PushByteArray(chainhash.DoubleHashB(buf)) + return nil +} + +// opcodeCodeSeparator stores the current script offset as the most recently seen OP_CODESEPARATOR which is used during +// signature checking. +// +// This opcode does not change the contents of the data stack. +func opcodeCodeSeparator(op *parsedOpcode, vm *Engine) (e error) { + vm.lastCodeSep = int(vm.scriptOff.Load()) + return nil +} + +// opcodeCheckSig treats the top 2 items on the stack as a public key and a signature and replaces them with a bool +// which indicates if the signature was successfully verified. +// +// The process of verifying a signature requires calculating a signature hash in the same way the transaction signer +// did. +// +// It involves hashing portions of the transaction based on the hash type byte (which is the final byte of the +// signature) and the portion of the script starting from the most recent OP_CODESEPARATOR ( or the beginning of the +// script if there are none) to the end of the script (with any other OP_CODESEPARATORs removed). +// +// Once this "script hash" is calculated, the signature is checked using standard cryptographic methods against the +// provided public key. +// +// Stack transformation: [... signature pubkey] -> [... bool] +func opcodeCheckSig(op *parsedOpcode, vm *Engine) (e error) { + pkBytes, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + fullSigBytes, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + // The signature actually needs needs to be longer than this, but at 1 byte is needed for the hash type below. The + // full length is checked depending on the script flags and upon parsing the signature. + if len(fullSigBytes) < 1 { + vm.dstack.PushBool(false) + return nil + } + // Trim off hash type from the signature string and check if the signature and pubkey conform to the strict encoding + // requirements depending on the flags. + // + // NOTE: When the strict encoding flags are set, any errors in the signature or public encoding here result in an + // immediate script error (and thus no result bool is pushed to the data stack). This differs from the logic below + // where any errors in parsing the signature is treated as the signature failure resulting in false being pushed to + // the data stack. + // + // This is required because the more general script validation consensus rules do not have the new strict encoding + // requirements enabled by the flags. + hashType := SigHashType(fullSigBytes[len(fullSigBytes)-1]) + sigBytes := fullSigBytes[:len(fullSigBytes)-1] + if e = vm.checkHashTypeEncoding(hashType); E.Chk(e) { + return e + } + if e = vm.checkSignatureEncoding(sigBytes); E.Chk(e) { + return e + } + if e = vm.checkPubKeyEncoding(pkBytes); E.Chk(e) { + return e + } + // Get script starting from the most recent OP_CODESEPARATOR. + subScript := vm.subScript() + // Generate the signature hash based on the signature hash type. + var hassh []byte + if vm.isWitnessVersionActive(0) { + var sigHashes *TxSigHashes + if vm.hashCache != nil { + sigHashes = vm.hashCache + } else { + sigHashes = NewTxSigHashes(&vm.tx) + } + hassh, e = calcWitnessSignatureHash( + subScript, sigHashes, hashType, + &vm.tx, vm.txIdx, vm.inputAmount, + ) + if e != nil { + return e + } + } else { + // Remove the signature since there is no way for a signature to sign itself. + subScript = removeOpcodeByData(subScript, fullSigBytes) + hassh = calcSignatureHash(subScript, hashType, &vm.tx, vm.txIdx) + } + pubKey, e := ec.ParsePubKey(pkBytes, ec.S256()) + if e != nil { + vm.dstack.PushBool(false) + return nil + } + var signature *ec.Signature + if vm.hasFlag(ScriptVerifyStrictEncoding) || + vm.hasFlag(ScriptVerifyDERSignatures) { + signature, e = ec.ParseDERSignature(sigBytes, ec.S256()) + } else { + signature, e = ec.ParseSignature(sigBytes, ec.S256()) + } + if e != nil { + vm.dstack.PushBool(false) + return nil + } + var valid bool + if vm.sigCache != nil { + var sigHash chainhash.Hash + copy(sigHash[:], hassh) + valid = vm.sigCache.Exists(sigHash, signature, pubKey) + if !valid && signature.Verify(hassh, pubKey) { + vm.sigCache.Add(sigHash, signature, pubKey) + valid = true + } + } else { + valid = signature.Verify(hassh, pubKey) + } + if !valid && vm.hasFlag(ScriptVerifyNullFail) && len(sigBytes) > 0 { + str := "signature not empty on failed checksig" + return scriptError(ErrNullFail, str) + } + vm.dstack.PushBool(valid) + return nil +} + +// opcodeCheckSigVerify is a combination of opcodeCheckSig and opcodeVerify. The opcodeCheckSig function is invoked +// followed by opcodeVerify. See the documentation for each of those opcodes for more details. +// +// Stack transformation: signature pubkey] -> [... bool] -> [...] +func opcodeCheckSigVerify(op *parsedOpcode, vm *Engine) (e error) { + e = opcodeCheckSig(op, vm) + if e == nil { + e = abstractVerify(op, vm, ErrCheckSigVerify) + } + return e +} + +// parsedSigInfo houses a raw signature along with its parsed form and a flag for whether or not it has already been +// parsed. +// +// It is used to prevent parsing the same signature multiple times when verifying a multisig. +type parsedSigInfo struct { + signature []byte + parsedSignature *ec.Signature + parsed bool +} + +// opcodeCheckMultiSig treats the top item on the stack as an integer number of public keys, followed by that many +// entries as raw data representing the public keys, followed by the integer number of signatures, followed by that many +// entries as raw data representing the signatures. +// +// Due to a bug in the original Satoshi client implementation, an additional dummy argument is also required by the +// consensus rules, although it is not used. The dummy value SHOULD be an OP_0, although that is not required by the +// consensus rules. +// +// When the ScriptStrictMultiSig flag is set, it must be OP_0. All of the aforementioned stack items are replaced with a +// bool which indicates if the requisite number of signatures were successfully verified. See the opcodeCheckSigVerify +// documentation for more details about the process for verifying each signature. +// +// Stack transformation: +// +// [... dummy [sig ...] numsigs [pubkey ...] numpubkeys] -> [... bool] +func opcodeCheckMultiSig(op *parsedOpcode, vm *Engine) (e error) { + numKeys, e := vm.dstack.PopInt() + if e != nil { + return e + } + numPubKeys := int(numKeys.Int32()) + if numPubKeys < 0 { + str := fmt.Sprintf( + "number of pubkeys %d is negative", + numPubKeys, + ) + return scriptError(ErrInvalidPubKeyCount, str) + } + if numPubKeys > MaxPubKeysPerMultiSig { + str := fmt.Sprintf( + "too many pubkeys: %d > %d", + numPubKeys, MaxPubKeysPerMultiSig, + ) + return scriptError(ErrInvalidPubKeyCount, str) + } + vm.numOps += numPubKeys + if vm.numOps > MaxOpsPerScript { + str := fmt.Sprintf( + "exceeded max operation limit of %d", + MaxOpsPerScript, + ) + return scriptError(ErrTooManyOperations, str) + } + pubKeys := make([][]byte, 0, numPubKeys) + for i := 0; i < numPubKeys; i++ { + var pubKey []byte + pubKey, e = vm.dstack.PopByteArray() + if e != nil { + return e + } + pubKeys = append(pubKeys, pubKey) + } + numSigs, e := vm.dstack.PopInt() + if e != nil { + return e + } + numSignatures := int(numSigs.Int32()) + if numSignatures < 0 { + str := fmt.Sprintf( + "number of signatures %d is negative", + numSignatures, + ) + return scriptError(ErrInvalidSignatureCount, str) + } + if numSignatures > numPubKeys { + str := fmt.Sprintf( + "more signatures than pubkeys: %d > %d", + numSignatures, numPubKeys, + ) + return scriptError(ErrInvalidSignatureCount, str) + } + signatures := make([]*parsedSigInfo, 0, numSignatures) + for i := 0; i < numSignatures; i++ { + var signature []byte + signature, e = vm.dstack.PopByteArray() + if e != nil { + return e + } + sigInfo := &parsedSigInfo{signature: signature} + signatures = append(signatures, sigInfo) + } + // A bug in the original Satoshi client implementation means one more stack value than should be used must be + // popped. Unfortunately, this buggy behavior is now part of the consensus and a hard fork would be required to fix + // it. + dummy, e := vm.dstack.PopByteArray() + if e != nil { + return e + } + // Since the dummy argument is otherwise not checked, it could be any value which unfortunately provides a source of + // malleability. Thus, there is a script flag to force an error when the value is NOT 0. + if vm.hasFlag(ScriptStrictMultiSig) && len(dummy) != 0 { + str := fmt.Sprintf( + "multisig dummy argument has length %d "+ + "instead of 0", len(dummy), + ) + return scriptError(ErrSigNullDummy, str) + } + // Get script starting from the most recent OP_CODESEPARATOR. + script := vm.subScript() + // Remove the signature in pre version 0 segwit scripts since there is no way for a signature to sign itself. + if !vm.isWitnessVersionActive(0) { + for _, sigInfo := range signatures { + script = removeOpcodeByData(script, sigInfo.signature) + } + } + success := true + numPubKeys++ + pubKeyIdx := -1 + signatureIdx := 0 + for numSignatures > 0 { + // When there are more signatures than public keys remaining, there is no way to succeed since too many + // signatures are invalid, so exit early. + pubKeyIdx++ + numPubKeys-- + if numSignatures > numPubKeys { + success = false + break + } + sigInfo := signatures[signatureIdx] + pubKey := pubKeys[pubKeyIdx] + // The order of the signature and public key evaluation is important here since it can be distinguished by an + // OP_CHECKMULTISIG NOT when the strict encoding flag is set. + rawSig := sigInfo.signature + if len(rawSig) == 0 { + // Skip to the next pubkey if signature is empty. + continue + } + // Split the signature into hash type and signature components. + hashType := SigHashType(rawSig[len(rawSig)-1]) + signature := rawSig[:len(rawSig)-1] + // Only parse and check the signature encoding once. + var parsedSig *ec.Signature + if !sigInfo.parsed { + if e := vm.checkHashTypeEncoding(hashType); E.Chk(e) { + return e + } + if e := vm.checkSignatureEncoding(signature); E.Chk(e) { + return e + } + // Parse the signature. + var e error + if vm.hasFlag(ScriptVerifyStrictEncoding) || + vm.hasFlag(ScriptVerifyDERSignatures) { + parsedSig, e = ec.ParseDERSignature( + signature, + ec.S256(), + ) + } else { + parsedSig, e = ec.ParseSignature( + signature, + ec.S256(), + ) + } + sigInfo.parsed = true + if e != nil { + continue + } + sigInfo.parsedSignature = parsedSig + } else { + // Skip to the next pubkey if the signature is invalid. + if sigInfo.parsedSignature == nil { + continue + } + // Use the already parsed signature. + parsedSig = sigInfo.parsedSignature + } + if e := vm.checkPubKeyEncoding(pubKey); E.Chk(e) { + return e + } + // Parse the pubkey. + parsedPubKey, e := ec.ParsePubKey(pubKey, ec.S256()) + if e != nil { + continue + } + // Generate the signature hash based on the signature hash type. + var hash []byte + if vm.isWitnessVersionActive(0) { + var sigHashes *TxSigHashes + if vm.hashCache != nil { + sigHashes = vm.hashCache + } else { + sigHashes = NewTxSigHashes(&vm.tx) + } + hash, e = calcWitnessSignatureHash( + script, sigHashes, hashType, + &vm.tx, vm.txIdx, vm.inputAmount, + ) + if e != nil { + return e + } + } else { + hash = calcSignatureHash(script, hashType, &vm.tx, vm.txIdx) + } + var valid bool + if vm.sigCache != nil { + var sigHash chainhash.Hash + copy(sigHash[:], hash) + valid = vm.sigCache.Exists(sigHash, parsedSig, parsedPubKey) + if !valid && parsedSig.Verify(hash, parsedPubKey) { + vm.sigCache.Add(sigHash, parsedSig, parsedPubKey) + valid = true + } + } else { + valid = parsedSig.Verify(hash, parsedPubKey) + } + if valid { + // PubKey verified, move on to the next signature. + signatureIdx++ + numSignatures-- + } + } + if !success && vm.hasFlag(ScriptVerifyNullFail) { + for _, sig := range signatures { + if len(sig.signature) > 0 { + str := "not all signatures empty on failed checkmultisig" + return scriptError(ErrNullFail, str) + } + } + } + vm.dstack.PushBool(success) + return nil +} + +// opcodeCheckMultiSigVerify is a combination of opcodeCheckMultiSig and opcodeVerify. +// +// The opcodeCheckMultiSig is invoked followed by opcodeVerify. See the documentation for each of those opcodes for more +// details. + +// Stack transformation: +// +// [... dummy [sig ...] numsigs [pubkey ...] numpubkeys] -> [... bool] -> [...] +func opcodeCheckMultiSigVerify(op *parsedOpcode, vm *Engine) (e error) { + e = opcodeCheckMultiSig(op, vm) + if e == nil { + e = abstractVerify(op, vm, ErrCheckMultiSigVerify) + } + return e +} + +// OpcodeByName is a map that can be used to lookup an opcode by its human-readable name (OP_CHECKMULTISIG, OP_CHECKSIG, +// etc). +var OpcodeByName = make(map[string]byte) + +// Initialize the opcode name to value map using the contents of the opcode array. Also add entries for "OP_FALSE", +// "OP_TRUE", and "OP_NOP2" since they are aliases for "OP_0", "OP_1", and "OP_CHECKLOCKTIMEVERIFY" respectively. +func init() { + for _, op := range OpcodeArray { + OpcodeByName[op.name] = op.value + } + OpcodeByName["OP_FALSE"] = OP_FALSE + OpcodeByName["OP_TRUE"] = OP_TRUE + OpcodeByName["OP_NOP2"] = OP_CHECKLOCKTIMEVERIFY + OpcodeByName["OP_NOP3"] = OP_CHECKSEQUENCEVERIFY +} diff --git a/pkg/txscript/opcode_test.go b/pkg/txscript/opcode_test.go new file mode 100644 index 0000000..7b28a92 --- /dev/null +++ b/pkg/txscript/opcode_test.go @@ -0,0 +1,186 @@ +package txscript + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "testing" +) + +// TestOpcodeDisabled tests the opcodeDisabled function manually because all disabled opcodes result in a script +// execution failure when executed normally, so the function is not called under normal circumstances. +func TestOpcodeDisabled(t *testing.T) { + t.Parallel() + tests := []byte{OP_CAT, OP_SUBSTR, OP_LEFT, OP_RIGHT, OP_INVERT, + OP_AND, OP_OR, OP_2MUL, OP_2DIV, OP_MUL, OP_DIV, OP_MOD, + OP_LSHIFT, OP_RSHIFT, + } + for _, opcodeVal := range tests { + pop := parsedOpcode{opcode: &OpcodeArray[opcodeVal], data: nil} + e := opcodeDisabled(&pop, nil) + if !IsErrorCode(e, ErrDisabledOpcode) { + t.Errorf("opcodeDisabled: unexpected error - got %v, "+ + "want %v", e, ErrDisabledOpcode, + ) + continue + } + } +} + +// TestOpcodeDisasm tests the print function for all opcodes in both the oneline and full modes to ensure it provides +// the expected disassembly. +func TestOpcodeDisasm(t *testing.T) { + t.Parallel() + // First, test the oneline disassembly. The expected strings for the data push opcodes are replaced in the test + // loops below since they involve repeating bytes. Also, the OP_NOP# and OP_UNKNOWN# are replaced below too, since + // it's easier than manually listing them here. + oneBytes := []byte{0x01} + oneStr := "01" + expectedStrings := [256]string{0x00: "0", 0x4f: "-1", + 0x50: "OP_RESERVED", 0x61: "OP_NOP", 0x62: "OP_VER", + 0x63: "OP_IF", 0x64: "OP_NOTIF", 0x65: "OP_VERIF", + 0x66: "OP_VERNOTIF", 0x67: "OP_ELSE", 0x68: "OP_ENDIF", + 0x69: "OP_VERIFY", 0x6a: "OP_RETURN", 0x6b: "OP_TOALTSTACK", + 0x6c: "OP_FROMALTSTACK", 0x6d: "OP_2DROP", 0x6e: "OP_2DUP", + 0x6f: "OP_3DUP", 0x70: "OP_2OVER", 0x71: "OP_2ROT", + 0x72: "OP_2SWAP", 0x73: "OP_IFDUP", 0x74: "OP_DEPTH", + 0x75: "OP_DROP", 0x76: "OP_DUP", 0x77: "OP_NIP", + 0x78: "OP_OVER", 0x79: "OP_PICK", 0x7a: "OP_ROLL", + 0x7b: "OP_ROT", 0x7c: "OP_SWAP", 0x7d: "OP_TUCK", + 0x7e: "OP_CAT", 0x7f: "OP_SUBSTR", 0x80: "OP_LEFT", + 0x81: "OP_RIGHT", 0x82: "OP_SIZE", 0x83: "OP_INVERT", + 0x84: "OP_AND", 0x85: "OP_OR", 0x86: "OP_XOR", + 0x87: "OP_EQUAL", 0x88: "OP_EQUALVERIFY", 0x89: "OP_RESERVED1", + 0x8a: "OP_RESERVED2", 0x8b: "OP_1ADD", 0x8c: "OP_1SUB", + 0x8d: "OP_2MUL", 0x8e: "OP_2DIV", 0x8f: "OP_NEGATE", + 0x90: "OP_ABS", 0x91: "OP_NOT", 0x92: "OP_0NOTEQUAL", + 0x93: "OP_ADD", 0x94: "OP_SUB", 0x95: "OP_MUL", 0x96: "OP_DIV", + 0x97: "OP_MOD", 0x98: "OP_LSHIFT", 0x99: "OP_RSHIFT", + 0x9a: "OP_BOOLAND", 0x9b: "OP_BOOLOR", 0x9c: "OP_NUMEQUAL", + 0x9d: "OP_NUMEQUALVERIFY", 0x9e: "OP_NUMNOTEQUAL", + 0x9f: "OP_LESSTHAN", 0xa0: "OP_GREATERTHAN", + 0xa1: "OP_LESSTHANOREQUAL", 0xa2: "OP_GREATERTHANOREQUAL", + 0xa3: "OP_MIN", 0xa4: "OP_MAX", 0xa5: "OP_WITHIN", + 0xa6: "OP_RIPEMD160", 0xa7: "OP_SHA1", 0xa8: "OP_SHA256", + 0xa9: "OP_HASH160", 0xaa: "OP_HASH256", 0xab: "OP_CODESEPARATOR", + 0xac: "OP_CHECKSIG", 0xad: "OP_CHECKSIGVERIFY", + 0xae: "OP_CHECKMULTISIG", 0xaf: "OP_CHECKMULTISIGVERIFY", + 0xfa: "OP_SMALLINTEGER", 0xfb: "OP_PUBKEYS", + 0xfd: "OP_PUBKEYHASH", 0xfe: "OP_PUBKEY", + 0xff: "OP_INVALIDOPCODE", + } + for opcodeVal, expectedStr := range expectedStrings { + var data []byte + switch { + // OP_DATA_1 through OP_DATA_65 display the pushed data. + case opcodeVal >= 0x01 && opcodeVal < 0x4c: + data = bytes.Repeat(oneBytes, opcodeVal) + expectedStr = strings.Repeat(oneStr, opcodeVal) + // OP_PUSHDATA1. + case opcodeVal == 0x4c: + data = bytes.Repeat(oneBytes, 1) + expectedStr = strings.Repeat(oneStr, 1) + // OP_PUSHDATA2. + case opcodeVal == 0x4d: + data = bytes.Repeat(oneBytes, 2) + expectedStr = strings.Repeat(oneStr, 2) + // OP_PUSHDATA4. + case opcodeVal == 0x4e: + data = bytes.Repeat(oneBytes, 3) + expectedStr = strings.Repeat(oneStr, 3) + // OP_1 through OP_16 display the numbers themselves. + case opcodeVal >= 0x51 && opcodeVal <= 0x60: + val := byte(opcodeVal - (0x51 - 1)) + data = []byte{val} + expectedStr = strconv.Itoa(int(val)) + // OP_NOP1 through OP_NOP10. + case opcodeVal >= 0xb0 && opcodeVal <= 0xb9: + switch opcodeVal { + case 0xb1: + // OP_NOP2 is an alias of OP_CHECKLOCKTIMEVERIFY + expectedStr = "OP_CHECKLOCKTIMEVERIFY" + case 0xb2: + // OP_NOP3 is an alias of OP_CHECKSEQUENCEVERIFY + expectedStr = "OP_CHECKSEQUENCEVERIFY" + default: + val := byte(opcodeVal - (0xb0 - 1)) + expectedStr = "OP_NOP" + strconv.Itoa(int(val)) + } + // OP_UNKNOWN#. + case opcodeVal >= 0xba && opcodeVal <= 0xf9 || opcodeVal == 0xfc: + expectedStr = "OP_UNKNOWN" + strconv.Itoa(opcodeVal) + } + pop := parsedOpcode{opcode: &OpcodeArray[opcodeVal], data: data} + gotStr := pop.print(true) + if gotStr != expectedStr { + t.Errorf("pop.print (opcode %x): Unexpected disasm "+ + "string - got %v, want %v", opcodeVal, gotStr, + expectedStr, + ) + continue + } + } + // Now, replace the relevant fields and test the full disassembly. + expectedStrings[0x00] = "OP_0" + expectedStrings[0x4f] = "OP_1NEGATE" + for opcodeVal, expectedStr := range expectedStrings { + var data []byte + switch { + // OP_DATA_1 through OP_DATA_65 display the opcode followed by the pushed data. + case opcodeVal >= 0x01 && opcodeVal < 0x4c: + data = bytes.Repeat(oneBytes, opcodeVal) + expectedStr = fmt.Sprintf("OP_DATA_%d 0x%s", opcodeVal, + strings.Repeat(oneStr, opcodeVal), + ) + // OP_PUSHDATA1. + case opcodeVal == 0x4c: + data = bytes.Repeat(oneBytes, 1) + expectedStr = fmt.Sprintf("OP_PUSHDATA1 0x%02x 0x%s", + len(data), strings.Repeat(oneStr, 1), + ) + // OP_PUSHDATA2. + case opcodeVal == 0x4d: + data = bytes.Repeat(oneBytes, 2) + expectedStr = fmt.Sprintf("OP_PUSHDATA2 0x%04x 0x%s", + len(data), strings.Repeat(oneStr, 2), + ) + // OP_PUSHDATA4. + case opcodeVal == 0x4e: + data = bytes.Repeat(oneBytes, 3) + expectedStr = fmt.Sprintf("OP_PUSHDATA4 0x%08x 0x%s", + len(data), strings.Repeat(oneStr, 3), + ) + // OP_1 through OP_16. + case opcodeVal >= 0x51 && opcodeVal <= 0x60: + val := byte(opcodeVal - (0x51 - 1)) + data = []byte{val} + expectedStr = "OP_" + strconv.Itoa(int(val)) + // OP_NOP1 through OP_NOP10. + case opcodeVal >= 0xb0 && opcodeVal <= 0xb9: + switch opcodeVal { + case 0xb1: + // OP_NOP2 is an alias of OP_CHECKLOCKTIMEVERIFY + expectedStr = "OP_CHECKLOCKTIMEVERIFY" + case 0xb2: + // OP_NOP3 is an alias of OP_CHECKSEQUENCEVERIFY + expectedStr = "OP_CHECKSEQUENCEVERIFY" + default: + val := byte(opcodeVal - (0xb0 - 1)) + expectedStr = "OP_NOP" + strconv.Itoa(int(val)) + } + // OP_UNKNOWN#. + case opcodeVal >= 0xba && opcodeVal <= 0xf9 || opcodeVal == 0xfc: + expectedStr = "OP_UNKNOWN" + strconv.Itoa(opcodeVal) + } + pop := parsedOpcode{opcode: &OpcodeArray[opcodeVal], data: data} + gotStr := pop.print(false) + if gotStr != expectedStr { + t.Errorf("pop.print (opcode %x): Unexpected disasm "+ + "string - got %v, want %v", opcodeVal, gotStr, + expectedStr, + ) + continue + } + } +} diff --git a/pkg/txscript/reference_test.go b/pkg/txscript/reference_test.go new file mode 100644 index 0000000..e7df1cb --- /dev/null +++ b/pkg/txscript/reference_test.go @@ -0,0 +1,864 @@ +package txscript + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/p9c/p9/pkg/amt" + "io/ioutil" + "strconv" + "strings" + "testing" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/wire" +) + +// scriptTestName returns a descriptive test name for the given reference script test data. +func scriptTestName(test []interface{}) (string, error) { + // Account for any optional leading witness data. + var witnessOffset int + if _, ok := test[0].([]interface{}); ok { + witnessOffset++ + } + // In addition to the optional leading witness data, the test must consist of at + // least a signature script, public key script, flags, and expected error. + // Finally, it may optionally contain a comment. + if len(test) < witnessOffset+4 || len(test) > witnessOffset+5 { + return "", fmt.Errorf("invalid test length %d", len(test)) + } + // Use the comment for the test name if one is specified, otherwise, construct the name based on the signature + // script, public key script, and flags. + var name string + if len(test) == witnessOffset+5 { + name = fmt.Sprintf("test (%s)", test[witnessOffset+4]) + } else { + name = fmt.Sprintf( + "test ([%s, %s, %s])", test[witnessOffset], + test[witnessOffset+1], test[witnessOffset+2], + ) + } + return name, nil +} + +// parse hex string into a []byte. +func parseHex(tok string) ([]byte, error) { + if !strings.HasPrefix(tok, "0x") { + return nil, errors.New("not a hex number") + } + return hex.DecodeString(tok[2:]) +} + +// parseWitnessStack parses a json array of witness items encoded as hex into a +// slice of witness elements. +func parseWitnessStack(elements []interface{}) ([][]byte, error) { + witness := make([][]byte, len(elements)) + for i, e := range elements { + witElement, e := hex.DecodeString(e.(string)) + if e != nil { + return nil, e + } + witness[i] = witElement + } + return witness, nil +} + +// shortFormOps holds a map of opcode names to values for use in short form parsing. It is declared here so it only +// needs to be created once. +var shortFormOps map[string]byte + +// parseShortForm parses a string as as used in the Bitcoin Core reference tests into the script it came from. +// +// The format used for these tests is pretty simple if ad-hoc: +// +// - Opcodes other than the push opcodes and unknown are present as either OP_NAME or just NAME +// - Plain numbers are made into push operations +// - Numbers beginning with 0x are inserted into the []byte as-is (so 0x14 is OP_DATA_20) +// - Single quoted strings are pushed as data +// - Anything else is an error +func parseShortForm(script string) ([]byte, error) { + // Only create the short form opcode map once. + if shortFormOps == nil { + ops := make(map[string]byte) + for opcodeName, opcodeValue := range OpcodeByName { + if strings.Contains(opcodeName, "OP_UNKNOWN") { + continue + } + ops[opcodeName] = opcodeValue + // The opcodes named OP_# can't have the OP_ prefix stripped or they would conflict with the plain numbers. + // Also, since OP_FALSE and OP_TRUE are aliases for the OP_0, and OP_1, respectively, they have the same + // value, so detect those by name and allow them. + if (opcodeName == "OP_FALSE" || opcodeName == "OP_TRUE") || + (opcodeValue != OP_0 && (opcodeValue < OP_1 || + opcodeValue > OP_16)) { + ops[strings.TrimPrefix(opcodeName, "OP_")] = opcodeValue + } + } + shortFormOps = ops + } + // Split only does one separator so convert all \n and tab into space. + script = strings.Replace(script, "\n", " ", -1) + script = strings.Replace(script, "\t", " ", -1) + tokens := strings.Split(script, " ") + builder := NewScriptBuilder() + for _, tok := range tokens { + if len(tok) == 0 { + continue + } + // if parses as a plain number + if num, e := strconv.ParseInt(tok, 10, 64); e == nil { + builder.AddInt64(num) + continue + } else if bts, e := parseHex(tok); e == nil { + // Concatenate the bytes manually since the test code intentionally creates scripts that are too large and + // would cause the builder to error otherwise. + if builder.err == nil { + builder.script = append(builder.script, bts...) + } + } else if len(tok) >= 2 && + tok[0] == '\'' && tok[len(tok)-1] == '\'' { + builder.AddFullData([]byte(tok[1 : len(tok)-1])) + } else if opcode, ok := shortFormOps[tok]; ok { + builder.AddOp(opcode) + } else { + return nil, fmt.Errorf("bad token %q", tok) + } + } + return builder.Script() +} + +// parseScriptFlags parses the provided flags string from the format used in the reference tests into ScriptFlags +// suitable for use in the script engine. +func parseScriptFlags(flagStr string) (ScriptFlags, error) { + var flags ScriptFlags + sFlags := strings.Split(flagStr, ",") + for _, flag := range sFlags { + switch flag { + case "": + // Nothing. + case "CHECKLOCKTIMEVERIFY": + flags |= ScriptVerifyCheckLockTimeVerify + case "CHECKSEQUENCEVERIFY": + flags |= ScriptVerifyCheckSequenceVerify + case "CLEANSTACK": + flags |= ScriptVerifyCleanStack + case "DERSIG": + flags |= ScriptVerifyDERSignatures + case "DISCOURAGE_UPGRADABLE_NOPS": + flags |= ScriptDiscourageUpgradableNops + case "LOW_S": + flags |= ScriptVerifyLowS + case "MINIMALDATA": + flags |= ScriptVerifyMinimalData + case "NONE": + // Nothing. + case "NULLDUMMY": + flags |= ScriptStrictMultiSig + case "NULLFAIL": + flags |= ScriptVerifyNullFail + case "P2SH": + flags |= ScriptBip16 + case "SIGPUSHONLY": + flags |= ScriptVerifySigPushOnly + case "STRICTENC": + flags |= ScriptVerifyStrictEncoding + case "WITNESS": + flags |= ScriptVerifyWitness + case "DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM": + flags |= ScriptVerifyDiscourageUpgradeableWitnessProgram + case "MINIMALIF": + flags |= ScriptVerifyMinimalIf + case "WITNESS_PUBKEYTYPE": + flags |= ScriptVerifyWitnessPubKeyType + default: + return flags, fmt.Errorf("invalid flag: %s", flag) + } + } + return flags, nil +} + +// parseExpectedResult parses the provided expected result string into allowed script error codes. An error is returned +// if the expected result string is not supported. +func parseExpectedResult(expected string) ([]ErrorCode, error) { + switch expected { + case "OK": + return nil, nil + case "UNKNOWN_ERROR": + return []ErrorCode{ErrNumberTooBig, ErrMinimalData}, nil + case "PUBKEYTYPE": + return []ErrorCode{ErrPubKeyType}, nil + case "SIG_DER": + return []ErrorCode{ + ErrSigTooShort, ErrSigTooLong, + ErrSigInvalidSeqID, ErrSigInvalidDataLen, ErrSigMissingSTypeID, + ErrSigMissingSLen, ErrSigInvalidSLen, + ErrSigInvalidRIntID, ErrSigZeroRLen, ErrSigNegativeR, + ErrSigTooMuchRPadding, ErrSigInvalidSIntID, + ErrSigZeroSLen, ErrSigNegativeS, ErrSigTooMuchSPadding, + ErrInvalidSigHashType, + }, nil + case "EVAL_FALSE": + return []ErrorCode{ErrEvalFalse, ErrEmptyStack}, nil + case "EQUALVERIFY": + return []ErrorCode{ErrEqualVerify}, nil + case "NULLFAIL": + return []ErrorCode{ErrNullFail}, nil + case "SIG_HIGH_S": + return []ErrorCode{ErrSigHighS}, nil + case "SIG_HASHTYPE": + return []ErrorCode{ErrInvalidSigHashType}, nil + case "SIG_NULLDUMMY": + return []ErrorCode{ErrSigNullDummy}, nil + case "SIG_PUSHONLY": + return []ErrorCode{ErrNotPushOnly}, nil + case "CLEANSTACK": + return []ErrorCode{ErrCleanStack}, nil + case "BAD_OPCODE": + return []ErrorCode{ErrReservedOpcode, ErrMalformedPush}, nil + case "UNBALANCED_CONDITIONAL": + return []ErrorCode{ + ErrUnbalancedConditional, + ErrInvalidStackOperation, + }, nil + case "OP_RETURN": + return []ErrorCode{ErrEarlyReturn}, nil + case "VERIFY": + return []ErrorCode{ErrVerify}, nil + case "INVALID_STACK_OPERATION", "INVALID_ALTSTACK_OPERATION": + return []ErrorCode{ErrInvalidStackOperation}, nil + case "DISABLED_OPCODE": + return []ErrorCode{ErrDisabledOpcode}, nil + case "DISCOURAGE_UPGRADABLE_NOPS": + return []ErrorCode{ErrDiscourageUpgradableNOPs}, nil + case "PUSH_SIZE": + return []ErrorCode{ErrElementTooBig}, nil + case "OP_COUNT": + return []ErrorCode{ErrTooManyOperations}, nil + case "STACK_SIZE": + return []ErrorCode{ErrStackOverflow}, nil + case "SCRIPT_SIZE": + return []ErrorCode{ErrScriptTooBig}, nil + case "PUBKEY_COUNT": + return []ErrorCode{ErrInvalidPubKeyCount}, nil + case "SIG_COUNT": + return []ErrorCode{ErrInvalidSignatureCount}, nil + case "MINIMALDATA": + return []ErrorCode{ErrMinimalData}, nil + case "NEGATIVE_LOCKTIME": + return []ErrorCode{ErrNegativeLockTime}, nil + case "UNSATISFIED_LOCKTIME": + return []ErrorCode{ErrUnsatisfiedLockTime}, nil + case "MINIMALIF": + return []ErrorCode{ErrMinimalIf}, nil + case "DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM": + return []ErrorCode{ErrDiscourageUpgradableWitnessProgram}, nil + case "WITNESS_PROGRAM_WRONG_LENGTH": + return []ErrorCode{ErrWitnessProgramWrongLength}, nil + case "WITNESS_PROGRAM_WITNESS_EMPTY": + return []ErrorCode{ErrWitnessProgramEmpty}, nil + case "WITNESS_PROGRAM_MISMATCH": + return []ErrorCode{ErrWitnessProgramMismatch}, nil + case "WITNESS_MALLEATED": + return []ErrorCode{ErrWitnessMalleated}, nil + case "WITNESS_MALLEATED_P2SH": + return []ErrorCode{ErrWitnessMalleatedP2SH}, nil + case "WITNESS_UNEXPECTED": + return []ErrorCode{ErrWitnessUnexpected}, nil + case "WITNESS_PUBKEYTYPE": + return []ErrorCode{ErrWitnessPubKeyType}, nil + } + return nil, fmt.Errorf( + "unrecognized expected result in test data: %v", + expected, + ) +} + +// createSpendTx generates a basic spending transaction given the passed +// signature, witness and public key scripts. +func createSpendingTx( + witness [][]byte, sigScript, pkScript []byte, + outputValue int64, +) *wire.MsgTx { + coinbaseTx := wire.NewMsgTx(wire.TxVersion) + outPoint := wire.NewOutPoint(&chainhash.Hash{}, ^uint32(0)) + txIn := wire.NewTxIn(outPoint, []byte{OP_0, OP_0}, nil) + txOut := wire.NewTxOut(outputValue, pkScript) + coinbaseTx.AddTxIn(txIn) + coinbaseTx.AddTxOut(txOut) + spendingTx := wire.NewMsgTx(wire.TxVersion) + coinbaseTxSha := coinbaseTx.TxHash() + outPoint = wire.NewOutPoint(&coinbaseTxSha, 0) + txIn = wire.NewTxIn(outPoint, sigScript, witness) + txOut = wire.NewTxOut(outputValue, nil) + spendingTx.AddTxIn(txIn) + spendingTx.AddTxOut(txOut) + return spendingTx +} + +// scriptWithInputVal wraps a target pkScript with the value of the output in +// which it is contained. The inputVal is necessary in order to properly +// validate inputs which spend nested, or native witness programs. +type scriptWithInputVal struct { + inputVal int64 + pkScript []byte +} + +// testScripts ensures all of the passed script tests execute with the expected results with or without using a +// signature cache, as specified by the parameter. +func testScripts(t *testing.T, tests [][]interface{}, useSigCache bool) { + // Create a signature cache to use only if requested. + var sigCache *SigCache + if useSigCache { + sigCache = NewSigCache(10) + } + for i, test := range tests { + // "Format is: [[wit..., amount]?, scriptSig, scriptPubKey, + // flags, expected_scripterror, ... comments]" + // Skip single line comments. + if len(test) == 1 { + continue + } + // Construct a name for the test based on the comment and test data. + name, e := scriptTestName(test) + if e != nil { + t.Errorf("TestScripts: invalid test #%d: %v", i, e) + continue + } + var ( + witness wire.TxWitness + inputAmt amt.Amount + ) + // When the first field of the test data is a slice it contains witness data and + // everything else is offset by 1 as a result. + witnessOffset := 0 + if witnessData, ok := test[0].([]interface{}); ok { + witnessOffset++ + // If this is a witness test, then the final element within the slice is the + // input amount, so we ignore all but the last element in order to parse the + // witness stack. + strWitnesses := witnessData[:len(witnessData)-1] + witness, e = parseWitnessStack(strWitnesses) + if e != nil { + t.Errorf("%s: can't parse witness; %v", name, e) + continue + } + inputAmt, e = amt.NewAmount(witnessData[len(witnessData)-1].(float64)) + if e != nil { + t.Errorf( + "%s: can't parse input amt: %v", + name, e, + ) + continue + } + } + // Extract and parse the signature script from the test fields. + scriptSigStr, ok := test[witnessOffset].(string) + if !ok { + t.Errorf("%s: signature script is not a string", name) + continue + } + scriptSig, e := parseShortForm(scriptSigStr) + if e != nil { + t.Errorf( + "%s: can't parse signature script: %v", name, + e, + ) + continue + } + // Extract and parse the public key script from the test fields. + scriptPubKeyStr, ok := test[witnessOffset+1].(string) + if !ok { + t.Errorf("%s: public key script is not a string", name) + continue + } + scriptPubKey, e := parseShortForm(scriptPubKeyStr) + if e != nil { + t.Errorf( + "%s: can't parse public key script: %v", name, + e, + ) + continue + } + // Extract and parse the script flags from the test fields. + flagsStr, ok := test[witnessOffset+2].(string) + if !ok { + t.Errorf("%s: flags field is not a string", name) + continue + } + flags, e := parseScriptFlags(flagsStr) + if e != nil { + t.Errorf("%s: %v", name, e) + continue + } + // Extract and parse the expected result from the test fields. Convert the expected result string into the + // allowed script error codes. This is necessary because txscript is more fine grained with its errors than the + // reference test data, so some of the reference test data errors map to more than one possibility. + resultStr, ok := test[witnessOffset+3].(string) + if !ok { + t.Errorf("%s: result field is not a string", name) + continue + } + allowedErrorCodes, e := parseExpectedResult(resultStr) + if e != nil { + t.Errorf("%s: %v", name, e) + continue + } + // Generate a transaction pair such that one spends from the other and the provided signature and public key + // scripts are used, then create a new engine to execute the scripts. + tx := createSpendingTx( + witness, scriptSig, scriptPubKey, + int64(inputAmt), + ) + vm, e := NewEngine( + scriptPubKey, tx, 0, flags, sigCache, nil, + int64(inputAmt), + ) + if e == nil { + e = vm.Execute() + } + // Ensure there were no errors when the expected result is OK. + if resultStr == "OK" { + if e != nil { + t.Errorf("%s failed to execute: %v", name, e) + } + continue + } + // At this point an error was expected so ensure the result of the execution matches it. + success := false + for _, code := range allowedErrorCodes { + if IsErrorCode(e, code) { + success = true + break + } + } + if !success { + if serr, ok := e.(ScriptError); ok { + t.Errorf( + "%s: want error codes %v, got %v", name, + allowedErrorCodes, serr.ErrorCode, + ) + continue + } + t.Errorf( + "%s: want error codes %v, got e: %v (%T)", + name, allowedErrorCodes, e, e, + ) + continue + } + } +} + +// TestScripts ensures all of the tests in script_tests.json execute with the expected results as defined in the test +// data. +func TestScripts(t *testing.T) { + file, e := ioutil.ReadFile("data/script_tests.json") + if e != nil { + t.Fatalf("TestScripts: %v\n", e) + } + var tests [][]interface{} + e = json.Unmarshal(file, &tests) + if e != nil { + t.Fatalf("TestScripts couldn't Unmarshal: %v", e) + } + // Run all script tests with and without the signature cache. + testScripts(t, tests, true) + testScripts(t, tests, false) +} + +// testVecF64ToUint32 properly handles conversion of float64s read from the JSON test data to unsigned 32-bit integers. +// This is necessary because some of the test data uses -1 as a shortcut to mean max uint32 and direct conversion of a +// negative float to an unsigned int is implementation dependent and therefore doesn't result in the expected value on +// all platforms. This function woks around that limitation by converting to a 32-bit signed integer first and then to a +// 32-bit unsigned integer which results in the expected behavior on all platforms. +func testVecF64ToUint32(f float64) uint32 { + return uint32(int32(f)) +} + +// TestTxInvalidTests ensures all of the tests in tx_invalid.json fail as expected. +func TestTxInvalidTests(t *testing.T) { + file, e := ioutil.ReadFile("data/tx_invalid.json") + if e != nil { + t.Fatalf("TestTxInvalidTests: %v\n", e) + } + var tests [][]interface{} + e = json.Unmarshal(file, &tests) + if e != nil { + t.Fatalf("TestTxInvalidTests couldn't Unmarshal: %v\n", e) + } + // form is either: + // ["this is a comment "] + // or: + // [[[previous hash, previous index, previous scriptPubKey]...,] + // serializedTransaction, verifyFlags] +testloop: + for i, test := range tests { + inputs, ok := test[0].([]interface{}) + if !ok { + continue + } + if len(test) != 3 { + t.Errorf("bad test (bad length) %d: %v", i, test) + continue + } + serializedhex, ok := test[1].(string) + if !ok { + t.Errorf("bad test (arg 2 not string) %d: %v", i, test) + continue + } + serializedTx, e := hex.DecodeString(serializedhex) + if e != nil { + t.Errorf( + "bad test (arg 2 not hex %v) %d: %v", e, i, + test, + ) + continue + } + tx, e := util.NewTxFromBytes(serializedTx) + if e != nil { + t.Errorf( + "bad test (arg 2 not msgtx %v) %d: %v", e, + i, test, + ) + continue + } + verifyFlags, ok := test[2].(string) + if !ok { + t.Errorf("bad test (arg 3 not string) %d: %v", i, test) + continue + } + flags, e := parseScriptFlags(verifyFlags) + if e != nil { + t.Errorf("bad test %d: %v", i, e) + continue + } + prevOuts := make(map[wire.OutPoint]scriptWithInputVal) + for j, iinput := range inputs { + input, ok := iinput.([]interface{}) + if !ok { + t.Errorf( + "bad test (%dth input not array)"+ + "%d: %v", j, i, test, + ) + continue testloop + } + if len(input) < 3 || len(input) > 4 { + t.Errorf( + "bad test (%dth input wrong length)"+ + "%d: %v", j, i, test, + ) + continue testloop + } + previoustx, ok := input[0].(string) + if !ok { + t.Errorf( + "bad test (%dth input hash not string)"+ + "%d: %v", j, i, test, + ) + continue testloop + } + prevhash, e := chainhash.NewHashFromStr(previoustx) + if e != nil { + t.Errorf( + "bad test (%dth input hash not hash %v)"+ + "%d: %v", j, e, i, test, + ) + continue testloop + } + idxf, ok := input[1].(float64) + if !ok { + t.Errorf( + "bad test (%dth input idx not number)"+ + "%d: %v", j, i, test, + ) + continue testloop + } + idx := testVecF64ToUint32(idxf) + oscript, ok := input[2].(string) + if !ok { + t.Errorf( + "bad test (%dth input script not "+ + "string) %d: %v", j, i, test, + ) + continue testloop + } + script, e := parseShortForm(oscript) + if e != nil { + t.Errorf( + "bad test (%dth input script doesn't "+ + "parse %v) %d: %v", j, e, i, test, + ) + continue testloop + } + var inputValue float64 + if len(input) == 4 { + inputValue, ok = input[3].(float64) + if !ok { + t.Errorf( + "bad test (%dth input value not int) "+ + "%d: %v", j, i, test, + ) + continue + } + } + v := scriptWithInputVal{ + inputVal: int64(inputValue), + pkScript: script, + } + prevOuts[*wire.NewOutPoint(prevhash, idx)] = v + } + for k, txin := range tx.MsgTx().TxIn { + prevOut, ok := prevOuts[txin.PreviousOutPoint] + if !ok { + t.Errorf( + "bad test (missing %dth input) %d:%v", + k, i, test, + ) + continue testloop + } + // These are meant to fail, so as soon as the first input fails the transaction has failed. (some of the + // test txns have good inputs, too.. + vm, e := NewEngine( + prevOut.pkScript, tx.MsgTx(), k, + flags, nil, nil, prevOut.inputVal, + ) + if e != nil { + continue testloop + } + e = vm.Execute() + if e != nil { + continue testloop + } + } + t.Errorf( + "test (%d:%v) succeeded when should fail", + i, test, + ) + } +} + +// TestTxValidTests ensures all of the tests in tx_valid.json pass as expected. +func TestTxValidTests(t *testing.T) { + file, e := ioutil.ReadFile("data/tx_valid.json") + if e != nil { + t.Fatalf("TestTxValidTests: %v\n", e) + } + var tests [][]interface{} + e = json.Unmarshal(file, &tests) + if e != nil { + t.Fatalf("TestTxValidTests couldn't Unmarshal: %v\n", e) + } + // form is either: + // ["this is a comment "] + // or: + // [[[previous hash, previous index, previous scriptPubKey, input value]...,] + // serializedTransaction, verifyFlags] +testloop: + for i, test := range tests { + inputs, ok := test[0].([]interface{}) + if !ok { + continue + } + if len(test) != 3 { + t.Errorf("bad test (bad length) %d: %v", i, test) + continue + } + serializedhex, ok := test[1].(string) + if !ok { + t.Errorf("bad test (arg 2 not string) %d: %v", i, test) + continue + } + serializedTx, e := hex.DecodeString(serializedhex) + if e != nil { + t.Errorf( + "bad test (arg 2 not hex %v) %d: %v", e, i, + test, + ) + continue + } + tx, e := util.NewTxFromBytes(serializedTx) + if e != nil { + t.Errorf( + "bad test (arg 2 not msgtx %v) %d: %v", e, + i, test, + ) + continue + } + verifyFlags, ok := test[2].(string) + if !ok { + t.Errorf("bad test (arg 3 not string) %d: %v", i, test) + continue + } + flags, e := parseScriptFlags(verifyFlags) + if e != nil { + t.Errorf("bad test %d: %v", i, e) + continue + } + prevOuts := make(map[wire.OutPoint]scriptWithInputVal) + for j, iinput := range inputs { + input, ok := iinput.([]interface{}) + if !ok { + t.Errorf( + "bad test (%dth input not array)"+ + "%d: %v", j, i, test, + ) + continue + } + if len(input) < 3 || len(input) > 4 { + t.Errorf( + "bad test (%dth input wrong length)"+ + "%d: %v", j, i, test, + ) + continue + } + previoustx, ok := input[0].(string) + if !ok { + t.Errorf( + "bad test (%dth input hash not string)"+ + "%d: %v", j, i, test, + ) + continue + } + prevhash, e := chainhash.NewHashFromStr(previoustx) + if e != nil { + t.Errorf( + "bad test (%dth input hash not hash %v)"+ + "%d: %v", j, e, i, test, + ) + continue + } + idxf, ok := input[1].(float64) + if !ok { + t.Errorf( + "bad test (%dth input idx not number)"+ + "%d: %v", j, i, test, + ) + continue + } + idx := testVecF64ToUint32(idxf) + oscript, ok := input[2].(string) + if !ok { + t.Errorf( + "bad test (%dth input script not "+ + "string) %d: %v", j, i, test, + ) + continue + } + script, e := parseShortForm(oscript) + if e != nil { + t.Errorf( + "bad test (%dth input script doesn't "+ + "parse %v) %d: %v", j, e, i, test, + ) + continue + } + var inputValue float64 + if len(input) == 4 { + inputValue, ok = input[3].(float64) + if !ok { + t.Errorf( + "bad test (%dth input value not int) "+ + "%d: %v", j, i, test, + ) + continue + } + } + v := scriptWithInputVal{ + inputVal: int64(inputValue), + pkScript: script, + } + prevOuts[*wire.NewOutPoint(prevhash, idx)] = v + } + for k, txin := range tx.MsgTx().TxIn { + prevOut, ok := prevOuts[txin.PreviousOutPoint] + if !ok { + t.Errorf( + "bad test (missing %dth input) %d:%v", + k, i, test, + ) + continue testloop + } + vm, e := NewEngine( + prevOut.pkScript, tx.MsgTx(), k, + flags, nil, nil, prevOut.inputVal, + ) + if e != nil { + t.Errorf( + "test (%d:%v:%d) failed to create "+ + "script: %v", i, test, k, e, + ) + continue + } + e = vm.Execute() + if e != nil { + t.Errorf( + "test (%d:%v:%d) failed to execute: "+ + "%v", i, test, k, e, + ) + continue + } + } + } +} + +// TestCalcSignatureHash runs the Bitcoin Core signature hash calculation tests in sighash.json. +// https://github.com/bitcoin/bitcoin/blob/master/src/test/data/sighash.json +func TestCalcSignatureHash(t *testing.T) { + file, e := ioutil.ReadFile("data/sighash.json") + if e != nil { + t.Fatalf("TestCalcSignatureHash: %v\n", e) + } + var tests [][]interface{} + e = json.Unmarshal(file, &tests) + if e != nil { + t.Fatalf( + "TestCalcSignatureHash couldn't Unmarshal: %v\n", + e, + ) + } + for i, test := range tests { + if i == 0 { + // Skip first line -- contains comments only. + continue + } + if len(test) != 5 { + t.Fatalf( + "TestCalcSignatureHash: Test #%d has "+ + "wrong length.", i, + ) + } + var tx wire.MsgTx + rawTx, _ := hex.DecodeString(test[0].(string)) + e := tx.Deserialize(bytes.NewReader(rawTx)) + if e != nil { + t.Errorf( + "TestCalcSignatureHash failed test #%d: "+ + "Failed to parse transaction: %v", i, e, + ) + continue + } + subScript, _ := hex.DecodeString(test[1].(string)) + parsedScript, e := parseScript(subScript) + if e != nil { + t.Errorf( + "TestCalcSignatureHash failed test #%d: "+ + "Failed to parse sub-script: %v", i, e, + ) + continue + } + hashType := SigHashType(testVecF64ToUint32(test[3].(float64))) + hash := calcSignatureHash( + parsedScript, hashType, &tx, + int(test[2].(float64)), + ) + expectedHash, _ := chainhash.NewHashFromStr(test[4].(string)) + if !bytes.Equal(hash, expectedHash[:]) { + t.Errorf( + "TestCalcSignatureHash failed test #%d: "+ + "Signature hash mismatch.", i, + ) + } + } +} diff --git a/pkg/txscript/script.go b/pkg/txscript/script.go new file mode 100644 index 0000000..950e96f --- /dev/null +++ b/pkg/txscript/script.go @@ -0,0 +1,771 @@ +package txscript + +import ( + "bytes" + "encoding/binary" + "fmt" + "time" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/wire" +) + +// Bip16Activation is the timestamp where BIP0016 is valid to use in the blockchain. To be used to determine if BIP0016 +// should be called for or not. This timestamp corresponds to Sun Apr 1 00:00:00 UTC 2012. +var Bip16Activation = time.Unix(1333238400, 0) + +type // SigHashType represents hash type bits at the end of a signature. + SigHashType uint32 + +const ( // Hash type bits from the end of a signature. + SigHashOld SigHashType = 0x0 + SigHashAll SigHashType = 0x1 + SigHashNone SigHashType = 0x2 + SigHashSingle SigHashType = 0x3 + SigHashAnyOneCanPay SigHashType = 0x80 + // sigHashMask defines the number of bits of the hash type which is used to identify which outputs are signed. + sigHashMask = 0x1f + + // These are the constants specified for maximums in individual scripts. + + MaxOpsPerScript = 201 // Max number of non-push operations. + MaxPubKeysPerMultiSig = 20 // Multisig can't have more sigs than this. + MaxScriptElementSize = 520 // Max bytes pushable to the stack. +) + +// isSmallInt returns whether or not the opcode is considered a small integer, which is an OP_0, or OP_1 through OP_16. +func isSmallInt(op *opcode) bool { + if op.value == OP_0 || (op.value >= OP_1 && op.value <= OP_16) { + return true + } + return false +} + +// isScriptHash returns true if the script passed is a pay-to-script -hash transaction, false otherwise. +func isScriptHash(pops []parsedOpcode) bool { + return len(pops) == 3 && + pops[0].opcode.value == OP_HASH160 && + pops[1].opcode.value == OP_DATA_20 && + pops[2].opcode.value == OP_EQUAL +} + +// IsPayToScriptHash returns true if the script is in the standard pay -to-script-hash (P2SH) format, false otherwise. +func IsPayToScriptHash(script []byte) bool { + pops, e := parseScript(script) + if e != nil { + return false + } + return isScriptHash(pops) +} + +// isWitnessScriptHash returns true if the passed script is a pay-to +// -witness-script-hash transaction, false otherwise. +func isWitnessScriptHash(pops []parsedOpcode) bool { + return len(pops) == 2 && + pops[0].opcode.value == OP_0 && + pops[1].opcode.value == OP_DATA_32 +} + +// IsPayToWitnessScriptHash returns true if the is in the standard pay +// -to-witness-script-hash (P2WSH) format, false otherwise. +func IsPayToWitnessScriptHash(script []byte) bool { + pops, e := parseScript(script) + if e != nil { + return false + } + return isWitnessScriptHash(pops) +} + +// IsPayToWitnessPubKeyHash returns true if the is in the standard pay +// -to-witness-pubkey-hash (P2WKH) format, false otherwise. +func IsPayToWitnessPubKeyHash(script []byte) bool { + pops, e := parseScript(script) + if e != nil { + return false + } + return isWitnessPubKeyHash(pops) +} + +// isWitnessPubKeyHash returns true if the passed script is a pay-to +// -witness-pubkey-hash, and false otherwise. +func isWitnessPubKeyHash(pops []parsedOpcode) bool { + return len(pops) == 2 && + pops[0].opcode.value == OP_0 && + pops[1].opcode.value == OP_DATA_20 +} + +// IsWitnessProgram returns true if the passed script is a valid witness program +// which is encoded according to the passed witness program version. A witness +// program must be a small integer (from 0-16), followed by 2-40 bytes of pushed +// data. +func IsWitnessProgram(script []byte) bool { + // The length of the script must be between 4 and 42 bytes. The smallest program + // is the witness version, followed by a data push of 2 bytes. The largest + // allowed witness program has a data push of 40-bytes. + if len(script) < 4 || len(script) > 42 { + return false + } + pops, e := parseScript(script) + if e != nil { + return false + } + return isWitnessProgram(pops) +} + +// isWitnessProgram returns true if the passed script is a witness program, and +// false otherwise. A witness program MUST adhere to the following constraints: +// there must be exactly two pops (program version and the program itself), the +// first opcode MUST be a small integer (0-16), the push data MUST be canonical, +// and finally the size of the push data must be between 2 and 40 bytes. +func isWitnessProgram(pops []parsedOpcode) bool { + return len(pops) == 2 && + isSmallInt(pops[0].opcode) && + canonicalPush(pops[1]) && + (len(pops[1].data) >= 2 && len(pops[1].data) <= 40) +} + +// // ExtractWitnessProgramInfo attempts to extract the witness program version, as +// // well as the witness program itself from the passed script. +// func ExtractWitnessProgramInfo(script []byte) (int, []byte, error) { +// pops, e := parseScript(script) +// if e != nil { +// return 0, nil, e +// } +// // If at this point, the scripts doesn't resemble a witness program, then we'll +// // exit early as there isn't a valid version or program to extract. +// if !isWitnessProgram(pops) { +// return 0, nil, fmt.Errorf( +// "script is not a witness program, " + +// "unable to extract version or witness program", +// ) +// } +// witnessVersion := asSmallInt(pops[0].opcode) +// witnessProgram := pops[1].data +// return witnessVersion, witnessProgram, nil +// } + +func isPushOnly(pops []parsedOpcode) bool { + // isPushOnly returns true if the script only pushes data, false otherwise. NOTE: This function does NOT verify + // opcodes directly since it is internal and is only called with parsed opcodes for scripts that did not have any + // parse errors. Thus, consensus is properly maintained. + for _, pop := range pops { + // All opcodes up to OP_16 are data push instructions. NOTE: This does consider OP_RESERVED to be a data push + // instruction but execution of OP_RESERVED will fail anyways and matches the behavior required by consensus. + if pop.opcode.value > OP_16 { + return false + } + } + return true +} + +func IsPushOnlyScript(script []byte) bool { + // IsPushOnlyScript returns whether or not the passed script only pushes data. False will be returned when the + // script does not parse. + pops, e := parseScript(script) + if e != nil { + return false + } + return isPushOnly(pops) +} + +// ParseScriptTemplate is the same as parseScript but allows the passing of the template list for testing purposes. When +// there are parse errors, it returns the list of parsed opcodes up to the point of failure along with the error. +func ParseScriptTemplate(script []byte, opcodes *[256]opcode) ([]parsedOpcode, error) { + retScript := make([]parsedOpcode, 0, len(script)) + for i := 0; i < len(script); { + instr := script[i] + op := &opcodes[instr] + pop := parsedOpcode{opcode: op} + // Parse data out of instruction. + switch { + // No additional data. Note that some of the opcodes, + // notably OP_1NEGATE, OP_0, + // and OP_[1-16] represent the data themselves. + case op.length == 1: + i++ + // Data pushes of specific lengths -- OP_DATA_[1-75]. + case op.length > 1: + if len(script[i:]) < op.length { + str := fmt.Sprintf( + "opcode %s requires %d "+ + "bytes, but script only has %d remaining", + op.name, op.length, len(script[i:]), + ) + return retScript, scriptError( + ErrMalformedPush, + str, + ) + } + // Slice out the data. + pop.data = script[i+1 : i+op.length] + i += op.length + // Data pushes with parsed lengths -- OP_PUSHDATAP{1,2,4}. + case op.length < 0: + var l uint + off := i + 1 + if len(script[off:]) < -op.length { + str := fmt.Sprintf( + "opcode %s requires %d "+ + "bytes, but script only has %d remaining", + op.name, -op.length, len(script[off:]), + ) + return retScript, scriptError( + ErrMalformedPush, + str, + ) + } + // Next -length bytes are little endian length of data. + switch op.length { + case -1: + l = uint(script[off]) + case -2: + l = (uint(script[off+1]) << 8) | + uint(script[off]) + case -4: + l = (uint(script[off+3]) << 24) | + (uint(script[off+2]) << 16) | + (uint(script[off+1]) << 8) | + uint(script[off]) + default: + str := fmt.Sprintf( + "invalid opcode length %d", + op.length, + ) + return retScript, scriptError( + ErrMalformedPush, + str, + ) + } + // Move offset to beginning of the data. + off += -op.length + // Disallow entries that do not fit script or were sign extended. + if int(l) > len(script[off:]) || int(l) < 0 { + str := fmt.Sprintf( + "opcode %s pushes %d bytes, "+ + "but script only has %d remaining", + op.name, int(l), len(script[off:]), + ) + return retScript, scriptError( + ErrMalformedPush, + str, + ) + } + pop.data = script[off : off+int(l)] + i += 1 - op.length + int(l) + } + retScript = append(retScript, pop) + } + return retScript, nil +} + +// parseScript preparses the script in bytes into a list of parsedOpcodes while applying a number of sanity checks. +func parseScript(script []byte) ([]parsedOpcode, error) { + return ParseScriptTemplate(script, &OpcodeArray) +} + +// unparseScript reversed the action of parseScript and returns the parsedOpcodes as a list of bytes +func unparseScript(pops []parsedOpcode) ([]byte, error) { + script := make([]byte, 0, len(pops)) + for _, pop := range pops { + b, e := pop.bytes() + if e != nil { + return nil, e + } + script = append(script, b...) + } + return script, nil +} + +// DisasmString formats a disassembled script for one line printing. When the script fails to parse, the returned string +// will contain the disassembled script up to the point the failure occurred along with the string '[error]' appended. +// In addition, the reason the script failed to parse is returned if the caller wants more information about the +// failure. +func DisasmString(buf []byte) (string, error) { + var disbuf bytes.Buffer + opcodes, e := parseScript(buf) + for _, pop := range opcodes { + disbuf.WriteString(pop.print(true)) + disbuf.WriteByte(' ') + } + if disbuf.Len() > 0 { + disbuf.Truncate(disbuf.Len() - 1) + } + if e != nil { + disbuf.WriteString("[error]") + } + return disbuf.String(), e +} + +// removeOpcode will remove any opcode matching ``opcode'' from the +// opcode stream in pkscript +func removeOpcode(pkscript []parsedOpcode, opcode byte) []parsedOpcode { + retScript := make([]parsedOpcode, 0, len(pkscript)) + for _, pop := range pkscript { + if pop.opcode.value != opcode { + retScript = append(retScript, pop) + } + } + return retScript +} + +// canonicalPush returns true if the object is either not a push instruction or the push instruction contained wherein +// is matches the canonical form or using the smallest instruction to do the job. False otherwise. +func canonicalPush(pop parsedOpcode) bool { + opcode := pop.opcode.value + data := pop.data + dataLen := len(pop.data) + if opcode > OP_16 { + return true + } + if opcode < OP_PUSHDATA1 && opcode > OP_0 && (dataLen == 1 && data[0] <= 16) { + return false + } + if opcode == OP_PUSHDATA1 && dataLen < OP_PUSHDATA1 { + return false + } + if opcode == OP_PUSHDATA2 && dataLen <= 0xff { + return false + } + if opcode == OP_PUSHDATA4 && dataLen <= 0xffff { + return false + } + return true +} + +// removeOpcodeByData will return the script minus any opcodes that would push the passed data to the stack. +func removeOpcodeByData(pkscript []parsedOpcode, data []byte) []parsedOpcode { + retScript := make([]parsedOpcode, 0, len(pkscript)) + for _, pop := range pkscript { + if !canonicalPush(pop) || !bytes.Contains(pop.data, data) { + retScript = append(retScript, pop) + } + } + return retScript +} + +// calcHashPrevOuts calculates a single hash of all the previous outputs (txid:index) referenced within the passed +// transaction. This calculated hash can be re-used when validating all inputs spending segwit outputs, with a signature +// hash type of SigHashAll. This allows validation to re-use previous hashing computation, reducing the complexity of +// validating SigHashAll inputs from O(N^2) to O(N). +func calcHashPrevOuts(tx *wire.MsgTx) chainhash.Hash { + var b bytes.Buffer + for _, in := range tx.TxIn { + // First write out the 32-byte transaction ID one of whose outputs are being referenced by this input. + b.Write(in.PreviousOutPoint.Hash[:]) + // Next, we'll encode the index of the referenced output as a little endian integer. + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], in.PreviousOutPoint.Index) + b.Write(buf[:]) + } + return chainhash.DoubleHashH(b.Bytes()) +} + +// calcHashSequence computes an aggregated hash of each of the sequence numbers within the inputs of the passed +// transaction. This single hash can be re-used when validating all inputs spending segwit outputs, which include +// signatures using the SigHashAll sighash type. This allows validation to re-use previous hashing computation, reducing +// the complexity of validating SigHashAll inputs from O(N^2) to O(N). +func calcHashSequence(tx *wire.MsgTx) chainhash.Hash { + var b bytes.Buffer + for _, in := range tx.TxIn { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], in.Sequence) + b.Write(buf[:]) + } + return chainhash.DoubleHashH(b.Bytes()) +} + +// calcHashOutputs computes a hash digest of all outputs created by the +// transaction encoded using the wire format. This single hash can be re-used +// when validating all inputs spending witness programs, which include +// signatures using the SigHashAll sighash type. This allows computation to be +// cached, reducing the total hashing complexity from O(N^2) to O(N). +func calcHashOutputs(tx *wire.MsgTx) chainhash.Hash { + var b bytes.Buffer + for _, out := range tx.TxOut { + e := wire.WriteTxOut(&b, 0, 0, out) + if e != nil { + } + } + return chainhash.DoubleHashH(b.Bytes()) +} + +// calcWitnessSignatureHash computes the sighash digest of a transaction's +// segwit input using the new optimized digest calculation algorithm defined in +// BIP0143: https://github.// com/bitcoin/bips/blob/master/bip-0143.mediawiki +// This function makes use of pre-calculated sighash fragments stored within the +// passed HashCache to eliminate duplicate hashing computations when calculating +// the final digest, reducing the complexity from O(N^2) to O(N). Additionally, +// signatures now cover the input value of the referenced unspent output. This +// allows offline or hardware wallets to compute the exact amount being spent in +// addition to the final transaction fee. In the case the wallet if fed an +// invalid input amount, the real sighash will differ causing the produced +// signature to be invalid. +func calcWitnessSignatureHash( + subScript []parsedOpcode, + sigHashes *TxSigHashes, + hashType SigHashType, + tx *wire.MsgTx, + idx int, + amt int64, +) ([]byte, error) { + // As a sanity check, + // ensure the passed input index for the transaction is valid. + if idx > len(tx.TxIn)-1 { + return nil, fmt.Errorf("idx %d but %d txins", idx, len(tx.TxIn)) + } + // We'll utilize this buffer throughout to incrementally calculate the signature hash for this transaction. + var sigHash bytes.Buffer + // First write out, then encode the transaction's version number. + var bVersion [4]byte + binary.LittleEndian.PutUint32(bVersion[:], uint32(tx.Version)) + sigHash.Write(bVersion[:]) + // Next write out the possibly pre-calculated hashes for the sequence numbers of all inputs, and the hashes of the + // previous outs for all outputs. + var zeroHash chainhash.Hash + // If anyone can pay isn't active, then we can use the cached hashPrevOuts, otherwise we just write zeroes for the + // prev outs. + if hashType&SigHashAnyOneCanPay == 0 { + sigHash.Write(sigHashes.HashPrevOuts[:]) + } else { + sigHash.Write(zeroHash[:]) + } + // If the sighash isn't anyone can pay, single, or none, the use the cached hash sequences, otherwise write all + // zeroes for the hashSequence. + if hashType&SigHashAnyOneCanPay == 0 && + hashType&sigHashMask != SigHashSingle && + hashType&sigHashMask != SigHashNone { + sigHash.Write(sigHashes.HashSequence[:]) + } else { + sigHash.Write(zeroHash[:]) + } + txIn := tx.TxIn[idx] + // Next, write the outpoint being spent. + sigHash.Write(txIn.PreviousOutPoint.Hash[:]) + var bIndex [4]byte + binary.LittleEndian.PutUint32(bIndex[:], txIn.PreviousOutPoint.Index) + sigHash.Write(bIndex[:]) + if isWitnessPubKeyHash(subScript) { + // The script code for a p2wkh is a length prefix varint for the next 25 bytes, followed by a re-creation of the + // original p2pkh pk script. + sigHash.Write([]byte{0x19}) + sigHash.Write([]byte{OP_DUP}) + sigHash.Write([]byte{OP_HASH160}) + sigHash.Write([]byte{OP_DATA_20}) + sigHash.Write(subScript[1].data) + sigHash.Write([]byte{OP_EQUALVERIFY}) + sigHash.Write([]byte{OP_CHECKSIG}) + } else { + // For p2wsh outputs, and future outputs, the script code is the original script, with all code separators + // removed, serialized with a var int length prefix. + rawScript, _ := unparseScript(subScript) + e := wire.WriteVarBytes(&sigHash, 0, rawScript) + if e != nil { + } + } + // Next, add the input amount, and sequence number of the input being signed. + var bAmount [8]byte + binary.LittleEndian.PutUint64(bAmount[:], uint64(amt)) + sigHash.Write(bAmount[:]) + var bSequence [4]byte + binary.LittleEndian.PutUint32(bSequence[:], txIn.Sequence) + sigHash.Write(bSequence[:]) + // If the current signature mode isn't single, or none, then we can re-use the pre-generated hashoutputs sighash + // fragment. Otherwise, we'll serialize and add only the target output index to the signature pre-image. + if hashType&SigHashSingle != SigHashSingle && + hashType&SigHashNone != SigHashNone { + sigHash.Write(sigHashes.HashOutputs[:]) + } else if hashType&sigHashMask == SigHashSingle && idx < len(tx.TxOut) { + var b bytes.Buffer + e := wire.WriteTxOut(&b, 0, 0, tx.TxOut[idx]) + if e != nil { + } + sigHash.Write(chainhash.DoubleHashB(b.Bytes())) + } else { + sigHash.Write(zeroHash[:]) + } + // Finally, write out the transaction's locktime, and the sig hash type. + var bLockTime [4]byte + binary.LittleEndian.PutUint32(bLockTime[:], tx.LockTime) + sigHash.Write(bLockTime[:]) + var bHashType [4]byte + binary.LittleEndian.PutUint32(bHashType[:], uint32(hashType)) + sigHash.Write(bHashType[:]) + return chainhash.DoubleHashB(sigHash.Bytes()), nil +} + +// CalcWitnessSigHash computes the sighash digest for the specified input of the +// target transaction observing the desired sig hash type. +func CalcWitnessSigHash( + script []byte, + sigHashes *TxSigHashes, + hType SigHashType, + tx *wire.MsgTx, + idx int, + amt int64, +) ([]byte, error) { + parsedScript, e := parseScript(script) + if e != nil { + return nil, fmt.Errorf("cannot parse output script: %v", e) + } + return calcWitnessSignatureHash( + parsedScript, sigHashes, hType, tx, idx, + amt, + ) +} + +// shallowCopyTx creates a shallow copy of the transaction for use when calculating the signature hash. It is used over +// the Copy method on the transaction itself since that is a deep copy and therefore does more work and allocates much +// more space than needed. +func shallowCopyTx(tx *wire.MsgTx) wire.MsgTx { + // As an additional memory optimization, use contiguous backing arrays for the copied inputs and outputs and point + // the final slice of pointers into the contiguous arrays. This avoids a lot of small allocations. + txCopy := wire.MsgTx{ + Version: tx.Version, + TxIn: make([]*wire.TxIn, len(tx.TxIn)), + TxOut: make([]*wire.TxOut, len(tx.TxOut)), + LockTime: tx.LockTime, + } + txIns := make([]wire.TxIn, len(tx.TxIn)) + for i, oldTxIn := range tx.TxIn { + txIns[i] = *oldTxIn + txCopy.TxIn[i] = &txIns[i] + } + txOuts := make([]wire.TxOut, len(tx.TxOut)) + for i, oldTxOut := range tx.TxOut { + txOuts[i] = *oldTxOut + txCopy.TxOut[i] = &txOuts[i] + } + return txCopy +} + +// CalcSignatureHash will given a script and hash type for the current script engine instance calculate the signature +// hash to be used for signing and verification. +func CalcSignatureHash(script []byte, hashType SigHashType, tx *wire.MsgTx, idx int) ([]byte, error) { + parsedScript, e := parseScript(script) + if e != nil { + return nil, fmt.Errorf("cannot parse output script: %v", e) + } + return calcSignatureHash(parsedScript, hashType, tx, idx), nil +} + +// calcSignatureHash will given a script and hash type for the current script engine instance calculate the signature +// hash to be used for signing and verification. +func calcSignatureHash(script []parsedOpcode, hashType SigHashType, tx *wire.MsgTx, idx int) []byte { + // The SigHashSingle signature type signs only the corresponding input and output (the output with the same index + // number as the input). Since transactions can have more inputs than outputs, this means it is improper to use + // SigHashSingle on input indices that don't have a corresponding output. A bug in the original Satoshi client + // implementation means specifying an index that is out of range results in a signature hash of 1 ( as a uint256 + // little endian). The original intent appeared to be to indicate failure, but unfortunately, it was never checked + // and thus is treated as the actual signature hash. This buggy behavior is now part of the consensus and a hard + // fork would be required to fix it. Due to this, care must be taken by software that creates transactions which + // make use of SigHashSingle because it can lead to an extremely dangerous situation where the invalid inputs will + // end up signing a hash of 1. This in turn presents an opportunity for attackers to cleverly construct transactions + // which can steal those coins provided they can reuse signatures. + if hashType&sigHashMask == SigHashSingle && idx >= len(tx.TxOut) { + var hash chainhash.Hash + hash[0] = 0x01 + return hash[:] + } + // Remove all instances of OP_CODESEPARATOR from the script. + script = removeOpcode(script, OP_CODESEPARATOR) + // Make a shallow copy of the transaction, zeroing out the script for all inputs that are not currently being + // processed. + txCopy := shallowCopyTx(tx) + for i := range txCopy.TxIn { + if i == idx { + // UnparseScript cannot fail here because removeOpcode above only returns a valid script. + sigScript, _ := unparseScript(script) + txCopy.TxIn[idx].SignatureScript = sigScript + } else { + txCopy.TxIn[i].SignatureScript = nil + } + } + switch hashType & sigHashMask { + case SigHashNone: + txCopy.TxOut = txCopy.TxOut[0:0] // Empty slice. + for i := range txCopy.TxIn { + if i != idx { + txCopy.TxIn[i].Sequence = 0 + } + } + case SigHashSingle: + // Resize output array to up to and including requested index. + txCopy.TxOut = txCopy.TxOut[:idx+1] + // All but current output get zeroed out. + for i := 0; i < idx; i++ { + txCopy.TxOut[i].Value = -1 + txCopy.TxOut[i].PkScript = nil + } + // Sequence on all other inputs is 0, too. + for i := range txCopy.TxIn { + if i != idx { + txCopy.TxIn[i].Sequence = 0 + } + } + default: + // Consensus treats undefined hashtypes like normal SigHashAll for purposes of hash generation. + fallthrough + case SigHashOld: + fallthrough + case SigHashAll: + // Nothing special here. + } + if hashType&SigHashAnyOneCanPay != 0 { + txCopy.TxIn = txCopy.TxIn[idx : idx+1] + } + // The final hash is the double sha256 of both the serialized modified transaction and the hash type ( encoded as a + // 4-byte little-endian value) appended. + wbuf := bytes.NewBuffer(make([]byte, 0, txCopy.SerializeSizeStripped()+4)) + e := txCopy.SerializeNoWitness(wbuf) + if e != nil { + } + e = binary.Write(wbuf, binary.LittleEndian, hashType) + if e != nil { + } + return chainhash.DoubleHashB(wbuf.Bytes()) +} + +// asSmallInt returns the passed opcode which must be true according to isSmallInt(), as an integer. +func asSmallInt(op *opcode) int { + if op.value == OP_0 { + return 0 + } + return int(op.value - (OP_1 - 1)) +} + +// getSigOpCount is the implementation function for counting the number of signature operations in the script provided +// by pops. If precise mode is requested then we attempt to count the number of operations for a multisig op. Otherwise +// we use the maximum. +func getSigOpCount(pops []parsedOpcode, precise bool) int { + nSigs := 0 + for i, pop := range pops { + switch pop.opcode.value { + case OP_CHECKSIG: + fallthrough + case OP_CHECKSIGVERIFY: + nSigs++ + case OP_CHECKMULTISIG: + fallthrough + case OP_CHECKMULTISIGVERIFY: + // If we are being precise then look for familiar patterns for multisig, for now all we recognize is OP_1 - + // OP_16 to signify the number of pubkeys. Otherwise, we use the max of 20. + if precise && i > 0 && + pops[i-1].opcode.value >= OP_1 && + pops[i-1].opcode.value <= OP_16 { + nSigs += asSmallInt(pops[i-1].opcode) + } else { + nSigs += MaxPubKeysPerMultiSig + } + default: + // Not a sigop. + } + } + return nSigs +} + +// GetSigOpCount provides a quick count of the number of signature operations in a script. a CHECKSIG operations counts +// for 1, and a CHECK_MULTISIG for 20. If the script fails to parse, then the count up to the point of failure is +// returned. +func GetSigOpCount(script []byte) int { + // Don't check error since parseScript returns the parsed-up-to-error list of pops. + pops, _ := parseScript(script) + return getSigOpCount(pops, false) +} + +// GetPreciseSigOpCount returns the number of signature operations in scriptPubKey. If bip16 is true then scriptSig may +// be searched for the Pay -To-Script-Hash script in order to find the precise number of signature operations in the +// transaction. If the script fails to parse, then the count up to the point of failure is returned. +func GetPreciseSigOpCount(scriptSig, scriptPubKey []byte, bip16 bool) int { + // Don't check error since parseScript returns the parsed-up-to-error + // list of pops. + pops, _ := parseScript(scriptPubKey) + // Treat non P2SH transactions as normal. + if !(bip16 && isScriptHash(pops)) { + return getSigOpCount(pops, true) + } + // The public key script is a pay-to-script-hash, so parse the signature script to get the final item. Scripts that + // fail to fully parse count as 0 signature operations. + sigPops, e := parseScript(scriptSig) + if e != nil { + return 0 + } + // The signature script must only push data to the stack for P2SH to be a valid pair, so the signature operation + // count is 0 when that is not the case. + if !isPushOnly(sigPops) || len(sigPops) == 0 { + return 0 + } + // The P2SH script is the last item the signature script pushes to the stack. When the script is empty, there are no + // signature operations. + shScript := sigPops[len(sigPops)-1].data + if len(shScript) == 0 { + return 0 + } + // Parse the P2SH script and don't check the error since parseScript returns the parsed-up-to-error list of pops and + // the consensus rules dictate signature operations are counted up to the first parse failure. + shPops, _ := parseScript(shScript) + return getSigOpCount(shPops, true) +} + +// // GetWitnessSigOpCount returns the number of signature operations generated by +// // spending the passed pkScript with the specified witness, or sigScript. Unlike +// // GetPreciseSigOpCount, this function is able to accurately count the number of +// // signature operations generated by spending witness programs, and nested p2sh +// // witness programs. If the script fails to parse, then the count up to the +// // point of failure is returned. +// func GetWitnessSigOpCount(sigScript, pkScript []byte, witness wire.TxWitness) int { +// // If this is a regular witness program, then we can proceed directly to +// // counting its signature operations without any further processing. +// if IsWitnessProgram(pkScript) { +// return getWitnessSigOps(pkScript, witness) +// } +// // Next, we'll check the sigScript to see if this is a nested p2sh witness +// // program. This is a case wherein the sigScript is actually a datapush of a +// // p2wsh witness program. +// sigPops, e := parseScript(sigScript) +// if e != nil { +// return 0 +// } +// if IsPayToScriptHash(pkScript) && isPushOnly(sigPops) && +// IsWitnessProgram(sigScript[1:]) { +// return getWitnessSigOps(sigScript[1:], witness) +// } +// return 0 +// } + +// // getWitnessSigOps returns the number of signature operations generated by +// // spending the passed witness program wit the passed witness. The exact +// // signature counting heuristic is modified by the version of the passed witness +// // program. If the version of the witness program is unable to be extracted, +// // then 0 is returned for the sig op count. +// func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int { +// // Attempt to extract the witness program version. +// witnessVersion, witnessProgram, e := ExtractWitnessPrograminfo( +// pkScript, +// ) +// if e != nil { +// return 0 +// } +// switch witnessVersion { +// case 0: +// switch { +// case len(witnessProgram) == payToWitnessPubKeyHashDataSize: +// return 1 +// case len(witnessProgram) == payToWitnessScriptHashDataSize && +// len(witness) > 0: +// witnessScript := witness[len(witness)-1] +// pops, _ := parseScript(witnessScript) +// return getSigOpCount(pops, true) +// } +// } +// return 0 +// } + +// IsUnspendable returns whether the passed public key script is unspendable, or guaranteed to fail at execution. This +// allows inputs to be pruned instantly when entering the UTXO set. +func IsUnspendable(pkScript []byte) bool { + pops, e := parseScript(pkScript) + if e != nil { + return true + } + return len(pops) > 0 && pops[0].opcode.value == OP_RETURN +} diff --git a/pkg/txscript/script_test.go b/pkg/txscript/script_test.go new file mode 100644 index 0000000..a0a4f03 --- /dev/null +++ b/pkg/txscript/script_test.go @@ -0,0 +1,4190 @@ +package txscript + +import ( + "bytes" + "reflect" + "testing" +) + +// TestParseOpcode tests for opcode parsing with bad data templates. +func TestParseOpcode(t *testing.T) { + // Deep copy the array and make one of the opcodes invalid by setting it to the wrong length. + fakeArray := OpcodeArray + fakeArray[OP_PUSHDATA4] = opcode{value: OP_PUSHDATA4, + name: "OP_PUSHDATA4", length: -8, opfunc: opcodePushData, + } + // This script would be fine if -8 was a valid length. + _, e := ParseScriptTemplate([]byte{OP_PUSHDATA4, 0x1, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + }, &fakeArray, + ) + if e == nil { + t.Errorf("no error with dodgy opcode array!") + } +} + +// TestUnparsingInvalidOpcodes tests for errors when unparsing invalid parsed opcodes. +func TestUnparsingInvalidOpcodes(t *testing.T) { + tests := []struct { + name string + pop *parsedOpcode + expectedErr error + }{ + { + name: "OP_FALSE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_FALSE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_FALSE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_FALSE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_1 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_1], + data: nil, + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_1", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_1], + data: make([]byte, 1), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_1 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_1], + data: make([]byte, 2), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_2 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_2], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_2", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_2], + data: make([]byte, 2), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_2 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_2], + data: make([]byte, 3), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_3 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_3], + data: make([]byte, 2), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_3", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_3], + data: make([]byte, 3), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_3 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_3], + data: make([]byte, 4), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_4 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_4], + data: make([]byte, 3), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_4", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_4], + data: make([]byte, 4), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_4 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_4], + data: make([]byte, 5), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_5 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_5], + data: make([]byte, 4), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_5", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_5], + data: make([]byte, 5), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_5 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_5], + data: make([]byte, 6), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_6 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_6], + data: make([]byte, 5), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_6", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_6], + data: make([]byte, 6), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_6 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_6], + data: make([]byte, 7), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_7 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_7], + data: make([]byte, 6), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_7", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_7], + data: make([]byte, 7), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_7 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_7], + data: make([]byte, 8), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_8 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_8], + data: make([]byte, 7), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_8", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_8], + data: make([]byte, 8), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_8 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_8], + data: make([]byte, 9), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_9 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_9], + data: make([]byte, 8), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_9", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_9], + data: make([]byte, 9), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_9 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_9], + data: make([]byte, 10), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_10 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_10], + data: make([]byte, 9), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_10", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_10], + data: make([]byte, 10), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_10 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_10], + data: make([]byte, 11), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_11 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_11], + data: make([]byte, 10), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_11", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_11], + data: make([]byte, 11), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_11 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_11], + data: make([]byte, 12), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_12 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_12], + data: make([]byte, 11), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_12", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_12], + data: make([]byte, 12), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_12 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_12], + data: make([]byte, 13), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_13 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_13], + data: make([]byte, 12), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_13", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_13], + data: make([]byte, 13), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_13 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_13], + data: make([]byte, 14), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_14 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_14], + data: make([]byte, 13), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_14", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_14], + data: make([]byte, 14), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_14 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_14], + data: make([]byte, 15), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_15 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_15], + data: make([]byte, 14), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_15", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_15], + data: make([]byte, 15), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_15 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_15], + data: make([]byte, 16), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_16 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_16], + data: make([]byte, 15), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_16", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_16], + data: make([]byte, 16), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_16 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_16], + data: make([]byte, 17), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_17 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_17], + data: make([]byte, 16), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_17", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_17], + data: make([]byte, 17), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_17 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_17], + data: make([]byte, 18), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_18 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_18], + data: make([]byte, 17), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_18", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_18], + data: make([]byte, 18), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_18 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_18], + data: make([]byte, 19), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_19 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_19], + data: make([]byte, 18), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_19", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_19], + data: make([]byte, 19), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_19 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_19], + data: make([]byte, 20), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_20 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_20], + data: make([]byte, 19), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_20", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_20], + data: make([]byte, 20), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_20 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_20], + data: make([]byte, 21), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_21 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_21], + data: make([]byte, 20), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_21", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_21], + data: make([]byte, 21), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_21 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_21], + data: make([]byte, 22), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_22 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_22], + data: make([]byte, 21), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_22", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_22], + data: make([]byte, 22), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_22 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_22], + data: make([]byte, 23), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_23 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_23], + data: make([]byte, 22), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_23", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_23], + data: make([]byte, 23), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_23 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_23], + data: make([]byte, 24), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_24 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_24], + data: make([]byte, 23), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_24", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_24], + data: make([]byte, 24), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_24 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_24], + data: make([]byte, 25), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_25 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_25], + data: make([]byte, 24), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_25", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_25], + data: make([]byte, 25), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_25 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_25], + data: make([]byte, 26), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_26 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_26], + data: make([]byte, 25), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_26", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_26], + data: make([]byte, 26), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_26 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_26], + data: make([]byte, 27), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_27 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_27], + data: make([]byte, 26), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_27", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_27], + data: make([]byte, 27), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_27 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_27], + data: make([]byte, 28), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_28 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_28], + data: make([]byte, 27), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_28", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_28], + data: make([]byte, 28), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_28 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_28], + data: make([]byte, 29), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_29 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_29], + data: make([]byte, 28), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_29", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_29], + data: make([]byte, 29), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_29 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_29], + data: make([]byte, 30), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_30 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_30], + data: make([]byte, 29), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_30", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_30], + data: make([]byte, 30), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_30 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_30], + data: make([]byte, 31), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_31 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_31], + data: make([]byte, 30), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_31", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_31], + data: make([]byte, 31), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_31 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_31], + data: make([]byte, 32), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_32 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_32], + data: make([]byte, 31), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_32", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_32], + data: make([]byte, 32), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_32 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_32], + data: make([]byte, 33), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_33 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_33], + data: make([]byte, 32), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_33", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_33], + data: make([]byte, 33), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_33 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_33], + data: make([]byte, 34), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_34 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_34], + data: make([]byte, 33), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_34", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_34], + data: make([]byte, 34), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_34 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_34], + data: make([]byte, 35), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_35 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_35], + data: make([]byte, 34), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_35", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_35], + data: make([]byte, 35), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_35 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_35], + data: make([]byte, 36), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_36 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_36], + data: make([]byte, 35), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_36", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_36], + data: make([]byte, 36), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_36 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_36], + data: make([]byte, 37), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_37 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_37], + data: make([]byte, 36), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_37", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_37], + data: make([]byte, 37), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_37 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_37], + data: make([]byte, 38), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_38 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_38], + data: make([]byte, 37), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_38", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_38], + data: make([]byte, 38), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_38 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_38], + data: make([]byte, 39), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_39 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_39], + data: make([]byte, 38), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_39", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_39], + data: make([]byte, 39), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_39 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_39], + data: make([]byte, 40), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_40 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_40], + data: make([]byte, 39), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_40", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_40], + data: make([]byte, 40), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_40 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_40], + data: make([]byte, 41), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_41 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_41], + data: make([]byte, 40), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_41", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_41], + data: make([]byte, 41), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_41 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_41], + data: make([]byte, 42), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_42 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_42], + data: make([]byte, 41), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_42", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_42], + data: make([]byte, 42), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_42 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_42], + data: make([]byte, 43), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_43 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_43], + data: make([]byte, 42), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_43", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_43], + data: make([]byte, 43), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_43 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_43], + data: make([]byte, 44), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_44 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_44], + data: make([]byte, 43), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_44", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_44], + data: make([]byte, 44), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_44 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_44], + data: make([]byte, 45), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_45 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_45], + data: make([]byte, 44), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_45", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_45], + data: make([]byte, 45), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_45 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_45], + data: make([]byte, 46), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_46 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_46], + data: make([]byte, 45), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_46", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_46], + data: make([]byte, 46), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_46 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_46], + data: make([]byte, 47), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_47 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_47], + data: make([]byte, 46), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_47", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_47], + data: make([]byte, 47), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_47 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_47], + data: make([]byte, 48), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_48 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_48], + data: make([]byte, 47), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_48", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_48], + data: make([]byte, 48), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_48 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_48], + data: make([]byte, 49), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_49 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_49], + data: make([]byte, 48), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_49", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_49], + data: make([]byte, 49), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_49 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_49], + data: make([]byte, 50), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_50 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_50], + data: make([]byte, 49), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_50", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_50], + data: make([]byte, 50), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_50 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_50], + data: make([]byte, 51), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_51 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_51], + data: make([]byte, 50), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_51", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_51], + data: make([]byte, 51), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_51 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_51], + data: make([]byte, 52), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_52 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_52], + data: make([]byte, 51), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_52", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_52], + data: make([]byte, 52), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_52 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_52], + data: make([]byte, 53), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_53 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_53], + data: make([]byte, 52), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_53", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_53], + data: make([]byte, 53), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_53 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_53], + data: make([]byte, 54), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_54 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_54], + data: make([]byte, 53), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_54", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_54], + data: make([]byte, 54), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_54 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_54], + data: make([]byte, 55), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_55 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_55], + data: make([]byte, 54), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_55", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_55], + data: make([]byte, 55), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_55 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_55], + data: make([]byte, 56), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_56 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_56], + data: make([]byte, 55), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_56", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_56], + data: make([]byte, 56), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_56 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_56], + data: make([]byte, 57), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_57 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_57], + data: make([]byte, 56), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_57", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_57], + data: make([]byte, 57), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_57 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_57], + data: make([]byte, 58), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_58 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_58], + data: make([]byte, 57), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_58", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_58], + data: make([]byte, 58), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_58 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_58], + data: make([]byte, 59), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_59 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_59], + data: make([]byte, 58), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_59", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_59], + data: make([]byte, 59), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_59 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_59], + data: make([]byte, 60), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_60 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_60], + data: make([]byte, 59), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_60", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_60], + data: make([]byte, 60), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_60 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_60], + data: make([]byte, 61), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_61 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_61], + data: make([]byte, 60), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_61", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_61], + data: make([]byte, 61), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_61 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_61], + data: make([]byte, 62), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_62 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_62], + data: make([]byte, 61), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_62", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_62], + data: make([]byte, 62), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_62 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_62], + data: make([]byte, 63), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_63 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_63], + data: make([]byte, 62), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_63", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_63], + data: make([]byte, 63), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_63 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_63], + data: make([]byte, 64), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_64 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_64], + data: make([]byte, 63), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_64", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_64], + data: make([]byte, 64), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_64 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_64], + data: make([]byte, 65), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_65 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_65], + data: make([]byte, 64), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_65", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_65], + data: make([]byte, 65), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_65 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_65], + data: make([]byte, 66), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_66 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_66], + data: make([]byte, 65), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_66", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_66], + data: make([]byte, 66), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_66 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_66], + data: make([]byte, 67), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_67 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_67], + data: make([]byte, 66), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_67", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_67], + data: make([]byte, 67), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_67 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_67], + data: make([]byte, 68), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_68 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_68], + data: make([]byte, 67), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_68", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_68], + data: make([]byte, 68), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_68 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_68], + data: make([]byte, 69), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_69 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_69], + data: make([]byte, 68), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_69", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_69], + data: make([]byte, 69), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_69 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_69], + data: make([]byte, 70), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_70 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_70], + data: make([]byte, 69), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_70", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_70], + data: make([]byte, 70), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_70 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_70], + data: make([]byte, 71), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_71 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_71], + data: make([]byte, 70), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_71", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_71], + data: make([]byte, 71), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_71 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_71], + data: make([]byte, 72), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_72 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_72], + data: make([]byte, 71), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_72", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_72], + data: make([]byte, 72), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_72 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_72], + data: make([]byte, 73), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_73 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_73], + data: make([]byte, 72), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_73", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_73], + data: make([]byte, 73), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_73 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_73], + data: make([]byte, 74), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_74 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_74], + data: make([]byte, 73), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_74", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_74], + data: make([]byte, 74), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_74 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_74], + data: make([]byte, 75), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_75 short", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_75], + data: make([]byte, 74), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DATA_75", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_75], + data: make([]byte, 75), + }, + expectedErr: nil, + }, + { + name: "OP_DATA_75 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DATA_75], + data: make([]byte, 76), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_PUSHDATA1", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUSHDATA1], + data: []byte{0, 1, 2, 3, 4}, + }, + expectedErr: nil, + }, + { + name: "OP_PUSHDATA2", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUSHDATA2], + data: []byte{0, 1, 2, 3, 4}, + }, + expectedErr: nil, + }, + { + name: "OP_PUSHDATA4", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUSHDATA1], + data: []byte{0, 1, 2, 3, 4}, + }, + expectedErr: nil, + }, + { + name: "OP_1NEGATE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_1NEGATE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_1NEGATE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_1NEGATE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RESERVED", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RESERVED], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RESERVED long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RESERVED], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_TRUE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_TRUE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_TRUE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_TRUE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_3", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_3], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_3 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_3], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_4", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_4], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_4 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_4], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_5", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_5], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_5 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_5], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_6", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_6], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_6 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_6], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_7", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_7], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_7 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_7], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_8", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_8], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_8 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_8], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_9", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_9], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_9 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_9], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_10", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_10], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_10 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_10], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_11", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_11], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_11 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_11], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_12", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_12], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_12 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_12], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_13", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_13], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_13 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_13], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_14", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_14], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_14 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_14], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_15", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_15], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_15 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_15], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_16", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_16], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_16 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_16], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_VER", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VER], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_VER long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VER], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_IF", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_IF], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_IF long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_IF], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOTIF", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOTIF], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOTIF long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOTIF], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_VERIF", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VERIF], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_VERIF long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VERIF], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_VERNOTIF", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VERNOTIF], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_VERNOTIF long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VERNOTIF], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_ELSE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ELSE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_ELSE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ELSE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_ENDIF", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ENDIF], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_ENDIF long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ENDIF], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_VERIFY", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VERIFY], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_VERIFY long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_VERIFY], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RETURN", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RETURN], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RETURN long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RETURN], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_TOALTSTACK", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_TOALTSTACK], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_TOALTSTACK long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_TOALTSTACK], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_FROMALTSTACK", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_FROMALTSTACK], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_FROMALTSTACK long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_FROMALTSTACK], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2DROP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2DROP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2DROP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2DROP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2DUP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2DUP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2DUP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2DUP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_3DUP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_3DUP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_3DUP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_3DUP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2OVER", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2OVER], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2OVER long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2OVER], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2ROT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2ROT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2ROT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2ROT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2SWAP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2SWAP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2SWAP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2SWAP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_IFDUP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_IFDUP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_IFDUP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_IFDUP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DEPTH", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DEPTH], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_DEPTH long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DEPTH], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DROP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DROP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_DROP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DROP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DUP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DUP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_DUP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DUP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NIP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NIP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NIP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NIP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_OVER", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_OVER], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_OVER long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_OVER], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_PICK", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PICK], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_PICK long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PICK], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_ROLL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ROLL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_ROLL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ROLL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_ROT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ROT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_ROT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ROT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_SWAP", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SWAP], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_SWAP long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SWAP], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_TUCK", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_TUCK], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_TUCK long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_TUCK], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_CAT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CAT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_CAT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CAT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_SUBSTR", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SUBSTR], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_SUBSTR long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SUBSTR], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_LEFT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LEFT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_LEFT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LEFT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_LEFT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LEFT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_LEFT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LEFT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RIGHT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RIGHT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RIGHT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RIGHT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_SIZE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SIZE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_SIZE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SIZE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_INVERT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_INVERT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_INVERT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_INVERT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_AND", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_AND], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_AND long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_AND], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_OR", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_OR], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_OR long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_OR], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_XOR", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_XOR], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_XOR long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_XOR], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_EQUAL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_EQUAL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_EQUAL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_EQUAL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_EQUALVERIFY", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_EQUALVERIFY], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_EQUALVERIFY long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_EQUALVERIFY], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RESERVED1", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RESERVED1], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RESERVED1 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RESERVED1], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RESERVED2", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RESERVED2], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RESERVED2 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RESERVED2], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_1ADD", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_1ADD], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_1ADD long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_1ADD], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_1SUB", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_1SUB], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_1SUB long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_1SUB], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2MUL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2MUL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2MUL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2MUL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_2DIV", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2DIV], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_2DIV long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_2DIV], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NEGATE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NEGATE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NEGATE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NEGATE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_ABS", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ABS], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_ABS long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ABS], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_0NOTEQUAL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_0NOTEQUAL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_0NOTEQUAL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_0NOTEQUAL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_ADD", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ADD], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_ADD long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_ADD], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_SUB", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SUB], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_SUB long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SUB], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_MUL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MUL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_MUL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MUL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_DIV", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DIV], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_DIV long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_DIV], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_MOD", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MOD], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_MOD long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MOD], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_LSHIFT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LSHIFT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_LSHIFT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LSHIFT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RSHIFT", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RSHIFT], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RSHIFT long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RSHIFT], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_BOOLAND", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_BOOLAND], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_BOOLAND long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_BOOLAND], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_BOOLOR", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_BOOLOR], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_BOOLOR long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_BOOLOR], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NUMEQUAL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NUMEQUAL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NUMEQUAL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NUMEQUAL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NUMEQUALVERIFY", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NUMEQUALVERIFY], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NUMEQUALVERIFY long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NUMEQUALVERIFY], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NUMNOTEQUAL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NUMNOTEQUAL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NUMNOTEQUAL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NUMNOTEQUAL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_LESSTHAN", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LESSTHAN], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_LESSTHAN long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LESSTHAN], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_GREATERTHAN", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_GREATERTHAN], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_GREATERTHAN long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_GREATERTHAN], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_LESSTHANOREQUAL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LESSTHANOREQUAL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_LESSTHANOREQUAL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_LESSTHANOREQUAL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_GREATERTHANOREQUAL", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_GREATERTHANOREQUAL], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_GREATERTHANOREQUAL long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_GREATERTHANOREQUAL], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_MIN", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MIN], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_MIN long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MIN], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_MAX", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MAX], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_MAX long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_MAX], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_WITHIN", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_WITHIN], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_WITHIN long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_WITHIN], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_RIPEMD160", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RIPEMD160], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_RIPEMD160 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_RIPEMD160], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_SHA1", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SHA1], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_SHA1 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SHA1], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_SHA256", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SHA256], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_SHA256 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_SHA256], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_HASH160", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_HASH160], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_HASH160 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_HASH160], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_HASH256", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_HASH256], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_HASH256 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_HASH256], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_CODESAPERATOR", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CODESEPARATOR], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_CODESEPARATOR long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CODESEPARATOR], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_CHECKSIG", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKSIG], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_CHECKSIG long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKSIG], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_CHECKSIGVERIFY", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKSIGVERIFY], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_CHECKSIGVERIFY long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKSIGVERIFY], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_CHECKMULTISIG", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKMULTISIG], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_CHECKMULTISIG long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKMULTISIG], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_CHECKMULTISIGVERIFY", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKMULTISIGVERIFY], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_CHECKMULTISIGVERIFY long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_CHECKMULTISIGVERIFY], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP1", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP1], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP1 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP1], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP2", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP2], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP2 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP2], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP3", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP3], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP3 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP3], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP4", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP4], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP4 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP4], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP5", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP5], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP5 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP5], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP6", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP6], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP6 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP6], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP7", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP7], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP7 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP7], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP8", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP8], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP8 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP8], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP9", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP9], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP9 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP9], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_NOP10", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP10], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_NOP10 long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_NOP10], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_PUBKEYHASH", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUBKEYHASH], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_PUBKEYHASH long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUBKEYHASH], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_PUBKEY", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUBKEY], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_PUBKEY long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_PUBKEY], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + { + name: "OP_INVALIDOPCODE", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_INVALIDOPCODE], + data: nil, + }, + expectedErr: nil, + }, + { + name: "OP_INVALIDOPCODE long", + pop: &parsedOpcode{ + opcode: &OpcodeArray[OP_INVALIDOPCODE], + data: make([]byte, 1), + }, + expectedErr: scriptError(ErrInternal, ""), + }, + } + var e error + for _, test := range tests { + _, e = test.pop.bytes() + if e = tstCheckScriptError(e, test.expectedErr); e != nil { + t.Errorf("Parsed opcode test '%s': %v", test.name, e) + continue + } + } +} + +// TestPushedData ensured the PushedData function extracts the expected data out of various scripts. +func TestPushedData(t *testing.T) { + t.Parallel() + var tests = []struct { + script string + out [][]byte + valid bool + }{ + { + "0 IF 0 ELSE 2 ENDIF", + [][]byte{nil, nil}, + true, + }, + { + "16777216 10000000", + [][]byte{ + {0x00, 0x00, 0x00, 0x01}, // 16777216 + {0x80, 0x96, 0x98, 0x00}, // 10000000 + }, + true, + }, + { + "DUP HASH160 '17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem' EQUALVERIFY CHECKSIG", + [][]byte{ + // 17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem + { + 0x31, 0x37, 0x56, 0x5a, 0x4e, 0x58, 0x31, 0x53, 0x4e, 0x35, + 0x4e, 0x74, 0x4b, 0x61, 0x38, 0x55, 0x51, 0x46, 0x78, 0x77, + 0x51, 0x62, 0x46, 0x65, 0x46, 0x63, 0x33, 0x69, 0x71, 0x52, + 0x59, 0x68, 0x65, 0x6d, + }, + }, + true, + }, + { + "PUSHDATA4 1000 EQUAL", + nil, + false, + }, + } + for i, test := range tests { + script := mustParseShortForm(test.script) + data, e := PushedData(script) + if test.valid && e != nil { + t.Errorf("TestPushedData failed test #%d: %v\n", i, e) + continue + } else if !test.valid && e == nil { + t.Errorf("TestPushedData failed test #%d: test should "+ + "be invalid\n", i, + ) + continue + } + if !reflect.DeepEqual(data, test.out) { + t.Errorf("TestPushedData failed test #%d: want: %x "+ + "got: %x\n", i, test.out, data, + ) + } + } +} + +// TestHasCanonicalPush ensures the canonicalPush function works as expected. +func TestHasCanonicalPush(t *testing.T) { + t.Parallel() + for i := 0; i < 65535; i++ { + script, e := NewScriptBuilder().AddInt64(int64(i)).Script() + if e != nil { + t.Errorf("Script: test #%d unexpected error: %v\n", i, + e, + ) + continue + } + if result := IsPushOnlyScript(script); !result { + t.Errorf("IsPushOnlyScript: test #%d failed: %x\n", i, + script, + ) + continue + } + pops, e := parseScript(script) + if e != nil { + t.Errorf("parseScript: #%d failed: %v", i, e) + continue + } + for _, pop := range pops { + if result := canonicalPush(pop); !result { + t.Errorf("canonicalPush: test #%d failed: %x\n", + i, script, + ) + break + } + } + } + for i := 0; i <= MaxScriptElementSize; i++ { + builder := NewScriptBuilder() + builder.AddData(bytes.Repeat([]byte{0x49}, i)) + script, e := builder.Script() + if e != nil { + t.Errorf("StandardPushesTests test #%d unexpected error: %v\n", i, e) + continue + } + if result := IsPushOnlyScript(script); !result { + t.Errorf("StandardPushesTests IsPushOnlyScript test #%d failed: %x\n", i, script) + continue + } + pops, e := parseScript(script) + if e != nil { + t.Errorf("StandardPushesTests #%d failed to TstParseScript: %v", i, e) + continue + } + for _, pop := range pops { + if result := canonicalPush(pop); !result { + t.Errorf("StandardPushesTests TstHasCanonicalPushes test #%d failed: %x\n", i, script) + break + } + } + } +} + +// TestGetPreciseSigOps ensures the more precise signature operation counting mechanism which includes signatures in P2SH scripts works as expected. +func TestGetPreciseSigOps(t *testing.T) { + t.Parallel() + tests := []struct { + name string + scriptSig []byte + nSigOps int + }{ + { + name: "scriptSig doesn't parse", + scriptSig: mustParseShortForm("PUSHDATA1 0x02"), + }, + { + name: "scriptSig isn't push only", + scriptSig: mustParseShortForm("1 DUP"), + nSigOps: 0, + }, + { + name: "scriptSig length 0", + scriptSig: nil, + nSigOps: 0, + }, + { + name: "No script at the end", + // No script at end but still push only. + scriptSig: mustParseShortForm("1 1"), + nSigOps: 0, + }, + { + name: "pushed script doesn't parse", + scriptSig: mustParseShortForm("DATA_2 PUSHDATA1 0x02"), + }, + } + // The signature in the p2sh script is nonsensical for the tests since this script will never be executed. What + // matters is that it matches the right pattern. + pkScript := mustParseShortForm("HASH160 DATA_20 0x433ec2ac1ffa1b7b7d0" + + "27f564529c57197f9ae88 EQUAL", + ) + for _, test := range tests { + count := GetPreciseSigOpCount(test.scriptSig, pkScript, true) + if count != test.nSigOps { + t.Errorf("%s: expected count of %d, got %d", test.name, + test.nSigOps, count, + ) + } + } +} + +// TestRemoveOpcodes ensures that removing opcodes from scripts behaves as expected. +func TestRemoveOpcodes(t *testing.T) { + t.Parallel() + tests := []struct { + name string + before string + remove byte + e error + after string + }{ + { + // Nothing to remove. + name: "nothing to remove", + before: "NOP", + remove: OP_CODESEPARATOR, + after: "NOP", + }, + { + // Test basic opcode removal. + name: "codeseparator 1", + before: "NOP CODESEPARATOR TRUE", + remove: OP_CODESEPARATOR, + after: "NOP TRUE", + }, + { + // The opcode in question is actually part of the data in a previous opcode. + name: "codeseparator by coincidence", + before: "NOP DATA_1 CODESEPARATOR TRUE", + remove: OP_CODESEPARATOR, + after: "NOP DATA_1 CODESEPARATOR TRUE", + }, + { + name: "invalid opcode", + before: "CAT", + remove: OP_CODESEPARATOR, + after: "CAT", + }, + { + name: "invalid length (instruction)", + before: "PUSHDATA1", + remove: OP_CODESEPARATOR, + e: scriptError(ErrMalformedPush, ""), + }, + { + name: "invalid length (data)", + before: "PUSHDATA1 0xff 0xfe", + remove: OP_CODESEPARATOR, + e: scriptError(ErrMalformedPush, ""), + }, + } + // tstRemoveOpcode is a convenience function to parse the provided raw script, remove the passed opcode, then + // unparse the result back into a raw script. + tstRemoveOpcode := func(script []byte, opcode byte) ([]byte, error) { + pops, e := parseScript(script) + if e != nil { + return nil, e + } + pops = removeOpcode(pops, opcode) + return unparseScript(pops) + } + for _, test := range tests { + before := mustParseShortForm(test.before) + after := mustParseShortForm(test.after) + result, e := tstRemoveOpcode(before, test.remove) + if e = tstCheckScriptError(e, test.e); e != nil { + t.Errorf("%s: %v", test.name, e) + continue + } + if !bytes.Equal(after, result) { + t.Errorf("%s: value does not equal expected: exp: %q"+ + " got: %q", test.name, after, result, + ) + } + } +} + +// TestRemoveOpcodeByData ensures that removing data carrying opcodes based on the data they contain works as expected. +func TestRemoveOpcodeByData(t *testing.T) { + t.Parallel() + tests := []struct { + name string + before []byte + remove []byte + e error + after []byte + }{ + { + name: "nothing to do", + before: []byte{OP_NOP}, + remove: []byte{1, 2, 3, 4}, + after: []byte{OP_NOP}, + }, + { + name: "simple case", + before: []byte{OP_DATA_4, 1, 2, 3, 4}, + remove: []byte{1, 2, 3, 4}, + after: nil, + }, + { + name: "simple case (miss)", + before: []byte{OP_DATA_4, 1, 2, 3, 4}, + remove: []byte{1, 2, 3, 5}, + after: []byte{OP_DATA_4, 1, 2, 3, 4}, + }, + { + // padded to keep it canonical. + name: "simple case (pushdata1)", + before: append(append([]byte{OP_PUSHDATA1, 76}, + bytes.Repeat([]byte{0}, 72)..., + ), + []byte{1, 2, 3, 4}..., + ), + remove: []byte{1, 2, 3, 4}, + after: nil, + }, + { + name: "simple case (pushdata1 miss)", + before: append(append([]byte{OP_PUSHDATA1, 76}, + bytes.Repeat([]byte{0}, 72)..., + ), + []byte{1, 2, 3, 4}..., + ), + remove: []byte{1, 2, 3, 5}, + after: append(append([]byte{OP_PUSHDATA1, 76}, + bytes.Repeat([]byte{0}, 72)..., + ), + []byte{1, 2, 3, 4}..., + ), + }, + { + name: "simple case (pushdata1 miss noncanonical)", + before: []byte{OP_PUSHDATA1, 4, 1, 2, 3, 4}, + remove: []byte{1, 2, 3, 4}, + after: []byte{OP_PUSHDATA1, 4, 1, 2, 3, 4}, + }, + { + name: "simple case (pushdata2)", + before: append(append([]byte{OP_PUSHDATA2, 0, 1}, + bytes.Repeat([]byte{0}, 252)..., + ), + []byte{1, 2, 3, 4}..., + ), + remove: []byte{1, 2, 3, 4}, + after: nil, + }, + { + name: "simple case (pushdata2 miss)", + before: append(append([]byte{OP_PUSHDATA2, 0, 1}, + bytes.Repeat([]byte{0}, 252)..., + ), + []byte{1, 2, 3, 4}..., + ), + remove: []byte{1, 2, 3, 4, 5}, + after: append(append([]byte{OP_PUSHDATA2, 0, 1}, + bytes.Repeat([]byte{0}, 252)..., + ), + []byte{1, 2, 3, 4}..., + ), + }, + { + name: "simple case (pushdata2 miss noncanonical)", + before: []byte{OP_PUSHDATA2, 4, 0, 1, 2, 3, 4}, + remove: []byte{1, 2, 3, 4}, + after: []byte{OP_PUSHDATA2, 4, 0, 1, 2, 3, 4}, + }, + { + // This is padded to make the push canonical. + name: "simple case (pushdata4)", + before: append(append([]byte{OP_PUSHDATA4, 0, 0, 1, 0}, + bytes.Repeat([]byte{0}, 65532)..., + ), + []byte{1, 2, 3, 4}..., + ), + remove: []byte{1, 2, 3, 4}, + after: nil, + }, + { + name: "simple case (pushdata4 miss noncanonical)", + before: []byte{OP_PUSHDATA4, 4, 0, 0, 0, 1, 2, 3, 4}, + remove: []byte{1, 2, 3, 4}, + after: []byte{OP_PUSHDATA4, 4, 0, 0, 0, 1, 2, 3, 4}, + }, + { + // This is padded to make the push canonical. + name: "simple case (pushdata4 miss)", + before: append(append([]byte{OP_PUSHDATA4, 0, 0, 1, 0}, + bytes.Repeat([]byte{0}, 65532)..., + ), []byte{1, 2, 3, 4}..., + ), + remove: []byte{1, 2, 3, 4, 5}, + after: append(append([]byte{OP_PUSHDATA4, 0, 0, 1, 0}, + bytes.Repeat([]byte{0}, 65532)..., + ), []byte{1, 2, 3, 4}..., + ), + }, + { + name: "invalid opcode ", + before: []byte{OP_UNKNOWN187}, + remove: []byte{1, 2, 3, 4}, + after: []byte{OP_UNKNOWN187}, + }, + { + name: "invalid length (instruction)", + before: []byte{OP_PUSHDATA1}, + remove: []byte{1, 2, 3, 4}, + e: scriptError(ErrMalformedPush, ""), + }, + { + name: "invalid length (data)", + before: []byte{OP_PUSHDATA1, 255, 254}, + remove: []byte{1, 2, 3, 4}, + e: scriptError(ErrMalformedPush, ""), + }, + } + // tstRemoveOpcodeByData is a convenience function to parse the provided raw script, remove the passed data, then + // unparse the result back into a raw script. + tstRemoveOpcodeByData := func(script []byte, data []byte) ([]byte, error) { + pops, e := parseScript(script) + if e != nil { + return nil, e + } + pops = removeOpcodeByData(pops, data) + return unparseScript(pops) + } + for _, test := range tests { + result, e := tstRemoveOpcodeByData(test.before, test.remove) + if e = tstCheckScriptError(e, test.e); e != nil { + t.Errorf("%s: %v", test.name, e) + continue + } + if !bytes.Equal(test.after, result) { + t.Errorf("%s: value does not equal expected: exp: %q"+ + " got: %q", test.name, test.after, result, + ) + } + } +} + +// TestIsPayToScriptHash ensures the IsPayToScriptHash function returns the expected results for all the scripts in +// scriptClassTests. +func TestIsPayToScriptHash(t *testing.T) { + t.Parallel() + for _, test := range scriptClassTests { + script := mustParseShortForm(test.script) + shouldBe := test.class == ScriptHashTy + p2sh := IsPayToScriptHash(script) + if p2sh != shouldBe { + t.Errorf("%s: expected p2sh %v, got %v", test.name, + shouldBe, p2sh, + ) + } + } +} + +// TestHasCanonicalPushes ensures the canonicalPush function properly determines what is considered a canonical push for +// the purposes of removeOpcodeByData. +func TestHasCanonicalPushes(t *testing.T) { + t.Parallel() + tests := []struct { + name string + script string + expected bool + }{ + { + name: "does not parse", + script: "0x046708afdb0fe5548271967f1a67130b7105cd6a82" + + "8e03909a67962e0ea1f61d", + expected: false, + }, + { + name: "non-canonical push", + script: "PUSHDATA1 0x04 0x01020304", + expected: false, + }, + } + for i, test := range tests { + script := mustParseShortForm(test.script) + pops, e := parseScript(script) + if e != nil { + if test.expected { + t.Errorf("TstParseScript #%d failed: %v", i, e) + } + continue + } + for _, pop := range pops { + if canonicalPush(pop) != test.expected { + t.Errorf("canonicalPush: #%d (%s) wrong result"+ + "\ngot: %v\nwant: %v", i, test.name, + true, test.expected, + ) + break + } + } + } +} + +// TestIsPushOnlyScript ensures the IsPushOnlyScript function returns the expected results. +func TestIsPushOnlyScript(t *testing.T) { + t.Parallel() + test := struct { + name string + script []byte + expected bool + }{ + name: "does not parse", + script: mustParseShortForm("0x046708afdb0fe5548271967f1a67130" + + "b7105cd6a828e03909a67962e0ea1f61d", + ), + expected: false, + } + if IsPushOnlyScript(test.script) != test.expected { + t.Errorf("IsPushOnlyScript (%s) wrong result\ngot: %v\nwant: "+ + "%v", test.name, true, test.expected, + ) + } +} + +// TestIsUnspendable ensures the IsUnspendable function returns the expected results. +func TestIsUnspendable(t *testing.T) { + t.Parallel() + tests := []struct { + // name string + pkScript []byte + expected bool + }{ + { + // Unspendable + pkScript: []byte{0x6a, 0x04, 0x74, 0x65, 0x73, 0x74}, + expected: true, + }, + { + // Spendable + pkScript: []byte{0x76, 0xa9, 0x14, 0x29, 0x95, 0xa0, + 0xfe, 0x68, 0x43, 0xfa, 0x9b, 0x95, 0x45, + 0x97, 0xf0, 0xdc, 0xa7, 0xa4, 0x4d, 0xf6, + 0xfa, 0x0b, 0x5c, 0x88, 0xac, + }, + expected: false, + }, + } + for i, test := range tests { + res := IsUnspendable(test.pkScript) + if res != test.expected { + t.Errorf("TestIsUnspendable #%d failed: got %v want %v", + i, res, test.expected, + ) + continue + } + } +} diff --git a/pkg/txscript/scriptbuilder.go b/pkg/txscript/scriptbuilder.go new file mode 100644 index 0000000..6123047 --- /dev/null +++ b/pkg/txscript/scriptbuilder.go @@ -0,0 +1,246 @@ +package txscript + +import ( + "encoding/binary" + "fmt" +) + +const ( + // defaultScriptAlloc is the default size used for the backing array for a script being built by the ScriptBuilder. + // The array will dynamically grow as needed, but this figure is intended to provide enough space for vast majority + // of scripts without needing to grow the backing array multiple times. + defaultScriptAlloc = 500 +) + +// ErrScriptNotCanonical identifies a non-canonical script. The caller can use a +// type assertion to detect this error type. +type ErrScriptNotCanonical string + +// ScriptError implements the error interface. +func (e ErrScriptNotCanonical) Error() string { + return string(e) +} + +// ScriptBuilder provides a facility for building custom scripts. It allows you +// to push opcodes, ints, and data while respecting canonical encoding. In +// general it does not ensure the script will execute correctly, however any +// data pushes which would exceed the maximum allowed script engine limits and +// are therefore guaranteed not to execute will not be pushed and will result in +// the Script function returning an error. For example, the following would +// podbuild a 2-of-3 multisig script for usage in a pay-to-script-hash (although in +// this situation MultiSigScript() would be a better choice to generate the +// script): +// +// builder := txscript.NewScriptBuilder() +// builder.AddOp(txscript.OP_2).AddData(pubKey1).AddData(pubKey2) +// builder.AddData(pubKey3).AddOp(txscript.OP_3) +// builder.AddOp(txscript.OP_CHECKMULTISIG) +// script, e := builder.Script() +// if e != nil { +// L.Script// // Handle the error. +// return +// } +// log.Printf("Final multi-sig script: %x\n", script) +type ScriptBuilder struct { + script []byte + err error +} + +// AddOp pushes the passed opcode to the end of the script. The script will not +// be modified if pushing the opcode would cause the script to exceed the +// maximum allowed script engine size. +func (b *ScriptBuilder) AddOp(opcode byte) *ScriptBuilder { + if b.err != nil { + return b + } + // Pushes that would cause the script to exceed the largest allowed script size + // would result in a non-canonical script. + if len(b.script)+1 > MaxScriptSize { + str := fmt.Sprintf("adding an opcode would exceed the maximum "+ + "allowed canonical script length of %d", MaxScriptSize, + ) + b.err = ErrScriptNotCanonical(str) + return b + } + b.script = append(b.script, opcode) + return b +} + +// AddOps pushes the passed opcodes to the end of the script. The script will +// not be modified if pushing the opcodes would cause the script to exceed the +// maximum allowed script engine size. +func (b *ScriptBuilder) AddOps(opcodes []byte) *ScriptBuilder { + if b.err != nil { + return b + } + // Pushes that would cause the script to exceed the largest allowed script size + // would result in a non-canonical script. + if len(b.script)+len(opcodes) > MaxScriptSize { + str := fmt.Sprintf("adding opcodes would exceed the maximum "+ + "allowed canonical script length of %d", MaxScriptSize, + ) + b.err = ErrScriptNotCanonical(str) + return b + } + b.script = append(b.script, opcodes...) + return b +} + +// canonicalDataSize returns the number of bytes the canonical encoding of the +// data will take. +func canonicalDataSize(data []byte) int { + dataLen := len(data) + // When the data consists of a single number that can be represented by one of + // the "small integer" opcodes, that opcode will be instead of a data push + // opcode followed by the number. + if dataLen == 0 { + return 1 + } else if dataLen == 1 && data[0] <= 16 { + return 1 + } else if dataLen == 1 && data[0] == 0x81 { + return 1 + } + if dataLen < OP_PUSHDATA1 { + return 1 + dataLen + } else if dataLen <= 0xff { + return 2 + dataLen + } else if dataLen <= 0xffff { + return 3 + dataLen + } + return 5 + dataLen +} + +// addData is the internal function that actually pushes the passed data to the +// end of the script. It automatically chooses canonical opcodes depending on +// the length of the data. A zero length buffer will lead to a push of empty +// data onto the stack (OP_0). No data limits are enforced with this function. +func (b *ScriptBuilder) addData(data []byte) *ScriptBuilder { + dataLen := len(data) + // When the data consists of a single number that can be represented by one of + // the "small integer" opcodes, use that opcode instead of a data push opcode + // followed by the number. + if dataLen == 0 || dataLen == 1 && data[0] == 0 { + b.script = append(b.script, OP_0) + return b + } else if dataLen == 1 && data[0] <= 16 { + b.script = append(b.script, (OP_1-1)+data[0]) + return b + } else if dataLen == 1 && data[0] == 0x81 { + b.script = append(b.script, byte(OP_1NEGATE)) + return b + } + // Use one of the OP_DATA_# opcodes if the length of the data is small enough so + // the data push instruction is only a single byte. Otherwise, choose the + // smallest possible OP_PUSHDATA# opcode that can represent the length of the + // data. + if dataLen < OP_PUSHDATA1 { + b.script = append(b.script, byte((OP_DATA_1-1)+dataLen)) + } else if dataLen <= 0xff { + b.script = append(b.script, OP_PUSHDATA1, byte(dataLen)) + } else if dataLen <= 0xffff { + buf := make([]byte, 2) + binary.LittleEndian.PutUint16(buf, uint16(dataLen)) + b.script = append(b.script, OP_PUSHDATA2) + b.script = append(b.script, buf...) + } else { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, uint32(dataLen)) + b.script = append(b.script, OP_PUSHDATA4) + b.script = append(b.script, buf...) + } + // Append the actual data. + b.script = append(b.script, data...) + return b +} + +// AddFullData should not typically be used by ordinary users as it does not include the checks which prevent data +// pushes larger than the maximum allowed txsizes which leads to scripts that can't be executed. This is provided for +// testing purposes such as regression tests where txsizes are intentionally made larger than allowed. Use AddData +// instead. +func (b *ScriptBuilder) AddFullData(data []byte) *ScriptBuilder { + if b.err != nil { + return b + } + return b.addData(data) +} + +// AddData pushes the passed data to the end of the script. It automatically chooses canonical opcodes depending on the +// length of the data. A zero length buffer will lead to a push of empty data onto the stack (OP_0) and any push of data +// greater than MaxScriptElementSize will not modify the script since that is not allowed by the script engine. Also, +// the script will not be modified if pushing the data would cause the script to exceed the maximum allowed script +// engine size. +func (b *ScriptBuilder) AddData(data []byte) *ScriptBuilder { + if b.err != nil { + return b + } + // Pushes that would cause the script to exceed the largest allowed script size would result in a non-canonical + // script. + dataSize := canonicalDataSize(data) + if len(b.script)+dataSize > MaxScriptSize { + str := fmt.Sprintf("adding %d bytes of data would exceed the "+ + "maximum allowed canonical script length of %d", + dataSize, MaxScriptSize, + ) + b.err = ErrScriptNotCanonical(str) + return b + } + // Pushes larger than the max script element size would result in a script that is not canonical. + dataLen := len(data) + if dataLen > MaxScriptElementSize { + str := fmt.Sprintf("adding a data element of %d bytes would "+ + "exceed the maximum allowed script element size of %d", + dataLen, MaxScriptElementSize, + ) + b.err = ErrScriptNotCanonical(str) + return b + } + return b.addData(data) +} + +// AddInt64 pushes the passed integer to the end of the script. The script will not be modified if pushing the data +// would cause the script to exceed the maximum allowed script engine size. +func (b *ScriptBuilder) AddInt64(val int64) *ScriptBuilder { + if b.err != nil { + return b + } + // Pushes that would cause the script to exceed the largest allowed script size would result in a non-canonical + // script. + if len(b.script)+1 > MaxScriptSize { + str := fmt.Sprintf("adding an integer would exceed the "+ + "maximum allow canonical script length of %d", + MaxScriptSize, + ) + b.err = ErrScriptNotCanonical(str) + return b + } + // Fast path for small integers and OP_1NEGATE. + if val == 0 { + b.script = append(b.script, OP_0) + return b + } + if val == -1 || (val >= 1 && val <= 16) { + b.script = append(b.script, byte((OP_1-1)+val)) + return b + } + return b.AddData(scriptNum(val).Bytes()) +} + +// Reset resets the script so it has no content. +func (b *ScriptBuilder) Reset() *ScriptBuilder { + b.script = b.script[0:0] + b.err = nil + return b +} + +// Script returns the currently built script. When any errors occurred while building the script, the script will be +// returned up the point of the first error along with the error. +func (b *ScriptBuilder) Script() ([]byte, error) { + return b.script, b.err +} + +// NewScriptBuilder returns a new instance of a script builder. See ScriptBuilder for details. +func NewScriptBuilder() *ScriptBuilder { + return &ScriptBuilder{ + script: make([]byte, 0, defaultScriptAlloc), + } +} diff --git a/pkg/txscript/scriptbuilder_test.go b/pkg/txscript/scriptbuilder_test.go new file mode 100644 index 0000000..9898c53 --- /dev/null +++ b/pkg/txscript/scriptbuilder_test.go @@ -0,0 +1,386 @@ +package txscript + +import ( + "bytes" + "testing" +) + +// TestScriptBuilderAddOp tests that pushing opcodes to a script via the ScriptBuilder API works as expected. +func TestScriptBuilderAddOp(t *testing.T) { + t.Parallel() + tests := []struct { + name string + opcodes []byte + expected []byte + }{ + { + name: "push OP_0", + opcodes: []byte{OP_0}, + expected: []byte{OP_0}, + }, + { + name: "push OP_1 OP_2", + opcodes: []byte{OP_1, OP_2}, + expected: []byte{OP_1, OP_2}, + }, + { + name: "push OP_HASH160 OP_EQUAL", + opcodes: []byte{OP_HASH160, OP_EQUAL}, + expected: []byte{OP_HASH160, OP_EQUAL}, + }, + } + // Run tests and individually add each op via AddOp. + builder := NewScriptBuilder() + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + builder.Reset() + for _, opcode := range test.opcodes { + builder.AddOp(opcode) + } + result, e := builder.Script() + if e != nil { + t.Errorf("ScriptBuilder.AddOp #%d (%s) unexpected "+ + "error: %v", i, test.name, e, + ) + continue + } + if !bytes.Equal(result, test.expected) { + t.Errorf("ScriptBuilder.AddOp #%d (%s) wrong result\n"+ + "got: %x\nwant: %x", i, test.name, result, + test.expected, + ) + continue + } + } + // Run tests and bulk add ops via AddOps. + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + builder.Reset() + result, e := builder.AddOps(test.opcodes).Script() + if e != nil { + t.Errorf("ScriptBuilder.AddOps #%d (%s) unexpected "+ + "error: %v", i, test.name, e, + ) + continue + } + if !bytes.Equal(result, test.expected) { + t.Errorf("ScriptBuilder.AddOps #%d (%s) wrong result\n"+ + "got: %x\nwant: %x", i, test.name, result, + test.expected, + ) + continue + } + } +} + +// TestScriptBuilderAddInt64 tests that pushing signed integers to a script via the ScriptBuilder API works as expected. +func TestScriptBuilderAddInt64(t *testing.T) { + t.Parallel() + tests := []struct { + name string + val int64 + expected []byte + }{ + {name: "push -1", val: -1, expected: []byte{OP_1NEGATE}}, + {name: "push small int 0", val: 0, expected: []byte{OP_0}}, + {name: "push small int 1", val: 1, expected: []byte{OP_1}}, + {name: "push small int 2", val: 2, expected: []byte{OP_2}}, + {name: "push small int 3", val: 3, expected: []byte{OP_3}}, + {name: "push small int 4", val: 4, expected: []byte{OP_4}}, + {name: "push small int 5", val: 5, expected: []byte{OP_5}}, + {name: "push small int 6", val: 6, expected: []byte{OP_6}}, + {name: "push small int 7", val: 7, expected: []byte{OP_7}}, + {name: "push small int 8", val: 8, expected: []byte{OP_8}}, + {name: "push small int 9", val: 9, expected: []byte{OP_9}}, + {name: "push small int 10", val: 10, expected: []byte{OP_10}}, + {name: "push small int 11", val: 11, expected: []byte{OP_11}}, + {name: "push small int 12", val: 12, expected: []byte{OP_12}}, + {name: "push small int 13", val: 13, expected: []byte{OP_13}}, + {name: "push small int 14", val: 14, expected: []byte{OP_14}}, + {name: "push small int 15", val: 15, expected: []byte{OP_15}}, + {name: "push small int 16", val: 16, expected: []byte{OP_16}}, + {name: "push 17", val: 17, expected: []byte{OP_DATA_1, 0x11}}, + {name: "push 65", val: 65, expected: []byte{OP_DATA_1, 0x41}}, + {name: "push 127", val: 127, expected: []byte{OP_DATA_1, 0x7f}}, + {name: "push 128", val: 128, expected: []byte{OP_DATA_2, 0x80, 0}}, + {name: "push 255", val: 255, expected: []byte{OP_DATA_2, 0xff, 0}}, + {name: "push 256", val: 256, expected: []byte{OP_DATA_2, 0, 0x01}}, + {name: "push 32767", val: 32767, expected: []byte{OP_DATA_2, 0xff, 0x7f}}, + {name: "push 32768", val: 32768, expected: []byte{OP_DATA_3, 0, 0x80, 0}}, + {name: "push -2", val: -2, expected: []byte{OP_DATA_1, 0x82}}, + {name: "push -3", val: -3, expected: []byte{OP_DATA_1, 0x83}}, + {name: "push -4", val: -4, expected: []byte{OP_DATA_1, 0x84}}, + {name: "push -5", val: -5, expected: []byte{OP_DATA_1, 0x85}}, + {name: "push -17", val: -17, expected: []byte{OP_DATA_1, 0x91}}, + {name: "push -65", val: -65, expected: []byte{OP_DATA_1, 0xc1}}, + {name: "push -127", val: -127, expected: []byte{OP_DATA_1, 0xff}}, + {name: "push -128", val: -128, expected: []byte{OP_DATA_2, 0x80, 0x80}}, + {name: "push -255", val: -255, expected: []byte{OP_DATA_2, 0xff, 0x80}}, + {name: "push -256", val: -256, expected: []byte{OP_DATA_2, 0x00, 0x81}}, + {name: "push -32767", val: -32767, expected: []byte{OP_DATA_2, 0xff, 0xff}}, + {name: "push -32768", val: -32768, expected: []byte{OP_DATA_3, 0x00, 0x80, 0x80}}, + } + builder := NewScriptBuilder() + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + builder.Reset().AddInt64(test.val) + result, e := builder.Script() + if e != nil { + t.Errorf("ScriptBuilder.AddInt64 #%d (%s) unexpected "+ + "error: %v", i, test.name, e, + ) + continue + } + if !bytes.Equal(result, test.expected) { + t.Errorf("ScriptBuilder.AddInt64 #%d (%s) wrong result\n"+ + "got: %x\nwant: %x", i, test.name, result, + test.expected, + ) + continue + } + } +} + +// TestScriptBuilderAddData tests that pushing data to a script via the ScriptBuilder API works as expected and conforms +// to BIP0062. +func TestScriptBuilderAddData(t *testing.T) { + t.Parallel() + tests := []struct { + name string + data []byte + expected []byte + useFull bool // use AddFullData instead of AddData. + }{ + // BIP0062: Pushing an empty byte sequence must use OP_0. + {name: "push empty byte sequence", data: nil, expected: []byte{OP_0}}, + {name: "push 1 byte 0x00", data: []byte{0x00}, expected: []byte{OP_0}}, + // BIP0062: Pushing a 1-byte sequence of byte 0x01 through 0x10 must use OP_n. + {name: "push 1 byte 0x01", data: []byte{0x01}, expected: []byte{OP_1}}, + {name: "push 1 byte 0x02", data: []byte{0x02}, expected: []byte{OP_2}}, + {name: "push 1 byte 0x03", data: []byte{0x03}, expected: []byte{OP_3}}, + {name: "push 1 byte 0x04", data: []byte{0x04}, expected: []byte{OP_4}}, + {name: "push 1 byte 0x05", data: []byte{0x05}, expected: []byte{OP_5}}, + {name: "push 1 byte 0x06", data: []byte{0x06}, expected: []byte{OP_6}}, + {name: "push 1 byte 0x07", data: []byte{0x07}, expected: []byte{OP_7}}, + {name: "push 1 byte 0x08", data: []byte{0x08}, expected: []byte{OP_8}}, + {name: "push 1 byte 0x09", data: []byte{0x09}, expected: []byte{OP_9}}, + {name: "push 1 byte 0x0a", data: []byte{0x0a}, expected: []byte{OP_10}}, + {name: "push 1 byte 0x0b", data: []byte{0x0b}, expected: []byte{OP_11}}, + {name: "push 1 byte 0x0c", data: []byte{0x0c}, expected: []byte{OP_12}}, + {name: "push 1 byte 0x0d", data: []byte{0x0d}, expected: []byte{OP_13}}, + {name: "push 1 byte 0x0e", data: []byte{0x0e}, expected: []byte{OP_14}}, + {name: "push 1 byte 0x0f", data: []byte{0x0f}, expected: []byte{OP_15}}, + {name: "push 1 byte 0x10", data: []byte{0x10}, expected: []byte{OP_16}}, + // BIP0062: Pushing the byte 0x81 must use OP_1NEGATE. + {name: "push 1 byte 0x81", data: []byte{0x81}, expected: []byte{OP_1NEGATE}}, + // BIP0062: Pushing any other byte sequence up to 75 bytes must use the normal data push (opcode byte n, with n + // the number of bytes, followed n bytes of data being pushed). + {name: "push 1 byte 0x11", data: []byte{0x11}, expected: []byte{OP_DATA_1, 0x11}}, + {name: "push 1 byte 0x80", data: []byte{0x80}, expected: []byte{OP_DATA_1, 0x80}}, + {name: "push 1 byte 0x82", data: []byte{0x82}, expected: []byte{OP_DATA_1, 0x82}}, + {name: "push 1 byte 0xff", data: []byte{0xff}, expected: []byte{OP_DATA_1, 0xff}}, + { + name: "push data len 17", + data: bytes.Repeat([]byte{0x49}, 17), + expected: append([]byte{OP_DATA_17}, bytes.Repeat([]byte{0x49}, 17)...), + }, + { + name: "push data len 75", + data: bytes.Repeat([]byte{0x49}, 75), + expected: append([]byte{OP_DATA_75}, bytes.Repeat([]byte{0x49}, 75)...), + }, + // BIP0062: Pushing 76 to 255 bytes must use OP_PUSHDATA1. + { + name: "push data len 76", + data: bytes.Repeat([]byte{0x49}, 76), + expected: append([]byte{OP_PUSHDATA1, 76}, bytes.Repeat([]byte{0x49}, 76)...), + }, + { + name: "push data len 255", + data: bytes.Repeat([]byte{0x49}, 255), + expected: append([]byte{OP_PUSHDATA1, 255}, bytes.Repeat([]byte{0x49}, 255)...), + }, + // BIP0062: Pushing 256 to 520 bytes must use OP_PUSHDATA2. + { + name: "push data len 256", + data: bytes.Repeat([]byte{0x49}, 256), + expected: append([]byte{OP_PUSHDATA2, 0, 1}, bytes.Repeat([]byte{0x49}, 256)...), + }, + { + name: "push data len 520", + data: bytes.Repeat([]byte{0x49}, 520), + expected: append([]byte{OP_PUSHDATA2, 0x08, 0x02}, bytes.Repeat([]byte{0x49}, 520)...), + }, + // BIP0062: OP_PUSHDATA4 can never be used, as pushes over 520 bytes are not allowed, and those below can be + // done using other operators. + { + name: "push data len 521", + data: bytes.Repeat([]byte{0x49}, 521), + expected: nil, + }, + { + name: "push data len 32767 (canonical)", + data: bytes.Repeat([]byte{0x49}, 32767), + expected: nil, + }, + { + name: "push data len 65536 (canonical)", + data: bytes.Repeat([]byte{0x49}, 65536), + expected: nil, + }, + // Additional tests for the PushFullData function that intentionally allows data pushes to exceed the limit for + // regression testing purposes. 3-byte data push via OP_PUSHDATA_2. + { + name: "push data len 32767 (non-canonical)", + data: bytes.Repeat([]byte{0x49}, 32767), + expected: append([]byte{OP_PUSHDATA2, 255, 127}, bytes.Repeat([]byte{0x49}, 32767)...), + useFull: true, + }, + // 5-byte data push via OP_PUSHDATA_4. + { + name: "push data len 65536 (non-canonical)", + data: bytes.Repeat([]byte{0x49}, 65536), + expected: append([]byte{OP_PUSHDATA4, 0, 0, 1, 0}, bytes.Repeat([]byte{0x49}, 65536)...), + useFull: true, + }, + } + builder := NewScriptBuilder() + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + if !test.useFull { + builder.Reset().AddData(test.data) + } else { + builder.Reset().AddFullData(test.data) + } + result, _ := builder.Script() + if !bytes.Equal(result, test.expected) { + t.Errorf("ScriptBuilder.AddData #%d (%s) wrong result\n"+ + "got: %x\nwant: %x", i, test.name, result, + test.expected, + ) + continue + } + } +} + +// TestExceedMaxScriptSize ensures that all of the functions that can be used to add data to a script don't allow the +// script to exceed the max allowed size. +func TestExceedMaxScriptSize(t *testing.T) { + t.Parallel() + // Start off by constructing a max size script. + builder := NewScriptBuilder() + builder.Reset().AddFullData(make([]byte, MaxScriptSize-3)) + origScript, e := builder.Script() + if e != nil { + t.Fatalf("Unexpected error for max size script: %v", e) + } + // Ensure adding data that would exceed the maximum size of the script does not add the data. + script, e := builder.AddData([]byte{0x00}).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatalf("ScriptBuilder.AddData allowed exceeding max script "+ + "size: %v", len(script), + ) + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddData unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + // Ensure adding an opcode that would exceed the maximum size of the script does not add the data. + builder.Reset().AddFullData(make([]byte, MaxScriptSize-3)) + script, e = builder.AddOp(OP_0).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatalf("ScriptBuilder.AddOp unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddOp unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + // Ensure adding an integer that would exceed the maximum size of the script does not add the data. + builder.Reset().AddFullData(make([]byte, MaxScriptSize-3)) + script, e = builder.AddInt64(0).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatalf("ScriptBuilder.AddInt64 unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddInt64 unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } +} + +// TestErroredScript ensures that all of the functions that can be used to add data to a script don't modify the script +// once an error has happened. +func TestErroredScript(t *testing.T) { + t.Parallel() + // Start off by constructing a near max size script that has enough space left to add each data type without an + // error and force an initial error condition. + builder := NewScriptBuilder() + builder.Reset().AddFullData(make([]byte, MaxScriptSize-8)) + origScript, e := builder.Script() + if e != nil { + t.Fatalf("ScriptBuilder.AddFullData unexpected error: %v", e) + } + script, e := builder.AddData([]byte{0x00, 0x00, 0x00, 0x00, 0x00}).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatalf("ScriptBuilder.AddData allowed exceeding max script "+ + "size: %v", len(script), + ) + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddData unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + // Ensure adding data, even using the non-canonical path, to a script that has errored doesn't succeed. + script, e = builder.AddFullData([]byte{0x00}).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatal("ScriptBuilder.AddFullData succeeded on errored script") + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddFullData unexpected modified "+ + "script - got len %d, want len %d", len(script), + len(origScript), + ) + } + // Ensure adding data to a script that has errored doesn't succeed. + script, e = builder.AddData([]byte{0x00}).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatal("ScriptBuilder.AddData succeeded on errored script") + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddData unexpected modified "+ + "script - got len %d, want len %d", len(script), + len(origScript), + ) + } + // Ensure adding an opcode to a script that has errored doesn't succeed. + script, e = builder.AddOp(OP_0).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatal("ScriptBuilder.AddOp succeeded on errored script") + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddOp unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + // Ensure adding an integer to a script that has errored doesn't succeed. + script, e = builder.AddInt64(0).Script() + if _, ok := e.(ErrScriptNotCanonical); !ok || e == nil { + t.Fatal("ScriptBuilder.AddInt64 succeeded on errored script") + } + if !bytes.Equal(script, origScript) { + t.Fatalf("ScriptBuilder.AddInt64 unexpected modified script - "+ + "got len %d, want len %d", len(script), len(origScript), + ) + } + // Ensure the error has a message set. + if e.Error() == "" { + t.Fatal("ErrScriptNotCanonical.ScriptError does not have any text") + } +} diff --git a/pkg/txscript/scriptnum.go b/pkg/txscript/scriptnum.go new file mode 100644 index 0000000..39c3bbf --- /dev/null +++ b/pkg/txscript/scriptnum.go @@ -0,0 +1,161 @@ +package txscript + +import ( + "fmt" +) + +const ( + maxInt32 = 1<<31 - 1 + minInt32 = -1 << 31 + // defaultScriptNumLen is the default number of bytes data being interpreted as an integer may be. + defaultScriptNumLen = 4 +) + +// scriptNum represents a numeric value used in the scripting engine with special handling to deal with the subtle +// semantics required by consensus. All numbers are stored on the data and alternate stacks encoded as little endian +// with a sign bit. All numeric opcodes such as OP_ADD, OP_SUB, and OP_MUL, are only allowed to operate on 4-byte +// integers in the range [-2^31 + 1, 2^31 - 1], however the results of numeric operations may overflow and remain valid +// so long as they are not used as inputs to other numeric operations or otherwise interpreted as an integer. For +// example, it is possible for OP_ADD to have 2^31 - 1 for its two operands resulting 2^32 - 2, which overflows, but is +// still pushed to the stack as the result of the addition. That value can then be used as input to OP_VERIFY which will +// succeed because the data is being interpreted as a boolean. However, if that same value were to be used as input to +// another numeric opcode, such as OP_SUB, it must fail. This type handles the aforementioned requirements by storing +// all numeric operation results as an int64 to handle overflow and provides the Hash method to get the serialized +// representation (including values that overflow). Then, whenever data is interpreted as an integer, it is converted to +// this type by using the makeScriptNum function which will return an error if the number is out of range or not +// minimally encoded depending on parameters. Since all numeric opcodes involve pulling data from the stack and +// interpreting it as an integer, it provides the required behavior. +type scriptNum int64 + +// checkMinimalDataEncoding returns whether or not the passed byte array adheres to the minimal encoding requirements. +func checkMinimalDataEncoding(v []byte) (e error) { + if len(v) == 0 { + return nil + } + // Chk that the number is encoded with the minimum possible number of bytes. If the most-significant-byte - + // excluding the sign bit - is zero then we're not minimal. Note how this test also rejects the negative-zero + // encoding, [0x80]. + if v[len(v)-1]&0x7f == 0 { + // One exception: if there's more than one byte and the most significant bit of the second-most-significant-byte + // is set it would conflict with the sign bit. An example of this case is +-255, which encode to 0xff00 and + // 0xff80 respectively. (big-endian). + if len(v) == 1 || v[len(v)-2]&0x80 == 0 { + str := fmt.Sprintf("numeric value encoded as %x is "+ + "not minimally encoded", v, + ) + return scriptError(ErrMinimalData, str) + } + } + return nil +} + +// Bytes returns the number serialized as a little endian with a sign bit. +// Example encodings: +// 127 -> [0x7f] +// -127 -> [0xff] +// 128 -> [0x80 0x00] +// -128 -> [0x80 0x80] +// 129 -> [0x81 0x00] +// -129 -> [0x81 0x80] +// 256 -> [0x00 0x01] +// -256 -> [0x00 0x81] +// 32767 -> [0xff 0x7f] +// -32767 -> [0xff 0xff] +// 32768 -> [0x00 0x80 0x00] +// -32768 -> [0x00 0x80 0x80] +func (sn scriptNum) Bytes() []byte { + n := sn + // Zero encodes as an empty byte slice. + if n == 0 { + return nil + } + // Take the absolute value and keep track of whether it was originally negative. + isNegative := n < 0 + if isNegative { + n = -n + } + // Encode to little endian. The maximum number of encoded bytes is 9 (8 bytes for max int64 plus a potential byte + // for sign extension). + result := make([]byte, 0, 9) + for n > 0 { + result = append(result, byte(n&0xff)) + n >>= 8 + } + // When the most significant byte already has the high bit set, an additional high byte is required to indicate + // whether the number is negative or positive. The additional byte is removed when converting back to an integral + // and its high bit is used to denote the sign. Otherwise, when the most significant byte does not already have the + // high bit set, use it to indicate the value is negative, if needed. + if result[len(result)-1]&0x80 != 0 { + extraByte := byte(0x00) + if isNegative { + extraByte = 0x80 + } + result = append(result, extraByte) + } else if isNegative { + result[len(result)-1] |= 0x80 + } + return result +} + +// Int32 returns the script number clamped to a valid int32. That is to say when the script number is higher than the +// max allowed int32, the max int32 value is returned and vice versa for the minimum value. Note that this behavior is +// different from a simple int32 cast because that truncates and the consensus rules dictate numbers which are directly +// cast to ints provide this behavior. In practice, for most opcodes, the number should never be out of range since it +// will have been created with makeScriptNum using the defaultScriptLen value, which rejects them. In case something in +// the future ends up calling this function against the result of some arithmetic, which IS allowed to be out of range +// before being reinterpreted as an integer, this will provide the correct behavior. +func (sn scriptNum) Int32() int32 { + if sn > maxInt32 { + return maxInt32 + } + if sn < minInt32 { + return minInt32 + } + return int32(sn) +} + +// makeScriptNum interprets the passed serialized bytes as an encoded integer and returns the result as a script number. +// Since the consensus rules dictate that serialized bytes interpreted as ints are only allowed to be in the range +// determined by a maximum number of bytes, on a per opcode basis, an error will be returned when the provided bytes +// would result in a number outside of that range. In particular, the range for the vast majority of opcodes dealing +// with numeric values are limited to 4 bytes and therefore will pass that value to this function resulting in an +// allowed range of [-2^31 + 1, 2^31 - 1]. The requireMinimal flag causes an error to be returned if additional checks +// on the encoding determine it is not represented with the smallest possible number of bytes or is the negative 0 +// encoding, [0x80]. For example, consider the number 127. It could be encoded as [0x7f], [0x7f 0x00], [0x7f 0x00 0x00 +// ...], etc. All forms except [0x7f] will return an error with requireMinimal enabled. The scriptNumLen is the maximum +// number of bytes the encoded value can be before an ErrStackNumberTooBig is returned. This effectively limits the +// range of allowed values. WARNING: Great care should be taken if passing a value larger than defaultScriptNumLen, +// which could lead to addition and multiplication overflows. See the Hash function documentation for example encodings. +func makeScriptNum(v []byte, requireMinimal bool, scriptNumLen int) (scriptNum, error) { + // Interpreting data requires that it is not larger than the the passed scriptNumLen value. + if len(v) > scriptNumLen { + str := fmt.Sprintf("numeric value encoded as %x is %d bytes "+ + "which exceeds the max allowed of %d", v, len(v), + scriptNumLen, + ) + return 0, scriptError(ErrNumberTooBig, str) + } + // Enforce minimal encoded if requested. + if requireMinimal { + if e := checkMinimalDataEncoding(v); E.Chk(e) { + return 0, e + } + } + // Zero is encoded as an empty byte slice. + if len(v) == 0 { + return 0, nil + } + // Decode from little endian. + var result int64 + for i, val := range v { + result |= int64(val) << uint8(8*i) + } + // When the most significant byte of the input bytes has the sign bit set, the result is negative. So, remove the + // sign bit from the result and make it negative. + if v[len(v)-1]&0x80 != 0 { + // The maximum length of v has already been determined to be 4 above, so uint8 is enough to cover the max possible shift value of 24. + result &= ^(int64(0x80) << uint8(8*(len(v)-1))) + return scriptNum(-result), nil + } + return scriptNum(result), nil +} diff --git a/pkg/txscript/scriptnum_test.go b/pkg/txscript/scriptnum_test.go new file mode 100644 index 0000000..c78d589 --- /dev/null +++ b/pkg/txscript/scriptnum_test.go @@ -0,0 +1,250 @@ +package txscript + +import ( + "bytes" + "encoding/hex" + "testing" +) + +// hexToBytes converts the passed hex string into bytes and will panic if there is an error. This is only provided for +// the hard-coded constants so errors in the source code can be detected. It will only (and must only) be called with +// hard-coded values. +func hexToBytes(s string) []byte { + b, e := hex.DecodeString(s) + if e != nil { + panic("invalid hex in source file: " + s) + } + return b +} + +// TestScriptNumBytes ensures that converting from integral script numbers to byte representations works as expected. +func TestScriptNumBytes(t *testing.T) { + t.Parallel() + tests := []struct { + num scriptNum + serialized []byte + }{ + {0, nil}, + {1, hexToBytes("01")}, + {-1, hexToBytes("81")}, + {127, hexToBytes("7f")}, + {-127, hexToBytes("ff")}, + {128, hexToBytes("8000")}, + {-128, hexToBytes("8080")}, + {129, hexToBytes("8100")}, + {-129, hexToBytes("8180")}, + {256, hexToBytes("0001")}, + {-256, hexToBytes("0081")}, + {32767, hexToBytes("ff7f")}, + {-32767, hexToBytes("ffff")}, + {32768, hexToBytes("008000")}, + {-32768, hexToBytes("008080")}, + {65535, hexToBytes("ffff00")}, + {-65535, hexToBytes("ffff80")}, + {524288, hexToBytes("000008")}, + {-524288, hexToBytes("000088")}, + {7340032, hexToBytes("000070")}, + {-7340032, hexToBytes("0000f0")}, + {8388608, hexToBytes("00008000")}, + {-8388608, hexToBytes("00008080")}, + {2147483647, hexToBytes("ffffff7f")}, + {-2147483647, hexToBytes("ffffffff")}, + // Values that are out of range for data that is interpreted as numbers, but are allowed as the result of + // numeric operations. + {2147483648, hexToBytes("0000008000")}, + {-2147483648, hexToBytes("0000008080")}, + {2415919104, hexToBytes("0000009000")}, + {-2415919104, hexToBytes("0000009080")}, + {4294967295, hexToBytes("ffffffff00")}, + {-4294967295, hexToBytes("ffffffff80")}, + {4294967296, hexToBytes("0000000001")}, + {-4294967296, hexToBytes("0000000081")}, + {281474976710655, hexToBytes("ffffffffffff00")}, + {-281474976710655, hexToBytes("ffffffffffff80")}, + {72057594037927935, hexToBytes("ffffffffffffff00")}, + {-72057594037927935, hexToBytes("ffffffffffffff80")}, + {9223372036854775807, hexToBytes("ffffffffffffff7f")}, + {-9223372036854775807, hexToBytes("ffffffffffffffff")}, + } + for _, test := range tests { + gotBytes := test.num.Bytes() + if !bytes.Equal(gotBytes, test.serialized) { + t.Errorf("Hash: did not get expected bytes for %d - "+ + "got %x, want %x", test.num, gotBytes, + test.serialized, + ) + continue + } + } +} + +// TestMakeScriptNum ensures that converting from byte representations to integral script numbers works as expected. +func TestMakeScriptNum(t *testing.T) { + t.Parallel() + // Errors used in the tests below defined here for convenience and to keep the horizontal test size shorter. + errNumTooBig := scriptError(ErrNumberTooBig, "") + errMinimalData := scriptError(ErrMinimalData, "") + tests := []struct { + serialized []byte + num scriptNum + numLen int + minimalEncoding bool + err error + }{ + // Minimal encoding must reject negative 0. + {hexToBytes("80"), 0, defaultScriptNumLen, true, errMinimalData}, + // Minimally encoded valid values with minimal encoding flag. Should not error and return expected integral + // number. + {nil, 0, defaultScriptNumLen, true, nil}, + {hexToBytes("01"), 1, defaultScriptNumLen, true, nil}, + {hexToBytes("81"), -1, defaultScriptNumLen, true, nil}, + {hexToBytes("7f"), 127, defaultScriptNumLen, true, nil}, + {hexToBytes("ff"), -127, defaultScriptNumLen, true, nil}, + {hexToBytes("8000"), 128, defaultScriptNumLen, true, nil}, + {hexToBytes("8080"), -128, defaultScriptNumLen, true, nil}, + {hexToBytes("8100"), 129, defaultScriptNumLen, true, nil}, + {hexToBytes("8180"), -129, defaultScriptNumLen, true, nil}, + {hexToBytes("0001"), 256, defaultScriptNumLen, true, nil}, + {hexToBytes("0081"), -256, defaultScriptNumLen, true, nil}, + {hexToBytes("ff7f"), 32767, defaultScriptNumLen, true, nil}, + {hexToBytes("ffff"), -32767, defaultScriptNumLen, true, nil}, + {hexToBytes("008000"), 32768, defaultScriptNumLen, true, nil}, + {hexToBytes("008080"), -32768, defaultScriptNumLen, true, nil}, + {hexToBytes("ffff00"), 65535, defaultScriptNumLen, true, nil}, + {hexToBytes("ffff80"), -65535, defaultScriptNumLen, true, nil}, + {hexToBytes("000008"), 524288, defaultScriptNumLen, true, nil}, + {hexToBytes("000088"), -524288, defaultScriptNumLen, true, nil}, + {hexToBytes("000070"), 7340032, defaultScriptNumLen, true, nil}, + {hexToBytes("0000f0"), -7340032, defaultScriptNumLen, true, nil}, + {hexToBytes("00008000"), 8388608, defaultScriptNumLen, true, nil}, + {hexToBytes("00008080"), -8388608, defaultScriptNumLen, true, nil}, + {hexToBytes("ffffff7f"), 2147483647, defaultScriptNumLen, true, nil}, + {hexToBytes("ffffffff"), -2147483647, defaultScriptNumLen, true, nil}, + {hexToBytes("ffffffff7f"), 549755813887, 5, true, nil}, + {hexToBytes("ffffffffff"), -549755813887, 5, true, nil}, + {hexToBytes("ffffffffffffff7f"), 9223372036854775807, 8, true, nil}, + {hexToBytes("ffffffffffffffff"), -9223372036854775807, 8, true, nil}, + {hexToBytes("ffffffffffffffff7f"), -1, 9, true, nil}, + {hexToBytes("ffffffffffffffffff"), 1, 9, true, nil}, + {hexToBytes("ffffffffffffffffff7f"), -1, 10, true, nil}, + {hexToBytes("ffffffffffffffffffff"), 1, 10, true, nil}, + // Minimally encoded values that are out of range for data that is interpreted as script numbers with the + // minimal encoding flag set. Should error and return 0. + {hexToBytes("0000008000"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000008080"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000009000"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000009080"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffff00"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffff80"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000000001"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("0000000081"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffff00"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffff80"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffff00"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffff80"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffff7f"), 0, defaultScriptNumLen, true, errNumTooBig}, + {hexToBytes("ffffffffffffffff"), 0, defaultScriptNumLen, true, errNumTooBig}, + // Non-minimally encoded, but otherwise valid values with minimal encoding flag. Should error and return 0. + {hexToBytes("00"), 0, defaultScriptNumLen, true, errMinimalData}, // 0 + {hexToBytes("0100"), 0, defaultScriptNumLen, true, errMinimalData}, // 1 + {hexToBytes("7f00"), 0, defaultScriptNumLen, true, errMinimalData}, // 127 + {hexToBytes("800000"), 0, defaultScriptNumLen, true, errMinimalData}, // 128 + {hexToBytes("810000"), 0, defaultScriptNumLen, true, errMinimalData}, // 129 + {hexToBytes("000100"), 0, defaultScriptNumLen, true, errMinimalData}, // 256 + {hexToBytes("ff7f00"), 0, defaultScriptNumLen, true, errMinimalData}, // 32767 + {hexToBytes("00800000"), 0, defaultScriptNumLen, true, errMinimalData}, // 32768 + {hexToBytes("ffff0000"), 0, defaultScriptNumLen, true, errMinimalData}, // 65535 + {hexToBytes("00000800"), 0, defaultScriptNumLen, true, errMinimalData}, // 524288 + {hexToBytes("00007000"), 0, defaultScriptNumLen, true, errMinimalData}, // 7340032 + {hexToBytes("0009000100"), 0, 5, true, errMinimalData}, // 16779520 + // Non-minimally encoded, but otherwise valid values without minimal encoding flag. Should not error and return + // expected integral number. + {hexToBytes("00"), 0, defaultScriptNumLen, false, nil}, + {hexToBytes("0100"), 1, defaultScriptNumLen, false, nil}, + {hexToBytes("7f00"), 127, defaultScriptNumLen, false, nil}, + {hexToBytes("800000"), 128, defaultScriptNumLen, false, nil}, + {hexToBytes("810000"), 129, defaultScriptNumLen, false, nil}, + {hexToBytes("000100"), 256, defaultScriptNumLen, false, nil}, + {hexToBytes("ff7f00"), 32767, defaultScriptNumLen, false, nil}, + {hexToBytes("00800000"), 32768, defaultScriptNumLen, false, nil}, + {hexToBytes("ffff0000"), 65535, defaultScriptNumLen, false, nil}, + {hexToBytes("00000800"), 524288, defaultScriptNumLen, false, nil}, + {hexToBytes("00007000"), 7340032, defaultScriptNumLen, false, nil}, + {hexToBytes("0009000100"), 16779520, 5, false, nil}, + } + for _, test := range tests { + // Ensure the error code is of the expected type and the error code matches the value specified in the test + // instance. + gotNum, e := makeScriptNum(test.serialized, test.minimalEncoding, + test.numLen, + ) + if e = tstCheckScriptError(e, test.err); e != nil { + t.Errorf("makeScriptNum(%#x): %v", test.serialized, e) + continue + } + if gotNum != test.num { + t.Errorf("makeScriptNum(%#x): did not get expected "+ + "number - got %d, want %d", test.serialized, + gotNum, test.num, + ) + continue + } + } +} + +// TestScriptNumInt32 ensures that the Int32 function on script number behaves as expected. +func TestScriptNumInt32(t *testing.T) { + t.Parallel() + tests := []struct { + in scriptNum + want int32 + }{ + // Values inside the valid int32 range are just the values themselves cast to an int32. + {0, 0}, + {1, 1}, + {-1, -1}, + {127, 127}, + {-127, -127}, + {128, 128}, + {-128, -128}, + {129, 129}, + {-129, -129}, + {256, 256}, + {-256, -256}, + {32767, 32767}, + {-32767, -32767}, + {32768, 32768}, + {-32768, -32768}, + {65535, 65535}, + {-65535, -65535}, + {524288, 524288}, + {-524288, -524288}, + {7340032, 7340032}, + {-7340032, -7340032}, + {8388608, 8388608}, + {-8388608, -8388608}, + {2147483647, 2147483647}, + {-2147483647, -2147483647}, + {-2147483648, -2147483648}, + // Values outside of the valid int32 range are limited to int32. + {2147483648, 2147483647}, + {-2147483649, -2147483648}, + {1152921504606846975, 2147483647}, + {-1152921504606846975, -2147483648}, + {2305843009213693951, 2147483647}, + {-2305843009213693951, -2147483648}, + {4611686018427387903, 2147483647}, + {-4611686018427387903, -2147483648}, + {9223372036854775807, 2147483647}, + {-9223372036854775808, -2147483648}, + } + for _, test := range tests { + got := test.in.Int32() + if got != test.want { + t.Errorf("Int32: did not get expected value for %d - "+ + "got %d, want %d", test.in, got, test.want, + ) + continue + } + } +} diff --git a/pkg/txscript/sigcache.go b/pkg/txscript/sigcache.go new file mode 100644 index 0000000..78eb958 --- /dev/null +++ b/pkg/txscript/sigcache.go @@ -0,0 +1,75 @@ +package txscript + +import ( + "sync" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/ecc" +) + +// sigCacheEntry represents an entry in the SigCache. Entries within the SigCache are keyed according to the sigHash of +// the signature. In the scenario of a cache-hit (according to the sigHash), an additional comparison of the signature, +// and public key will be executed in order to ensure a complete match. In the occasion that two sigHashes collide, the +// newer sigHash will simply overwrite the existing entry. +type sigCacheEntry struct { + sig *ecc.Signature + pubKey *ecc.PublicKey +} + +// SigCache implements an ECDSA signature verification cache with a randomized entry eviction policy. Only valid +// signatures will be added to the cache. The benefits of SigCache are two fold. Firstly, usage of SigCache mitigates a +// DoS attack wherein an attack causes a victim's client to hang due to worst-case behavior triggered while processing +// attacker crafted invalid transactions. A detailed description of the mitigated DoS attack can be found here: +// https://bitslog.wordpress.com/2013/01/23/fixed-bitcoin-vulnerability-explanation-why-the-signature-cache-is-a-dos-protection/. +// Secondly, usage of the SigCache introduces a signature verification optimization which speeds up the validation of +// transactions within a block, if they've already been seen and verified within the mempool. +type SigCache struct { + sync.RWMutex + validSigs map[chainhash.Hash]sigCacheEntry + maxEntries uint +} + +// NewSigCache creates and initializes a new instance of SigCache. Its sole parameter 'maxEntries' represents the +// maximum number of entries allowed to exist in the SigCache at any particular moment. Random entries are evicted make +// room for new entries that would cause the number of entries in the cache to exceed the max. +func NewSigCache(maxEntries uint) *SigCache { + return &SigCache{ + validSigs: make(map[chainhash.Hash]sigCacheEntry, maxEntries), + maxEntries: maxEntries, + } +} + +// Exists returns true if an existing entry of 'sig' over 'sigHash' for public key 'pubKey' is found within the +// SigCache. Otherwise, false is returned. NOTE: This function is safe for concurrent access. Readers won't be blocked +// unless there exists a writer, adding an entry to the SigCache. +func (s *SigCache) Exists(sigHash chainhash.Hash, sig *ecc.Signature, pubKey *ecc.PublicKey) bool { + s.RLock() + entry, ok := s.validSigs[sigHash] + s.RUnlock() + return ok && entry.pubKey.IsEqual(pubKey) && entry.sig.IsEqual(sig) +} + +// Add adds an entry for a signature over 'sigHash' under public key 'pubKey' to the signature cache. In the event that +// the SigCache is 'full', an existing entry is randomly chosen to be evicted in order to make space for the new entry. +// NOTE: This function is safe for concurrent access. Writers will block simultaneous readers until function execution +// has concluded. +func (s *SigCache) Add(sigHash chainhash.Hash, sig *ecc.Signature, pubKey *ecc.PublicKey) { + s.Lock() + defer s.Unlock() + if s.maxEntries <= 0 { + return + } + // If adding this new entry will put us over the max number of allowed entries, then evict an entry. + if uint(len(s.validSigs)+1) > s.maxEntries { + // Remove a random entry from the map. Relying on the random starting point of Go's map iteration. It's worth + // noting that the random iteration starting point is not 100% guaranteed by the spec, however most Go compilers + // support it. Ultimately, the iteration order isn't important here because in order to manipulate which items + // are evicted, an adversary would need to be able to execute preimage attacks on the hashing function in order + // to start eviction at a specific entry. + for sigEntry := range s.validSigs { + delete(s.validSigs, sigEntry) + break + } + } + s.validSigs[sigHash] = sigCacheEntry{sig, pubKey} +} diff --git a/pkg/txscript/sigcache_test.go b/pkg/txscript/sigcache_test.go new file mode 100644 index 0000000..83ad03c --- /dev/null +++ b/pkg/txscript/sigcache_test.go @@ -0,0 +1,121 @@ +package txscript + +import ( + "crypto/rand" + "testing" + + "github.com/p9c/p9/pkg/chainhash" + "github.com/p9c/p9/pkg/ecc" +) + +// genRandomSig returns a random message, a signature of the message under the public key and the public key. This +// function is used to generate randomized test data. +func genRandomSig() (*chainhash.Hash, *ecc.Signature, *ecc.PublicKey, error) { + privKey, e := ecc.NewPrivateKey(ecc.S256()) + if e != nil { + return nil, nil, nil, e + } + var msgHash chainhash.Hash + if _, e = rand.Read(msgHash[:]); E.Chk(e) { + return nil, nil, nil, e + } + sig, e := privKey.Sign(msgHash[:]) + if e != nil { + return nil, nil, nil, e + } + return &msgHash, sig, privKey.PubKey(), nil +} + +// TestSigCacheAddExists tests the ability to add, and later check the existence of a signature triplet in the signature +// cache. +func TestSigCacheAddExists(t *testing.T) { + sigCache := NewSigCache(200) + // Generate a random sigCache entry triplet. + msg1, sig1, key1, e := genRandomSig() + if e != nil { + t.Fatalf("unable to generate random signature test data") + } + // Add the triplet to the signature cache. + sigCache.Add(*msg1, sig1, key1) + // The previously added triplet should now be found within the sigcache. + sig1Copy, _ := ecc.ParseSignature(sig1.Serialize(), ecc.S256()) + key1Copy, _ := ecc.ParsePubKey(key1.SerializeCompressed(), ecc.S256()) + if !sigCache.Exists(*msg1, sig1Copy, key1Copy) { + t.Errorf("previously added item not found in signature cache") + } +} + +// TestSigCacheAddEvictEntry tests the eviction case where a new signature triplet is added to a full signature cache +// which should trigger randomized eviction, followed by adding the new element to the cache. +func TestSigCacheAddEvictEntry(t *testing.T) { + // Create a sigcache that can hold up to 100 entries. + sigCacheSize := uint(100) + sigCache := NewSigCache(sigCacheSize) + // Fill the sigcache up with some random sig triplets. + for i := uint(0); i < sigCacheSize; i++ { + msg, sig, key, e := genRandomSig() + if e != nil { + t.Fatalf("unable to generate random signature test data") + } + sigCache.Add(*msg, sig, key) + sigCopy, _ := ecc.ParseSignature(sig.Serialize(), ecc.S256()) + keyCopy, _ := ecc.ParsePubKey(key.SerializeCompressed(), ecc.S256()) + if !sigCache.Exists(*msg, sigCopy, keyCopy) { + t.Errorf("previously added item not found in signature" + + "cache", + ) + } + } + // The sigcache should now have sigCacheSize entries within it. + if uint(len(sigCache.validSigs)) != sigCacheSize { + t.Fatalf("sigcache should now have %v entries, instead it has %v", + sigCacheSize, len(sigCache.validSigs), + ) + } + // Add a new entry, this should cause eviction of a randomly chosen previous entry. + msgNew, sigNew, keyNew, e := genRandomSig() + if e != nil { + t.Fatalf("unable to generate random signature test data") + } + sigCache.Add(*msgNew, sigNew, keyNew) + // The sigcache should still have sigCache entries. + if uint(len(sigCache.validSigs)) != sigCacheSize { + t.Fatalf("sigcache should now have %v entries, instead it has %v", + sigCacheSize, len(sigCache.validSigs), + ) + } + // The entry added above should be found within the sigcache. + sigNewCopy, _ := ecc.ParseSignature(sigNew.Serialize(), ecc.S256()) + keyNewCopy, _ := ecc.ParsePubKey(keyNew.SerializeCompressed(), ecc.S256()) + if !sigCache.Exists(*msgNew, sigNewCopy, keyNewCopy) { + t.Fatalf("previously added item not found in signature cache") + } +} + +// TestSigCacheAddMaxEntriesZeroOrNegative tests that if a sigCache is created with a max size <= 0, then no entries are +// added to the sigcache at all. +func TestSigCacheAddMaxEntriesZeroOrNegative(t *testing.T) { + // Create a sigcache that can hold up to 0 entries. + sigCache := NewSigCache(0) + // Generate a random sigCache entry triplet. + msg1, sig1, key1, e := genRandomSig() + if e != nil { + t.Fatalf("unable to generate random signature test data") + } + // Add the triplet to the signature cache. + sigCache.Add(*msg1, sig1, key1) + // The generated triplet should not be found. + sig1Copy, _ := ecc.ParseSignature(sig1.Serialize(), ecc.S256()) + key1Copy, _ := ecc.ParsePubKey(key1.SerializeCompressed(), ecc.S256()) + if sigCache.Exists(*msg1, sig1Copy, key1Copy) { + t.Errorf("previously added signature found in sigcache, but" + + "shouldn't have been", + ) + } + // There shouldn't be any entries in the sigCache. + if len(sigCache.validSigs) != 0 { + t.Errorf("%v items found in sigcache, no items should have"+ + "been added", len(sigCache.validSigs), + ) + } +} diff --git a/pkg/txscript/sign.go b/pkg/txscript/sign.go new file mode 100644 index 0000000..bee3906 --- /dev/null +++ b/pkg/txscript/sign.go @@ -0,0 +1,429 @@ +package txscript + +import ( + "errors" + "fmt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + + "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/wire" +) + +// RawTxInWitnessSignature returns the serialized ECDSA signature for the input idx of the given transaction, with the +// hashType appended to it. This function is identical to RawTxInSignature, however the signature generated signs a new +// sighash digest defined in BIP0143. +func RawTxInWitnessSignature( + tx *wire.MsgTx, sigHashes *TxSigHashes, idx int, + amt int64, subScript []byte, hashType SigHashType, + key *ecc.PrivateKey, +) ([]byte, error) { + parsedScript, e := parseScript(subScript) + if e != nil { + return nil, fmt.Errorf("cannot parse output script: %v", e) + } + hash, e := calcWitnessSignatureHash( + parsedScript, sigHashes, hashType, tx, + idx, amt, + ) + if e != nil { + return nil, e + } + signature, e := key.Sign(hash) + if e != nil { + return nil, fmt.Errorf("cannot sign tx input: %s", e) + } + return append(signature.Serialize(), byte(hashType)), nil +} + +// WitnessSignature creates an input witness stack for tx to spend DUO sent from a previous output to the owner of +// privKey using the p2wkh script template. The passed transaction must contain all the inputs and outputs as dictated +// by the passed hashType. The signature generated observes the new transaction digest algorithm defined within BIP0143. +func WitnessSignature( + tx *wire.MsgTx, sigHashes *TxSigHashes, idx int, amt int64, + subscript []byte, hashType SigHashType, privKey *ecc.PrivateKey, + compress bool, +) (wire.TxWitness, error) { + sig, e := RawTxInWitnessSignature( + tx, sigHashes, idx, amt, subscript, + hashType, privKey, + ) + if e != nil { + return nil, e + } + pk := (*ecc.PublicKey)(&privKey.PublicKey) + var pkData []byte + if compress { + pkData = pk.SerializeCompressed() + } else { + pkData = pk.SerializeUncompressed() + } + // A witness script is actually a stack, so we return an array of byte slices here, rather than a single byte slice. + return wire.TxWitness{sig, pkData}, nil +} + +// RawTxInSignature returns the serialized ECDSA signature for the input idx of the given transaction, with hashType +// appended to it. +func RawTxInSignature( + tx *wire.MsgTx, idx int, subScript []byte, + hashType SigHashType, key *ecc.PrivateKey, +) ([]byte, error) { + hash, e := CalcSignatureHash(subScript, hashType, tx, idx) + if e != nil { + return nil, e + } + signature, e := key.Sign(hash) + if e != nil { + return nil, fmt.Errorf("cannot sign tx input: %s", e) + } + return append(signature.Serialize(), byte(hashType)), nil +} + +// SignatureScript creates an input signature script for tx to spend DUO sent from a previous output to the owner of +// privKey. tx must include all transaction inputs and outputs, however txin scripts are allowed to be filled or empty. +// The returned script is calculated to be used as the idx'th txin sigscript for tx. subscript is the PkScript of the +// previous output being used as the idx'th input. privKey is serialized in either a compressed or uncompressed format +// based on compress. This format must match the same format used to generate the payment address, or the script +// validation will fail. +func SignatureScript( + tx *wire.MsgTx, idx int, subscript []byte, hashType SigHashType, privKey *ecc.PrivateKey, + compress bool, +) ([]byte, error) { + sig, e := RawTxInSignature(tx, idx, subscript, hashType, privKey) + if e != nil { + return nil, e + } + pk := (*ecc.PublicKey)(&privKey.PublicKey) + var pkData []byte + if compress { + pkData = pk.SerializeCompressed() + } else { + pkData = pk.SerializeUncompressed() + } + return NewScriptBuilder().AddData(sig).AddData(pkData).Script() +} + +func p2pkSignatureScript( + tx *wire.MsgTx, idx int, subScript []byte, hashType SigHashType, privKey *ecc.PrivateKey, +) ([]byte, error) { + sig, e := RawTxInSignature(tx, idx, subScript, hashType, privKey) + if e != nil { + return nil, e + } + return NewScriptBuilder().AddData(sig).Script() +} + +// signMultiSig signs as many of the outputs in the provided multisig script as possible. It returns the generated +// script and a boolean if the script fulfils the contract (i.e. nrequired signatures are provided). Since it is +// arguably legal to not be able to sign any of the outputs, no error is returned. +func signMultiSig( + tx *wire.MsgTx, idx int, subScript []byte, hashType SigHashType, + addresses []btcaddr.Address, nRequired int, kdb KeyDB, +) (sig []byte, k bool) { + // We start with a single OP_FALSE to work around the (now standard) but in the reference implementation that causes + // a spurious pop at the end of OP_CHECKMULTISIG. + builder := NewScriptBuilder().AddOp(OP_FALSE) + signed := 0 + for _, addr := range addresses { + key, _, e := kdb.GetKey(addr) + if e != nil { + continue + } + sig, e = RawTxInSignature(tx, idx, subScript, hashType, key) + if e != nil { + continue + } + builder.AddData(sig) + signed++ + if signed == nRequired { + break + } + } + script, _ := builder.Script() + return script, signed == nRequired +} + +func sign( + chainParams *chaincfg.Params, tx *wire.MsgTx, idx int, + subScript []byte, hashType SigHashType, kdb KeyDB, sdb ScriptDB, +) ( + []byte, + ScriptClass, []btcaddr.Address, int, error, +) { + class, addresses, nrequired, e := ExtractPkScriptAddrs( + subScript, + chainParams, + ) + if e != nil { + return nil, NonStandardTy, nil, 0, e + } + switch class { + case PubKeyTy: + // look up key for address + var key *ecc.PrivateKey + key, _, e = kdb.GetKey(addresses[0]) + if e != nil { + E.Ln(e) + return nil, class, nil, 0, e + } + script, e := p2pkSignatureScript( + tx, idx, subScript, hashType, + key, + ) + if e != nil { + return nil, class, nil, 0, e + } + return script, class, addresses, nrequired, nil + case PubKeyHashTy: + // look up key for address + key, compressed, e := kdb.GetKey(addresses[0]) + if e != nil { + return nil, class, nil, 0, e + } + script, e := SignatureScript( + tx, idx, subScript, hashType, + key, compressed, + ) + if e != nil { + return nil, class, nil, 0, e + } + return script, class, addresses, nrequired, nil + case ScriptHashTy: + script, e := sdb.GetScript(addresses[0]) + if e != nil { + return nil, class, nil, 0, e + } + return script, class, addresses, nrequired, nil + case MultiSigTy: + script, _ := signMultiSig( + tx, idx, subScript, hashType, + addresses, nrequired, kdb, + ) + return script, class, addresses, nrequired, nil + case NullDataTy: + return nil, class, nil, 0, + errors.New("can't sign NULLDATA transactions") + default: + return nil, class, nil, 0, + errors.New("can't sign unknown transactions") + } +} + +// mergeScripts merges sigScript and prevScript assuming they are both partial solutions for pkScript spending output +// idx of tx. class, addresses and nrequired are the result of extracting the addresses from pkscript. The return value +// is the best effort merging of the two scripts. Calling this function with addresses, class and nrequired that do not +// match pkScript is an error and results in undefined behaviour. +func mergeScripts( + chainParams *chaincfg.Params, tx *wire.MsgTx, idx int, pkScript []byte, class ScriptClass, + addresses []btcaddr.Address, nRequired int, sigScript, prevScript []byte, +) []byte { + // TODO: the scripthash and multisig paths here are overly inefficient in that they will recompute already known data. + // some internal refactoring could probably make this avoid needless extra calculations. + switch class { + case ScriptHashTy: + // Remove the last push in the script and then recurse. this could be a lot less inefficient. + sigPops, e := parseScript(sigScript) + if e != nil || len(sigPops) == 0 { + return prevScript + } + prevPops, e := parseScript(prevScript) + if e != nil || len(prevPops) == 0 { + return sigScript + } + // assume that script in sigPops is the correct one, we just made it. + script := sigPops[len(sigPops)-1].data + // We already know this information somewhere up the stack. + var nrequired int + class, addresses, nrequired, _ = + ExtractPkScriptAddrs(script, chainParams) + // regenerate scripts. + sigScript, _ = unparseScript(sigPops) + prevScript, _ = unparseScript(prevPops) + // Merge + mergedScript := mergeScripts( + chainParams, tx, idx, script, + class, addresses, nrequired, sigScript, prevScript, + ) + // Reappend the script and return the result. + builder := NewScriptBuilder() + builder.AddOps(mergedScript) + builder.AddData(script) + finalScript, _ := builder.Script() + return finalScript + case MultiSigTy: + return mergeMultiSig( + tx, idx, addresses, nRequired, pkScript, + sigScript, prevScript, + ) + // It doesn't actually make sense to merge anything other than multiig and scripthash (because it could contain + // multisig). Everything else has either zero signature, can't be spent, or has a single signature which is either + // present or not. The other two cases are handled above. In the conflict case here we just assume the longest is + // correct (this matches behaviour of the reference implementation). + default: + if len(sigScript) > len(prevScript) { + return sigScript + } + return prevScript + } +} + +// mergeMultiSig combines the two signature scripts sigScript and prevScript that both provide signatures for pkScript +// in output idx of tx. addresses and nRequired should be the results from extracting the addresses from pkScript. Since +// this function is internal only we assume that the arguments have come from other functions internally and thus are +// all consistent with each other, behaviour is undefined if this contract is broken. +func mergeMultiSig( + tx *wire.MsgTx, idx int, addresses []btcaddr.Address, nRequired int, pkScript, sigScript, + prevScript []byte, +) []byte { + // This is an internal only function and we already parsed this script as ok for multisig (this is how we got here), + // so if this fails then all assumptions are broken and who knows which way is up? + pkPops, _ := parseScript(pkScript) + sigPops, e := parseScript(sigScript) + if e != nil || len(sigPops) == 0 { + return prevScript + } + prevPops, e := parseScript(prevScript) + if e != nil || len(prevPops) == 0 { + return sigScript + } + // Convenience function to avoid duplication. + extractSigs := func(pops []parsedOpcode, sigs [][]byte) [][]byte { + for _, pop := range pops { + if len(pop.data) != 0 { + sigs = append(sigs, pop.data) + } + } + return sigs + } + possibleSigs := make([][]byte, 0, len(sigPops)+len(prevPops)) + possibleSigs = extractSigs(sigPops, possibleSigs) + possibleSigs = extractSigs(prevPops, possibleSigs) + // Now we need to match the signatures to pubkeys, the only real way to do that is to try to verify them all and match + // it to the pubkey that verifies it. we then can go through the addresses in order to podbuild our script. Anything that + // doesn't parse or doesn't verify we throw away. + addrToSig := make(map[string][]byte) +sigLoop: + for _, sig := range possibleSigs { + // can't have a valid signature that doesn't at least have a hashtype, in practise it is even longer than this. but + // that'll be checked next. + if len(sig) < 1 { + continue + } + tSig := sig[:len(sig)-1] + hashType := SigHashType(sig[len(sig)-1]) + pSig, e := ecc.ParseDERSignature(tSig, ecc.S256()) + if e != nil { + continue + } + // We have to do this each round since hash types may vary between signatures and so the hash will vary. We can, + // however, assume no sigs etc are in the script since that would make the transaction nonstandard and thus not + // MultiSigTy, so we just need to hash the full thing. + hash := calcSignatureHash(pkPops, hashType, tx, idx) + for _, addr := range addresses { + // All multisig addresses should be pubkey addresses it is an error to call this internal function with bad input. + pkaddr := addr.(*btcaddr.PubKey) + pubKey := pkaddr.PubKey() + // If it matches we put it in the map. We only can take one signature per public key so if we already have one, we + // can throw this away. + if pSig.Verify(hash, pubKey) { + aStr := addr.EncodeAddress() + if _, ok := addrToSig[aStr]; !ok { + addrToSig[aStr] = sig + } + continue sigLoop + } + } + } + // Extra opcode to handle the extra arg consumed (due to previous bugs in the reference implementation). + builder := NewScriptBuilder().AddOp(OP_FALSE) + doneSigs := 0 + // This assumes that addresses are in the same order as in the script. + for _, addr := range addresses { + sig, ok := addrToSig[addr.EncodeAddress()] + if !ok { + continue + } + builder.AddData(sig) + doneSigs++ + if doneSigs == nRequired { + break + } + } + // padding for missing ones. + for i := doneSigs; i < nRequired; i++ { + builder.AddOp(OP_0) + } + script, _ := builder.Script() + return script +} + +// KeyDB is an interface type provided to SignTxOutput, it encapsulates any user state required to get the private keys +// for an address. +type KeyDB interface { + GetKey(btcaddr.Address) (*ecc.PrivateKey, bool, error) +} + +// KeyClosure implements KeyDB with a closure. +type KeyClosure func(btcaddr.Address) (*ecc.PrivateKey, bool, error) + +// GetKey implements KeyDB by returning the result of calling the closure. +func (kc KeyClosure) GetKey(address btcaddr.Address) ( + *ecc.PrivateKey, + bool, error, +) { + return kc(address) +} + +// ScriptDB is an interface type provided to SignTxOutput, it encapsulates any user state required to get the scripts +// for an pay-to-script-hash address. +type ScriptDB interface { + GetScript(btcaddr.Address) ([]byte, error) +} + +// ScriptClosure implements ScriptDB with a closure. +type ScriptClosure func(btcaddr.Address) ([]byte, error) + +// GetScript implements ScriptDB by returning the result of calling the closure. +func (sc ScriptClosure) GetScript(address btcaddr.Address) ([]byte, error) { + return sc(address) +} + +// SignTxOutput signs output idx of the given tx to resolve the script given in pkScript with a signature type of +// hashType. Any keys required will be looked up by calling getKey() with the string of the given address. Any +// pay-to-script-hash signatures will be similarly looked up by calling getScript. If previousScript is provided +// then the results in previousScript will be merged in a type-dependent manner with the newly generated signature +// script. +func SignTxOutput( + chainParams *chaincfg.Params, tx *wire.MsgTx, idx int, pkScript []byte, hashType SigHashType, + kdb KeyDB, sdb ScriptDB, previousScript []byte, +) ([]byte, error) { + sigScript, class, addresses, nrequired, e := + sign(chainParams, tx, idx, pkScript, hashType, kdb, sdb) + if e != nil { + E.Ln(e) + return nil, e + } + if class == ScriptHashTy { + // TODO keep the sub addressed and pass down to merge. + realSigScript, _, _, _, e := sign( + chainParams, tx, idx, + sigScript, hashType, kdb, sdb, + ) + if e != nil { + E.Ln(e) + return nil, e + } + // Append the p2sh script as the last push in the script. + builder := NewScriptBuilder() + builder.AddOps(realSigScript) + builder.AddData(sigScript) + sigScript, _ = builder.Script() + // TODO keep a copy of the script for merging. + } + // Merge scripts. with any previous data, if any. + mergedScript := mergeScripts( + chainParams, tx, idx, pkScript, class, + addresses, nrequired, sigScript, previousScript, + ) + return mergedScript, nil +} diff --git a/pkg/txscript/stack.go b/pkg/txscript/stack.go new file mode 100644 index 0000000..85f5245 --- /dev/null +++ b/pkg/txscript/stack.go @@ -0,0 +1,352 @@ +package txscript + +import ( + "encoding/hex" + "fmt" + "sync" +) + +// asBool gets the boolean value of the byte array. +func asBool(t []byte) bool { + for i := range t { + if t[i] != 0 { + // Negative 0 is also considered false. + if i == len(t)-1 && t[i] == 0x80 { + return false + } + return true + } + } + return false +} + +// fromBool converts a boolean into the appropriate byte array. +func fromBool(v bool) []byte { + if v { + return []byte{1} + } + return nil +} + +// stack represents a stack of immutable objects to be used with bitcoin scripts. Objects may be shared, therefore in +// usage if a value is to be changed it *must* be deep-copied first to avoid changing other values on the stack. +type stack struct { + stk [][]byte + stkMutex sync.Mutex + verifyMinimalData bool +} + +// Depth returns the number of items on the stack. +func (s *stack) Depth() int32 { + s.stkMutex.Lock() + defer s.stkMutex.Unlock() + return int32(len(s.stk)) +} + +// PushByteArray adds the given back array to the top of the stack. +// Stack transformation: [... x1 x2] -> [... x1 x2 data] +func (s *stack) PushByteArray(so []byte) { + s.stkMutex.Lock() + { + s.stk = append(s.stk, so) + s.stkMutex.Unlock() + } +} + +// PushInt converts the provided scriptNum to a suitable byte array then pushes it onto the top of the stack. Stack +// transformation: [... x1 x2] -> [... x1 x2 int] +func (s *stack) PushInt(val scriptNum) { + s.PushByteArray(val.Bytes()) +} + +// PushBool converts the provided boolean to a suitable byte array then pushes it onto the top of the stack. +// Stack transformation: [... x1 x2] -> [... x1 x2 bool] +func (s *stack) PushBool(val bool) { + s.PushByteArray(fromBool(val)) +} + +// PopByteArray pops the value off the top of the stack and returns it. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func (s *stack) PopByteArray() ([]byte, error) { + return s.nipN(0) +} + +// PopInt pops the value off the top of the stack, converts it into a script num, and returns it. The act of converting +// to a script num enforces the consensus rules imposed on data interpreted as numbers. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func (s *stack) PopInt() (scriptNum, error) { + so, e := s.PopByteArray() + if e != nil { + return 0, e + } + return makeScriptNum(so, s.verifyMinimalData, defaultScriptNumLen) +} + +// PopBool pops the value off the top of the stack, converts it into a bool, and returns it. +// +// Stack transformation: [... x1 x2 x3] -> [... x1 x2] +func (s *stack) PopBool() (bool, error) { + so, e := s.PopByteArray() + if e != nil { + return false, e + } + return asBool(so), nil +} + +// PeekByteArray returns the Nth item on the stack without removing it. +func (s *stack) PeekByteArray(idx int32) ([]byte, error) { + sz := int32(len(s.stk)) + if idx < 0 || idx >= sz { + str := fmt.Sprintf("index %d is invalid for stack size %d", idx, + sz, + ) + return nil, scriptError(ErrInvalidStackOperation, str) + } + return s.stk[sz-idx-1], nil +} + +// PeekInt returns the Nth item on the stack as a script num without removing it. The act of converting to a script num +// enforces the consensus rules imposed on data interpreted as numbers. +func (s *stack) PeekInt(idx int32) (scriptNum, error) { + so, e := s.PeekByteArray(idx) + if e != nil { + return 0, e + } + return makeScriptNum(so, s.verifyMinimalData, defaultScriptNumLen) +} + +// PeekBool returns the Nth item on the stack as a bool without removing it. +func (s *stack) PeekBool(idx int32) (bool, error) { + so, e := s.PeekByteArray(idx) + if e != nil { + return false, e + } + return asBool(so), nil +} + +// nipN is an internal function that removes the nth item on the stack and returns it. +// +// Stack transformation: +// nipN(0): [... x1 x2 x3] -> [... x1 x2] +// nipN(1): [... x1 x2 x3] -> [... x1 x3] +// nipN(2): [... x1 x2 x3] -> [... x2 x3] +func (s *stack) nipN(idx int32) ([]byte, error) { + s.stkMutex.Lock() + { + sz := int32(len(s.stk)) + if idx < 0 || idx > sz-1 { + str := fmt.Sprintf("index %d is invalid for stack size %d", idx, + sz, + ) + return nil, scriptError(ErrInvalidStackOperation, str) + } + so := s.stk[sz-idx-1] + if idx == 0 { + s.stk = s.stk[:sz-1] + } else if idx == sz-1 { + s1 := make([][]byte, sz-1) + copy(s1, s.stk[1:]) + s.stk = s1 + } else { + s1 := s.stk[sz-idx : sz] + s.stk = s.stk[:sz-idx-1] + s.stk = append(s.stk, s1...) + } + s.stkMutex.Unlock() + return so, nil + } +} + +// NipN removes the Nth object on the stack +// +// Stack transformation: +// +// NipN(0): [... x1 x2 x3] -> [... x1 x2] +// NipN(1): [... x1 x2 x3] -> [... x1 x3] +// NipN(2): [... x1 x2 x3] -> [... x2 x3] +func (s *stack) NipN(idx int32) (e error) { + _, e = s.nipN(idx) + return e +} + +// Tuck copies the item at the top of the stack and inserts it before the 2nd to top item. +// +// Stack transformation: [... x1 x2] -> [... x2 x1 x2] +func (s *stack) Tuck() (e error) { + so2, e := s.PopByteArray() + if e != nil { + return e + } + so1, e := s.PopByteArray() + if e != nil { + return e + } + s.PushByteArray(so2) // stack [... x2] + s.PushByteArray(so1) // stack [... x2 x1] + s.PushByteArray(so2) // stack [... x2 x1 x2] + return nil +} + +// DropN removes the top N items from the stack. +// +// Stack transformation: +// +// DropN(1): [... x1 x2] -> [... x1] +// DropN(2): [... x1 x2] -> [...] +func (s *stack) DropN(n int32) (e error) { + if n < 1 { + str := fmt.Sprintf("attempt to drop %d items from stack", n) + return scriptError(ErrInvalidStackOperation, str) + } + for ; n > 0; n-- { + _, e := s.PopByteArray() + if e != nil { + return e + } + } + return nil +} + +// DupN duplicates the top N items on the stack. +// +// Stack transformation: +// +// DupN(1): [... x1 x2] -> [... x1 x2 x2] +// DupN(2): [... x1 x2] -> [... x1 x2 x1 x2] +func (s *stack) DupN(n int32) (e error) { + if n < 1 { + str := fmt.Sprintf("attempt to dup %d stack items", n) + return scriptError(ErrInvalidStackOperation, str) + } + // Iteratively duplicate the value n-1 down the stack n times. This leaves an in-order duplicate of the top n items + // on the stack. + for i := n; i > 0; i-- { + so, e := s.PeekByteArray(n - 1) + if e != nil { + return e + } + s.PushByteArray(so) + } + return nil +} + +// RotN rotates the top 3N items on the stack to the left N times. +// +// Stack transformation: +// +// RotN(1): [... x1 x2 x3] -> [... x2 x3 x1] +// RotN(2): [... x1 x2 x3 x4 x5 x6] -> [... x3 x4 x5 x6 x1 x2] +func (s *stack) RotN(n int32) (e error) { + if n < 1 { + str := fmt.Sprintf("attempt to rotate %d stack items", n) + return scriptError(ErrInvalidStackOperation, str) + } + // Nip the 3n-1th item from the stack to the top n times to rotate them up to the head of the stack. + entry := 3*n - 1 + for i := n; i > 0; i-- { + so, e := s.nipN(entry) + if e != nil { + return e + } + s.PushByteArray(so) + } + return nil +} + +// SwapN swaps the top N items on the stack with those below them. +// +// Stack transformation: +// +// SwapN(1): [... x1 x2] -> [... x2 x1] +// SwapN(2): [... x1 x2 x3 x4] -> [... x3 x4 x1 x2] +func (s *stack) SwapN(n int32) (e error) { + if n < 1 { + str := fmt.Sprintf("attempt to swap %d stack items", n) + return scriptError(ErrInvalidStackOperation, str) + } + entry := 2*n - 1 + for i := n; i > 0; i-- { + // Swap 2n-1th entry to top. + so, e := s.nipN(entry) + if e != nil { + return e + } + s.PushByteArray(so) + } + return nil +} + +// OverN copies N items N items back to the top of the stack. +// +// Stack transformation: +// +// OverN(1): [... x1 x2 x3] -> [... x1 x2 x3 x2] +// OverN(2): [... x1 x2 x3 x4] -> [... x1 x2 x3 x4 x1 x2] +func (s *stack) OverN(n int32) (e error) { + if n < 1 { + str := fmt.Sprintf("attempt to perform over on %d stack items", + n, + ) + return scriptError(ErrInvalidStackOperation, str) + } + // Copy 2n-1th entry to top of the stack. + entry := 2*n - 1 + for ; n > 0; n-- { + so, e := s.PeekByteArray(entry) + if e != nil { + return e + } + s.PushByteArray(so) + } + return nil +} + +// PickN copies the item N items back in the stack to the top. +// +// Stack transformation: +// +// PickN(0): [x1 x2 x3] -> [x1 x2 x3 x3] +// PickN(1): [x1 x2 x3] -> [x1 x2 x3 x2] +// PickN(2): [x1 x2 x3] -> [x1 x2 x3 x1] +func (s *stack) PickN(n int32) (e error) { + so, e := s.PeekByteArray(n) + if e != nil { + return e + } + s.PushByteArray(so) + return nil +} + +// RollN moves the item N items back in the stack to the top. +// +// Stack transformation: +// +// RollN(0): [x1 x2 x3] -> [x1 x2 x3] +// RollN(1): [x1 x2 x3] -> [x1 x3 x2] +// RollN(2): [x1 x2 x3] -> [x2 x3 x1] +func (s *stack) RollN(n int32) (e error) { + so, e := s.nipN(n) + if e != nil { + return e + } + s.PushByteArray(so) + return nil +} + +// String returns the stack in a readable format. +func (s *stack) String() string { + s.stkMutex.Lock() + { + var result string + for _, stack := range s.stk { + if len(stack) == 0 { + result += "00000000 \n" + } + result += hex.Dump(stack) + } + s.stkMutex.Unlock() + return result + } +} diff --git a/pkg/txscript/stack_test.go b/pkg/txscript/stack_test.go new file mode 100644 index 0000000..582d47f --- /dev/null +++ b/pkg/txscript/stack_test.go @@ -0,0 +1,923 @@ +package txscript + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "testing" +) + +// tstCheckScriptError ensures the type of the two passed errors are of the same type (either both nil or both of type +// ScriptError) and their error codes match when not nil. +func tstCheckScriptError(gotErr, wantErr error) (e error) { + // Ensure the error code is of the expected type and the error code matches the value specified in the test + // instance. + if reflect.TypeOf(gotErr) != reflect.TypeOf(wantErr) { + return fmt.Errorf("wrong error - got %T (%[1]v), want %T", + gotErr, wantErr, + ) + } + if gotErr == nil { + return nil + } + // Ensure the want error type is a script error. + werr, ok := wantErr.(ScriptError) + if !ok { + return fmt.Errorf("unexpected test error type %T", wantErr) + } + // Ensure the error codes match. It's safe to use a raw type assert here since the code above already proved they + // are the same type and the want error is a script error. + gotErrorCode := gotErr.(ScriptError).ErrorCode + if gotErrorCode != werr.ErrorCode { + return fmt.Errorf("mismatched error code - got %v (%v), want %v", + gotErrorCode, gotErr, werr.ErrorCode, + ) + } + return nil +} + +// TestStack tests that all of the stack operations work as expected. +func TestStack(t *testing.T) { + t.Parallel() + tests := []struct { + name string + before [][]byte + operation func(*stack) error + err error + after [][]byte + }{ + { + "noop", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + return nil + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {5}}, + }, + { + "peek underflow (byte)", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + _, e = s.PeekByteArray(5) + return e + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "peek underflow (int)", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + _, e = s.PeekInt(5) + return e + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "peek underflow (bool)", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + _, e = s.PeekBool(5) + return e + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "pop", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + val, e := s.PopByteArray() + if e != nil { + return e + } + if !bytes.Equal(val, []byte{5}) { + return errors.New("not equal") + } + return e + }, + nil, + [][]byte{{1}, {2}, {3}, {4}}, + }, + { + "pop everything", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + for i := 0; i < 5; i++ { + _, e := s.PopByteArray() + if e != nil { + return e + } + } + return nil + }, + nil, + nil, + }, + { + "pop underflow", + [][]byte{{1}, {2}, {3}, {4}, {5}}, + func(s *stack) (e error) { + for i := 0; i < 6; i++ { + _, e := s.PopByteArray() + if e != nil { + return e + } + } + return nil + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "pop bool", + [][]byte{nil}, + func(s *stack) (e error) { + val, e := s.PopBool() + if e != nil { + return e + } + if val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "pop bool", + [][]byte{{1}}, + func(s *stack) (e error) { + val, e := s.PopBool() + if e != nil { + return e + } + if !val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "pop bool", + nil, + func(s *stack) (e error) { + _, e = s.PopBool() + return e + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "popInt 0", + [][]byte{{0x0}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != 0 { + return errors.New("0 != 0 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt -0", + [][]byte{{0x80}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != 0 { + return errors.New("-0 != 0 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt 1", + [][]byte{{0x01}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != 1 { + return errors.New("1 != 1 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt 1 leading 0", + [][]byte{{0x01, 0x00, 0x00, 0x00}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != 1 { + fmt.Printf("%v != %v\n", v, 1) + return errors.New("1 != 1 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt -1", + [][]byte{{0x81}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != -1 { + return errors.New("-1 != -1 on popInt") + } + return nil + }, + nil, + nil, + }, + { + "popInt -1 leading 0", + [][]byte{{0x01, 0x00, 0x00, 0x80}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != -1 { + fmt.Printf("%v != %v\n", v, -1) + return errors.New("-1 != -1 on popInt") + } + return nil + }, + nil, + nil, + }, + // Triggers the multibyte case in asInt + { + "popInt -513", + [][]byte{{0x1, 0x82}}, + func(s *stack) (e error) { + v, e := s.PopInt() + if e != nil { + return e + } + if v != -513 { + fmt.Printf("%v != %v\n", v, -513) + return errors.New("1 != 1 on popInt") + } + return nil + }, + nil, + nil, + }, + // Confirm that the asInt code doesn't modify the base data. + { + "peekint nomodify -1", + [][]byte{{0x01, 0x00, 0x00, 0x80}}, + func(s *stack) (e error) { + v, e := s.PeekInt(0) + if e != nil { + return e + } + if v != -1 { + fmt.Printf("%v != %v\n", v, -1) + return errors.New("-1 != -1 on popInt") + } + return nil + }, + nil, + [][]byte{{0x01, 0x00, 0x00, 0x80}}, + }, + { + "PushInt 0", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(0)) + return nil + }, + nil, + [][]byte{{}}, + }, + { + "PushInt 1", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(1)) + return nil + }, + nil, + [][]byte{{0x1}}, + }, + { + "PushInt -1", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(-1)) + return nil + }, + nil, + [][]byte{{0x81}}, + }, + { + "PushInt two bytes", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(256)) + return nil + }, + nil, + // little endian.. *sigh* + [][]byte{{0x00, 0x01}}, + }, + { + "PushInt leading zeros", + nil, + func(s *stack) (e error) { + // this will have the highbit set + s.PushInt(scriptNum(128)) + return nil + }, + nil, + [][]byte{{0x80, 0x00}}, + }, + { + "dup", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.DupN(1) + }, + nil, + [][]byte{{1}, {1}}, + }, + { + "dup2", + [][]byte{{1}, {2}}, + func(s *stack) (e error) { + return s.DupN(2) + }, + nil, + [][]byte{{1}, {2}, {1}, {2}}, + }, + { + "dup3", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.DupN(3) + }, + nil, + [][]byte{{1}, {2}, {3}, {1}, {2}, {3}}, + }, + { + "dup0", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.DupN(0) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "dup-1", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.DupN(-1) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "dup too much", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.DupN(2) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "PushBool true", + nil, + func(s *stack) (e error) { + s.PushBool(true) + return nil + }, + nil, + [][]byte{{1}}, + }, + { + "PushBool false", + nil, + func(s *stack) (e error) { + s.PushBool(false) + return nil + }, + nil, + [][]byte{nil}, + }, + { + "PushBool PopBool", + nil, + func(s *stack) (e error) { + s.PushBool(true) + val, e := s.PopBool() + if e != nil { + return e + } + if !val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "PushBool PopBool 2", + nil, + func(s *stack) (e error) { + s.PushBool(false) + val, e := s.PopBool() + if e != nil { + return e + } + if val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "PushInt PopBool", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(1)) + val, e := s.PopBool() + if e != nil { + return e + } + if !val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "PushInt PopBool 2", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(0)) + val, e := s.PopBool() + if e != nil { + return e + } + if val { + return errors.New("unexpected value") + } + return nil + }, + nil, + nil, + }, + { + "Nip top", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.NipN(0) + }, + nil, + [][]byte{{1}, {2}}, + }, + { + "Nip middle", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.NipN(1) + }, + nil, + [][]byte{{1}, {3}}, + }, + { + "Nip low", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.NipN(2) + }, + nil, + [][]byte{{2}, {3}}, + }, + { + "Nip too much", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + // bite off more than we can chew + return s.NipN(3) + }, + scriptError(ErrInvalidStackOperation, ""), + [][]byte{{2}, {3}}, + }, + { + "keep on tucking", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.Tuck() + }, + nil, + [][]byte{{1}, {3}, {2}, {3}}, + }, + { + "a little tucked up", + [][]byte{{1}}, // too few arguments for tuck + func(s *stack) (e error) { + return s.Tuck() + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "all tucked up", + nil, // too few arguments for tuck + func(s *stack) (e error) { + return s.Tuck() + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "drop 1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.DropN(1) + }, + nil, + [][]byte{{1}, {2}, {3}}, + }, + { + "drop 2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.DropN(2) + }, + nil, + [][]byte{{1}, {2}}, + }, + { + "drop 3", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.DropN(3) + }, + nil, + [][]byte{{1}}, + }, + { + "drop 4", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.DropN(4) + }, + nil, + nil, + }, + { + "drop 4/5", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.DropN(5) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "drop invalid", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.DropN(0) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Rot1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.RotN(1) + }, + nil, + [][]byte{{1}, {3}, {4}, {2}}, + }, + { + "Rot2", + [][]byte{{1}, {2}, {3}, {4}, {5}, {6}}, + func(s *stack) (e error) { + return s.RotN(2) + }, + nil, + [][]byte{{3}, {4}, {5}, {6}, {1}, {2}}, + }, + { + "Rot too little", + [][]byte{{1}, {2}}, + func(s *stack) (e error) { + return s.RotN(1) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Rot0", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.RotN(0) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Swap1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.SwapN(1) + }, + nil, + [][]byte{{1}, {2}, {4}, {3}}, + }, + { + "Swap2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.SwapN(2) + }, + nil, + [][]byte{{3}, {4}, {1}, {2}}, + }, + { + "Swap too little", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.SwapN(1) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Swap0", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.SwapN(0) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Over1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.OverN(1) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {3}}, + }, + { + "Over2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.OverN(2) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {1}, {2}}, + }, + { + "Over too little", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.OverN(1) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Over0", + [][]byte{{1}, {2}, {3}}, + func(s *stack) (e error) { + return s.OverN(0) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Pick1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.PickN(1) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {3}}, + }, + { + "Pick2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.PickN(2) + }, + nil, + [][]byte{{1}, {2}, {3}, {4}, {2}}, + }, + { + "Pick too little", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.PickN(1) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Roll1", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.RollN(1) + }, + nil, + [][]byte{{1}, {2}, {4}, {3}}, + }, + { + "Roll2", + [][]byte{{1}, {2}, {3}, {4}}, + func(s *stack) (e error) { + return s.RollN(2) + }, + nil, + [][]byte{{1}, {3}, {4}, {2}}, + }, + { + "Roll too little", + [][]byte{{1}}, + func(s *stack) (e error) { + return s.RollN(1) + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + { + "Peek bool", + [][]byte{{1}}, + func(s *stack) (e error) { + // Peek bool is otherwise pretty well tested, just check it works. + val, e := s.PeekBool(0) + if e != nil { + return e + } + if !val { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{{1}}, + }, + { + "Peek bool 2", + [][]byte{nil}, + func(s *stack) (e error) { + // Peek bool is otherwise pretty well tested, just check it works. + val, e := s.PeekBool(0) + if e != nil { + return e + } + if val { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{nil}, + }, + { + "Peek int", + [][]byte{{1}}, + func(s *stack) (e error) { + // Peek int is otherwise pretty well tested, just check it works. + val, e := s.PeekInt(0) + if e != nil { + return e + } + if val != 1 { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{{1}}, + }, + { + "Peek int 2", + [][]byte{{0}}, + func(s *stack) (e error) { + // Peek int is otherwise pretty well tested, just check it works. + val, e := s.PeekInt(0) + if e != nil { + return e + } + if val != 0 { + return errors.New("invalid result") + } + return nil + }, + nil, + [][]byte{{0}}, + }, + { + "pop int", + nil, + func(s *stack) (e error) { + s.PushInt(scriptNum(1)) + // Peek int is otherwise pretty well tested, just check it works. + val, e := s.PopInt() + if e != nil { + return e + } + if val != 1 { + return errors.New("invalid result") + } + return nil + }, + nil, + nil, + }, + { + "pop empty", + nil, + func(s *stack) (e error) { + // Peek int is otherwise pretty well tested, just check it works. + _, e = s.PopInt() + return e + }, + scriptError(ErrInvalidStackOperation, ""), + nil, + }, + } + var e error + for _, test := range tests { + // Setup the initial stack state and perform the test operation. + s := stack{} + for i := range test.before { + s.PushByteArray(test.before[i]) + } + e = test.operation(&s) + // Ensure the error code is of the expected type and the error code matches the value specified in the test instance. + if e = tstCheckScriptError(e, test.err); e != nil { + t.Errorf("%s: %v", test.name, e) + continue + } + // Ensure the resulting stack is the expected length. + if int32(len(test.after)) != s.Depth() { + t.Errorf("%s: stack depth doesn't match expected: %v "+ + "vs %v", test.name, len(test.after), + s.Depth(), + ) + continue + } + // Ensure all items of the resulting stack are the expected values. + for i := range test.after { + var val []byte + val, e = s.PeekByteArray(s.Depth() - int32(i) - 1) + if e != nil { + t.Errorf("%s: can't peek %dth stack entry: %v", + test.name, i, e, + ) + break + } + if !bytes.Equal(val, test.after[i]) { + t.Errorf("%s: %dth stack entry doesn't match "+ + "expected: %v vs %v", test.name, i, val, + test.after[i], + ) + break + } + } + } +} diff --git a/pkg/txscript/standard.go b/pkg/txscript/standard.go new file mode 100644 index 0000000..dffa4b0 --- /dev/null +++ b/pkg/txscript/standard.go @@ -0,0 +1,639 @@ +package txscript + +import ( + "fmt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" +) + +// ScriptClass is an enumeration for the list of standard types of script. +type ScriptClass byte + +// A bunch of constants +const ( + // MaxDataCarrierSize is the maximum number of bytes allowed in pushed data to + // be considered a nulldata transaction + MaxDataCarrierSize = 80 + // StandardVerifyFlags are the script flags which are used when executing + // transaction scripts to enforce additional checks which are required for the + // script to be considered standard. These checks help reduce issues related to + // transaction malleability as well as allow pay-to-script hash transactions. + // Note these flags are different than what is required for the consensus rules + // in that they are more strict. + // TODO: This definition does not belong here. It belongs in a policy package. + StandardVerifyFlags = ScriptBip16 | + ScriptVerifyDERSignatures | + ScriptVerifyStrictEncoding | + ScriptVerifyMinimalData | + ScriptStrictMultiSig | + ScriptDiscourageUpgradableNops | + ScriptVerifyCleanStack | + ScriptVerifyNullFail | + ScriptVerifyCheckLockTimeVerify | + // ScriptVerifyCheckSequenceVerify | + ScriptVerifyLowS | + ScriptStrictMultiSig | + // ScriptVerifyWitness | + // ScriptVerifyDiscourageUpgradeableWitnessProgram | + ScriptVerifyMinimalIf + // ScriptVerifyWitnessPubKeyType + + // Classes of script payment known about in the blockchain. + + NonStandardTy ScriptClass = iota // None of the recognized forms. + PubKeyTy // Pay pubkey. + PubKeyHashTy // Pay pubkey hash. + // WitnessV0PubKeyHashTy // Pay witness pubkey hash. + ScriptHashTy // Pay to script hash. + // WitnessV0ScriptHashTy // Pay to witness script hash. + MultiSigTy // Multi signature. + NullDataTy // Empty data-only (provably prunable). +) + +// scriptClassToName houses the human-readable strings which describe each +// script class. +var scriptClassToName = []string{ + NonStandardTy: "nonstandard", + PubKeyTy: "pubkey", + PubKeyHashTy: "pubkeyhash", + // WitnessV0PubKeyHashTy: "witness_v0_keyhash", + ScriptHashTy: "scripthash", + // WitnessV0ScriptHashTy: "witness_v0_scripthash", + MultiSigTy: "multisig", + NullDataTy: "nulldata", +} + +// String implements the Stringer interface by returning the name of the enum +// script class. If the enum is invalid then "Invalid" will be returned. +func (t ScriptClass) String() string { + if int(t) > len(scriptClassToName) || int(t) < 0 { + return "Invalid" + } + return scriptClassToName[t] +} + +// isPubkey returns true if the script passed is a pay-to-pubkey transaction, +// false otherwise. +func isPubkey(pops []parsedOpcode) bool { + // Valid pubkeys are either 33 or 65 bytes. + return len(pops) == 2 && + (len(pops[0].data) == 33 || len(pops[0].data) == 65) && + pops[1].opcode.value == OP_CHECKSIG +} + +// isPubkeyHash returns true if the script passed is a pay-to-pubkey-hash +// transaction, false otherwise. +func isPubkeyHash(pops []parsedOpcode) bool { + return len(pops) == 5 && + pops[0].opcode.value == OP_DUP && + pops[1].opcode.value == OP_HASH160 && + pops[2].opcode.value == OP_DATA_20 && + pops[3].opcode.value == OP_EQUALVERIFY && + pops[4].opcode.value == OP_CHECKSIG +} + +// isMultiSig returns true if the passed script is a multisig transaction, false +// otherwise. +func isMultiSig(pops []parsedOpcode) bool { + // The absolute minimum is 1 pubkey: + // OP_0/OP_1-16 OP_1 OP_CHECKMULTISIG + l := len(pops) + if l < 4 { + return false + } + if !isSmallInt(pops[0].opcode) { + return false + } + if !isSmallInt(pops[l-2].opcode) { + return false + } + if pops[l-1].opcode.value != OP_CHECKMULTISIG { + return false + } + // Verify the number of pubkeys specified matches the actual number of pubkeys provided. + if l-2-1 != asSmallInt(pops[l-2].opcode) { + return false + } + for _, pop := range pops[1 : l-2] { + // Valid pubkeys are either 33 or 65 bytes. + if len(pop.data) != 33 && len(pop.data) != 65 { + return false + } + } + return true +} + +// isNullData returns true if the passed script is a null data transaction, +// false otherwise. +func isNullData(pops []parsedOpcode) bool { + // A nulldata transaction is either a single OP_RETURN or an OP_RETURN SMALLDATA + // (where SMALLDATA is a data push up to MaxDataCarrierSize bytes). + l := len(pops) + if l == 1 && pops[0].opcode.value == OP_RETURN { + return true + } + return l == 2 && + pops[0].opcode.value == OP_RETURN && + (isSmallInt(pops[1].opcode) || pops[1].opcode.value <= + OP_PUSHDATA4) && + len(pops[1].data) <= MaxDataCarrierSize +} + +// scriptType returns the type of the script being inspected from the known +// standard types. +func typeOfScript(pops []parsedOpcode) ScriptClass { + if isPubkey(pops) { + return PubKeyTy + } else if isPubkeyHash(pops) { + return PubKeyHashTy + // } else if isWitnessPubKeyHash(pops) { + // return WitnessV0PubKeyHashTy + } else if isScriptHash(pops) { + return ScriptHashTy + // } else if isWitnessScriptHash(pops) { + // return WitnessV0ScriptHashTy + } else if isMultiSig(pops) { + return MultiSigTy + } else if isNullData(pops) { + return NullDataTy + } + return NonStandardTy +} + +// GetScriptClass returns the class of the script passed. NonStandardTy will be +// returned when the script does not parse. +func GetScriptClass(script []byte) ScriptClass { + pops, e := parseScript(script) + if e != nil { + return NonStandardTy + } + return typeOfScript(pops) +} + +// expectedInputs returns the number of arguments required by a script. If the +// script is of unknown type such that the number can not be determined then -1 +// is returned. We are an internal function and thus assume that class is the +// real class of pops (and we can thus assume things that were determined while +// finding out the type). +func expectedInputs(pops []parsedOpcode, class ScriptClass) int { + switch class { + case PubKeyTy: + return 1 + case PubKeyHashTy: + return 2 + // case WitnessV0PubKeyHashTy: + // return 2 + case ScriptHashTy: + // Not including script. That is handled by the caller. + return 1 + // case WitnessV0ScriptHashTy: + // // Not including script. That is handled by the caller. + // return 1 + case MultiSigTy: + // Standard multisig has a push a small number for the number of sigs and number + // of keys. Chk the first push instruction to see how many arguments are + // expected. typeOfScript already checked this so we know it'll be a small int. + // Also, due to the original bitcoind bug where OP_CHECKMULTISIG pops an + // additional item from the stack, add an extra expected input for the extra + // push that is required to compensate. + return asSmallInt(pops[0].opcode) + 1 + case NullDataTy: + fallthrough + default: + return -1 + } +} + +// ScriptInfo houses information about a script pair that is determined by +// CalcScriptInfo. +type ScriptInfo struct { + // PkScriptClass is the class of the public key script and is equivalent to + // calling GetScriptClass on it. + PkScriptClass ScriptClass + // NumInputs is the number of inputs provided by the public key script. + NumInputs int + // ExpectedInputs is the number of outputs required by the signature script and + // any pay-to-script-hash scripts. The number will be -1 if unknown. + ExpectedInputs int + // SigOps is the number of signature operations in the script pair. + SigOps int +} + +// CalcScriptInfo returns a structure providing data about the provided script +// pair. It will error if the pair is in someway invalid such that they can not +// be analysed, i.e. if they do not parse or the pkScript is not a push-only +// script +func CalcScriptInfo(sigScript, pkScript []byte, bip16 bool) (si *ScriptInfo, e error) { + var sigPops []parsedOpcode + if sigPops, e = parseScript(sigScript); E.Chk(e) { + return + } + var pkPops []parsedOpcode + if pkPops, e = parseScript(pkScript); E.Chk(e) { + return + } + // Push only sigScript makes little sense. + si = new(ScriptInfo) + si.PkScriptClass = typeOfScript(pkPops) + // Can't have a signature script that doesn't just push data. + if !isPushOnly(sigPops) { + return nil, scriptError( + ErrNotPushOnly, + "signature script is not push only", + ) + } + si.ExpectedInputs = expectedInputs(pkPops, si.PkScriptClass) + switch { + // Count sigops taking into account pay-to-script-hash. + case si.PkScriptClass == ScriptHashTy && bip16: + // The pay-to-hash-script is the final data push of the signature script. + script := sigPops[len(sigPops)-1].data + shPops, e := parseScript(script) + if e != nil { + return nil, e + } + shInputs := expectedInputs(shPops, typeOfScript(shPops)) + if shInputs == -1 { + si.ExpectedInputs = -1 + } else { + si.ExpectedInputs += shInputs + } + si.SigOps = getSigOpCount(shPops, true) + // All entries pushed to stack (or are OP_RESERVED and exec will fail). + si.NumInputs = len(sigPops) + // // If segwit is active, and this is a regular p2wkh output, then we'll treat the script as a p2pkh output in + // // essence. + // case si.PkScriptClass == WitnessV0PubKeyHashTy && segwit: + // si.SigOps = GetWitnessSigOpCount(sigScript, pkScript, witness) + // si.NumInputs = len(witness) + // // We'll attempt to detect the nested p2sh case so we can accurately count the signature operations involved. + // case si.PkScriptClass == ScriptHashTy && + // IsWitnessProgram(sigScript[1:]) && bip16 && segwit: + // // Extract the pushed witness program from the sigScript so we can determine the + // // number of expected inputs. + // pkPops, _ := parseScript(sigScript[1:]) + // shInputs := expectedInputs(pkPops, typeOfScript(pkPops)) + // if shInputs == -1 { + // si.ExpectedInputs = -1 + // } else { + // si.ExpectedInputs += shInputs + // } + // si.SigOps = GetWitnessSigOpCount(sigScript, pkScript, witness) + // si.NumInputs = len(witness) + // si.NumInputs += len(sigPops) + // // If segwit is active, and this is a p2wsh output, then we'll need to examine + // // the witness script to generate accurate script info. + // case si.PkScriptClass == WitnessV0ScriptHashTy && segwit: + // // The witness script is the final element of the witness stack. + // witnessScript := witness[len(witness)-1] + // pops, _ := parseScript(witnessScript) + // shInputs := expectedInputs(pops, typeOfScript(pops)) + // if shInputs == -1 { + // si.ExpectedInputs = -1 + // } else { + // si.ExpectedInputs += shInputs + // } + // si.SigOps = GetWitnessSigOpCount(sigScript, pkScript, witness) + // si.NumInputs = len(witness) + default: + si.SigOps = getSigOpCount(pkPops, true) + // All entries pushed to stack (or are OP_RESERVED and exec will fail). + si.NumInputs = len(sigPops) + } + return si, nil +} + +// CalcMultiSigStats returns the number of public keys and signatures from a +// multi-signature transaction script. The passed script MUST already be known +// to be a multi-signature script. +func CalcMultiSigStats(script []byte) (int, int, error) { + pops, e := parseScript(script) + if e != nil { + return 0, 0, e + } + // A multi-signature script is of the pattern: + // + // NUM_SIGS PUBKEY PUBKEY PUBKEY... NUM_PUBKEYS OP_CHECKMULTISIG + // + // Therefore the number of signatures is the oldest item on the stack and the + // number of pubkeys is the 2nd to last. Also, the absolute minimum for a + // multi-signature script is 1 pubkey, so at least 4 items must be on the stack + // per: + // + // OP_1 PUBKEY OP_1 OP_CHECKMULTISIG + if len(pops) < 4 { + str := fmt.Sprintf("script %x is not a multisig script", script) + return 0, 0, scriptError(ErrNotMultisigScript, str) + } + numSigs := asSmallInt(pops[0].opcode) + numPubKeys := asSmallInt(pops[len(pops)-2].opcode) + return numPubKeys, numSigs, nil +} + +// payToPubKeyHashScript creates a new script to pay a transaction output to a +// 20-byte pubkey hash. It is expected that the input is a valid hash. +func payToPubKeyHashScript(pubKeyHash []byte) ([]byte, error) { + return NewScriptBuilder().AddOp(OP_DUP).AddOp(OP_HASH160). + AddData(pubKeyHash).AddOp(OP_EQUALVERIFY).AddOp(OP_CHECKSIG). + Script() +} + +// // payToWitnessPubKeyHashScript creates a new script to pay to a version 0 +// // pubkey hash witness program. The passed hash is expected to be valid. +// func payToWitnessPubKeyHashScript(pubKeyHash []byte) ([]byte, error) { +// return NewScriptBuilder().AddOp(OP_0).AddData(pubKeyHash).Script() +// } + +// payToScriptHashScript creates a new script to pay a transaction output to a +// script hash. It is expected that the input is a valid hash. +func payToScriptHashScript(scriptHash []byte) ([]byte, error) { + return NewScriptBuilder().AddOp(OP_HASH160).AddData(scriptHash). + AddOp(OP_EQUAL).Script() +} + +// payToWitnessPubKeyHashScript creates a new script to pay to a version 0 +// script hash witness program. The passed hash is expected to be valid. +func payToWitnessScriptHashScript(scriptHash []byte) ([]byte, error) { + return NewScriptBuilder().AddOp(OP_0).AddData(scriptHash).Script() +} + +// payToPubkeyScript creates a new script to pay a transaction output to a +// public key. It is expected that the input is a valid pubkey. +func payToPubKeyScript(serializedPubKey []byte) ([]byte, error) { + return NewScriptBuilder().AddData(serializedPubKey). + AddOp(OP_CHECKSIG).Script() +} + +// PayToAddrScript creates a new script to pay a transaction output to a the specified address. +func PayToAddrScript(addr btcaddr.Address) ([]byte, error) { + const nilAddrErrStr = "unable to generate payment script for nil address" + switch addr := addr.(type) { + case *btcaddr.PubKeyHash: + if addr == nil { + return nil, scriptError( + ErrUnsupportedAddress, + nilAddrErrStr, + ) + } + return payToPubKeyHashScript(addr.ScriptAddress()) + case *btcaddr.ScriptHash: + if addr == nil { + return nil, scriptError( + ErrUnsupportedAddress, + nilAddrErrStr, + ) + } + return payToScriptHashScript(addr.ScriptAddress()) + case *btcaddr.PubKey: + if addr == nil { + return nil, scriptError( + ErrUnsupportedAddress, + nilAddrErrStr, + ) + } + return payToPubKeyScript(addr.ScriptAddress()) + // case *util.AddressWitnessPubKeyHash: + // if addr == nil { + // return nil, scriptError(ErrUnsupportedAddress, + // nilAddrErrStr) + // } + // return payToWitnessPubKeyHashScript(addr.ScriptAddress()) + // case *util.AddressWitnessScriptHash: + // if addr == nil { + // return nil, scriptError(ErrUnsupportedAddress, + // nilAddrErrStr) + // } + // return payToWitnessScriptHashScript(addr.ScriptAddress()) + } + str := fmt.Sprintf( + "unable to generate payment script for unsupported "+ + "address type %T", addr, + ) + return nil, scriptError(ErrUnsupportedAddress, str) +} + +// NullDataScript creates a provably-prunable script containing OP_RETURN +// followed by the passed data. An ScriptError with the error code +// errTooMuchNullData will be returned if the length of the passed data exceeds +// MaxDataCarrierSize. +func NullDataScript(data []byte) ([]byte, error) { + if len(data) > MaxDataCarrierSize { + str := fmt.Sprintf( + "data size %d is larger than max "+ + "allowed size %d", len(data), MaxDataCarrierSize, + ) + return nil, scriptError(ErrTooMuchNullData, str) + } + return NewScriptBuilder().AddOp(OP_RETURN).AddData(data).Script() +} + +// MultiSigScript returns a valid script for a multisignature redemption where +// nrequired of the keys in pubkeys are required to have signed the transaction +// for success. An ScriptError with the error code errTooManyRequiredSigs will +// be returned if nrequired is larger than the number of keys provided. +func MultiSigScript(pubkeys []*btcaddr.PubKey, nrequired int) ([]byte, error) { + if len(pubkeys) < nrequired { + str := fmt.Sprintf( + "unable to generate multisig script with "+ + "%d required signatures when there are only %d public "+ + "keys available", nrequired, len(pubkeys), + ) + return nil, scriptError(ErrTooManyRequiredSigs, str) + } + builder := NewScriptBuilder().AddInt64(int64(nrequired)) + for _, key := range pubkeys { + builder.AddData(key.ScriptAddress()) + } + builder.AddInt64(int64(len(pubkeys))) + builder.AddOp(OP_CHECKMULTISIG) + return builder.Script() +} + +// PushedData returns an array of byte slices containing any pushed data found +// in the passed script. This includes OP_0, but not OP_1 - OP_16. +func PushedData(script []byte) ([][]byte, error) { + pops, e := parseScript(script) + if e != nil { + return nil, e + } + var data [][]byte + for _, pop := range pops { + if pop.data != nil { + data = append(data, pop.data) + } else if pop.opcode.value == OP_0 { + data = append(data, nil) + } + } + return data, nil +} + +// ExtractPkScriptAddrs returns the type of script, addresses and required +// signatures associated with the passed PkScript. Note that it only works for +// 'standard' transaction script types. Any data such as public keys which are +// invalid are omitted from the results. +func ExtractPkScriptAddrs(pkScript []byte, chainParams *chaincfg.Params) (ScriptClass, []btcaddr.Address, int, error) { + var addrs []btcaddr.Address + var requiredSigs int + // No valid addresses or required signatures if the script doesn't parse. + pops, e := parseScript(pkScript) + if e != nil { + return NonStandardTy, nil, 0, e + } + scriptClass := typeOfScript(pops) + switch scriptClass { + case PubKeyHashTy: + // A pay-to-pubkey-hash script is of the form: OP_DUP OP_HASH160 + // OP_EQUALVERIFY OP_CHECKSIG Therefore the pubkey hash is the 3rd item on the + // stack. Skip the pubkey hash if it's invalid for some reason. + requiredSigs = 1 + addr, e := btcaddr.NewPubKeyHash( + pops[2].data, + chainParams, + ) + if e == nil { + addrs = append(addrs, addr) + } + // case WitnessV0PubKeyHashTy: + // // A pay-to-witness-pubkey-hash script is of the form: OP_0 <20-byte hash> + // // Therefore, the pubkey hash is the second item on the stack. Skip the pubkey + // // hash if it's invalid for some reason. + // requiredSigs = 1 + // addr, e := util.NewAddressWitnessPubKeyHash(pops[1].data, + // chainParams) + // if e == nil { + // addrs = append(addrs, addr) + // } + case PubKeyTy: + // A pay-to-pubkey script is of the form: OP_CHECKSIG Therefore the pubkey is the first item on the + // stack. Skip the pubkey if it's invalid for some reason. + requiredSigs = 1 + addr, e := btcaddr.NewPubKey(pops[0].data, chainParams) + if e == nil { + addrs = append(addrs, addr) + } + case ScriptHashTy: + // A pay-to-script-hash script is of the form: OP_HASH160 OP_EQUAL Therefore the script hash is the + // 2nd item on the stack. Skip the script hash if it's invalid for some reason. + requiredSigs = 1 + addr, e := btcaddr.NewScriptHashFromHash( + pops[1].data, + chainParams, + ) + if e == nil { + addrs = append(addrs, addr) + } + // case WitnessV0ScriptHashTy: + // // A pay-to-witness-script-hash script is of the form: OP_0 <32-byte hash> + // // Therefore, the script hash is the second item on the stack. Skip the script + // // hash if it's invalid for some reason. + // requiredSigs = 1 + // addr, e := util.NewAddressWitnessScriptHash(pops[1].data, + // chainParams) + // if e == nil { + // addrs = append(addrs, addr) + // } + case MultiSigTy: + // A multi-signature script is of the form: + // ... OP_CHECKMULTISIG Therefore the number of required + // signatures is the 1st item on the stack and the number of public keys is the + // 2nd to last item on the stack. + requiredSigs = asSmallInt(pops[0].opcode) + numPubKeys := asSmallInt(pops[len(pops)-2].opcode) + // Extract the public keys while skipping any that are invalid. + addrs = make([]btcaddr.Address, 0, numPubKeys) + for i := 0; i < numPubKeys; i++ { + addr, e := btcaddr.NewPubKey( + pops[i+1].data, + chainParams, + ) + if e == nil { + addrs = append(addrs, addr) + } + } + case NullDataTy: + // Null data transactions have no addresses or required signatures. + case NonStandardTy: + // Don't attempt to extract addresses or required signatures for nonstandard + // transactions. + } + return scriptClass, addrs, requiredSigs, nil +} + +// AtomicSwapDataPushes houses the data pushes found in atomic swap contracts. +type AtomicSwapDataPushes struct { + RecipientHash160 [20]byte + RefundHash160 [20]byte + SecretHash [32]byte + SecretSize int64 + LockTime int64 +} + +// ExtractAtomicSwapDataPushes returns the data pushes from an atomic swap +// contract. If the script is not an atomic swap contract, +// +// ExtractAtomicSwapDataPushes returns (nil, nil). Non-nil errors are returned +// for unparsable scripts. NOTE: Atomic swaps are not considered standard script +// types by the dcrd mempool policy and should be used with P2SH. The atomic +// swap format is also expected to change to use a more secure hash function in +// the future. This function is only defined in the txscript package due to API +// limitations which prevent callers using txscript to parse nonstandard +// scripts. +func ExtractAtomicSwapDataPushes(version uint16, pkScript []byte) (*AtomicSwapDataPushes, error) { + pops, e := parseScript(pkScript) + if e != nil { + return nil, e + } + if len(pops) != 20 { + return nil, nil + } + isAtomicSwap := pops[0].opcode.value == OP_IF && + pops[1].opcode.value == OP_SIZE && + canonicalPush(pops[2]) && + pops[3].opcode.value == OP_EQUALVERIFY && + pops[4].opcode.value == OP_SHA256 && + pops[5].opcode.value == OP_DATA_32 && + pops[6].opcode.value == OP_EQUALVERIFY && + pops[7].opcode.value == OP_DUP && + pops[8].opcode.value == OP_HASH160 && + pops[9].opcode.value == OP_DATA_20 && + pops[10].opcode.value == OP_ELSE && + canonicalPush(pops[11]) && + pops[12].opcode.value == OP_CHECKLOCKTIMEVERIFY && + pops[13].opcode.value == OP_DROP && + pops[14].opcode.value == OP_DUP && + pops[15].opcode.value == OP_HASH160 && + pops[16].opcode.value == OP_DATA_20 && + pops[17].opcode.value == OP_ENDIF && + pops[18].opcode.value == OP_EQUALVERIFY && + pops[19].opcode.value == OP_CHECKSIG + if !isAtomicSwap { + return nil, nil + } + pushes := new(AtomicSwapDataPushes) + copy(pushes.SecretHash[:], pops[5].data) + copy(pushes.RecipientHash160[:], pops[9].data) + copy(pushes.RefundHash160[:], pops[16].data) + if pops[2].data != nil { + locktime, e := makeScriptNum(pops[2].data, true, 5) + if e != nil { + return nil, nil + } + pushes.SecretSize = int64(locktime) + } else if op := pops[2].opcode; isSmallInt(op) { + pushes.SecretSize = int64(asSmallInt(op)) + } else { + return nil, nil + } + if pops[11].data != nil { + locktime, e := makeScriptNum(pops[11].data, true, 5) + if e != nil { + return nil, nil + } + pushes.LockTime = int64(locktime) + } else if op := pops[11].opcode; isSmallInt(op) { + pushes.LockTime = int64(asSmallInt(op)) + } else { + return nil, nil + } + return pushes, nil +} diff --git a/pkg/txscript/standard_test.go b/pkg/txscript/standard_test.go new file mode 100644 index 0000000..018da0e --- /dev/null +++ b/pkg/txscript/standard_test.go @@ -0,0 +1,1309 @@ +package txscript + +import ( + "bytes" + "encoding/hex" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "reflect" + "testing" + + "github.com/p9c/p9/pkg/wire" +) + +// mustParseShortForm parses the passed short form script and returns the resulting bytes. It panics if an error occurs. +// This is only used in the tests as a helper since the only way it can fail is if there is an error in the test source +// code. +func mustParseShortForm(script string) []byte { + s, e := parseShortForm(script) + if e != nil { + panic( + "invalid short form script in test source: err " + + e.Error() + ", script: " + script, + ) + } + return s +} + +// newAddressPubKey returns a new util.PubKey from the provided serialized public key. It panics if an error +// occurs. This is only used in the tests as a helper since the only way it can fail is if there is an error in the test +// source code. +func newAddressPubKey(serializedPubKey []byte) btcaddr.Address { + addr, e := btcaddr.NewPubKey( + serializedPubKey, + &chaincfg.MainNetParams, + ) + if e != nil { + panic("invalid public key in test source") + } + return addr +} + +// newAddressPubKeyHash returns a new util.PubKeyHash from the provided hash. It panics if an error occurs. This +// is only used in the tests as a helper since the only way it can fail is if there is an error in the test source code. +func newAddressPubKeyHash(pkHash []byte) btcaddr.Address { + addr, e := btcaddr.NewPubKeyHash(pkHash, &chaincfg.MainNetParams) + if e != nil { + panic("invalid public key hash in test source") + } + return addr +} + +// newAddressScriptHash returns a new util.ScriptHash from the provided hash. It panics if an error occurs. This +// is only used in the tests as a helper since the only way it can fail is if there is an error in the test source code. +func newAddressScriptHash(scriptHash []byte) btcaddr.Address { + addr, e := btcaddr.NewScriptHashFromHash( + scriptHash, + &chaincfg.MainNetParams, + ) + if e != nil { + panic("invalid script hash in test source") + } + return addr +} + +// TestExtractPkScriptAddrs ensures that extracting the type, addresses, and number of required signatures from +// PkScripts works as intended. +func TestExtractPkScriptAddrs(t *testing.T) { + t.Parallel() + tests := []struct { + name string + script []byte + addrs []btcaddr.Address + reqSigs int + class ScriptClass + }{ + { + name: "standard p2pk with compressed pubkey (0x02)", + script: hexToBytes( + "2102192d74d0cb94344c9569c2e779015" + + "73d8d7903c3ebec3a957724895dca52c6b4ac", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "02192d74d0cb9434" + + "4c9569c2e77901573d8d7903c3ebec3a9577" + + "24895dca52c6b4", + ), + ), + }, + reqSigs: 1, + class: PubKeyTy, + }, + { + name: "standard p2pk with uncompressed pubkey (0x04)", + script: hexToBytes( + "410411db93e1dcdb8a016b49840f8c53b" + + "c1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" + + "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b864" + + "3f656b412a3ac", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "0411db93e1dcdb8a" + + "016b49840f8c53bc1eb68a382e97b1482eca" + + "d7b148a6909a5cb2e0eaddfb84ccf9744464" + + "f82e160bfa9b8b64f9d4c03f999b8643f656" + + "b412a3", + ), + ), + }, + reqSigs: 1, + class: PubKeyTy, + }, + { + name: "standard p2pk with hybrid pubkey (0x06)", + script: hexToBytes( + "4106192d74d0cb94344c9569c2e779015" + + "73d8d7903c3ebec3a957724895dca52c6b40d4526483" + + "8c0bd96852662ce6a847b197376830160c6d2eb5e6a4" + + "c44d33f453eac", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "06192d74d0cb9434" + + "4c9569c2e77901573d8d7903c3ebec3a9577" + + "24895dca52c6b40d45264838c0bd96852662" + + "ce6a847b197376830160c6d2eb5e6a4c44d3" + + "3f453e", + ), + ), + }, + reqSigs: 1, + class: PubKeyTy, + }, + { + name: "standard p2pk with compressed pubkey (0x03)", + script: hexToBytes( + "2103b0bd634234abbb1ba1e986e884185" + + "c61cf43e001f9137f23c2c409273eb16e65ac", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "03b0bd634234abbb" + + "1ba1e986e884185c61cf43e001f9137f23c2" + + "c409273eb16e65", + ), + ), + }, + reqSigs: 1, + class: PubKeyTy, + }, + { + name: "2nd standard p2pk with uncompressed pubkey (0x04)", + script: hexToBytes( + "4104b0bd634234abbb1ba1e986e884185" + + "c61cf43e001f9137f23c2c409273eb16e6537a576782" + + "eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3" + + "c1e0908ef7bac", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "04b0bd634234abbb" + + "1ba1e986e884185c61cf43e001f9137f23c2" + + "c409273eb16e6537a576782eba668a7ef8bd" + + "3b3cfb1edb7117ab65129b8a2e681f3c1e09" + + "08ef7b", + ), + ), + }, + reqSigs: 1, + class: PubKeyTy, + }, + { + name: "standard p2pk with hybrid pubkey (0x07)", + script: hexToBytes( + "4107b0bd634234abbb1ba1e986e884185" + + "c61cf43e001f9137f23c2c409273eb16e6537a576782" + + "eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3" + + "c1e0908ef7bac", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "07b0bd634234abbb" + + "1ba1e986e884185c61cf43e001f9137f23c2" + + "c409273eb16e6537a576782eba668a7ef8bd" + + "3b3cfb1edb7117ab65129b8a2e681f3c1e09" + + "08ef7b", + ), + ), + }, + reqSigs: 1, + class: PubKeyTy, + }, + { + name: "standard p2pkh", + script: hexToBytes( + "76a914ad06dd6ddee55cbca9a9e3713bd" + + "7587509a3056488ac", + ), + addrs: []btcaddr.Address{ + newAddressPubKeyHash( + hexToBytes( + "ad06dd6ddee5" + + "5cbca9a9e3713bd7587509a30564", + ), + ), + }, + reqSigs: 1, + class: PubKeyHashTy, + }, + { + name: "standard p2sh", + script: hexToBytes( + "a91463bcc565f9e68ee0189dd5cc67f1b" + + "0e5f02f45cb87", + ), + addrs: []btcaddr.Address{ + newAddressScriptHash( + hexToBytes( + "63bcc565f9e6" + + "8ee0189dd5cc67f1b0e5f02f45cb", + ), + ), + }, + reqSigs: 1, + class: ScriptHashTy, + }, + // from real tx 60a20bd93aa49ab4b28d514ec10b06e1829ce6818ec06cd3aabd013ebcdc4bb1, vout 0 + { + name: "standard 1 of 2 multisig", + script: hexToBytes( + "514104cc71eb30d653c0c3163990c47b9" + + "76f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a47" + + "3e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d1" + + "1fcdd0d348ac4410461cbdcc5409fb4b4d42b51d3338" + + "1354d80e550078cb532a34bfa2fcfdeb7d76519aecc6" + + "2770f5b0e4ef8551946d8a540911abe3e7854a26f39f" + + "58b25c15342af52ae", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "04cc71eb30d653c0" + + "c3163990c47b976f3fb3f37cccdcbedb169a" + + "1dfef58bbfbfaff7d8a473e7e2e6d317b87b" + + "afe8bde97e3cf8f065dec022b51d11fcdd0d" + + "348ac4", + ), + ), + newAddressPubKey( + hexToBytes( + "0461cbdcc5409fb4" + + "b4d42b51d33381354d80e550078cb532a34b" + + "fa2fcfdeb7d76519aecc62770f5b0e4ef855" + + "1946d8a540911abe3e7854a26f39f58b25c1" + + "5342af", + ), + ), + }, + reqSigs: 1, + class: MultiSigTy, + }, + // from real tx d646f82bd5fbdb94a36872ce460f97662b80c3050ad3209bef9d1e398ea277ab, vin 1 + { + name: "standard 2 of 3 multisig", + script: hexToBytes( + "524104cb9c3c222c5f7a7d3b9bd152f36" + + "3a0b6d54c9eb312c4d4f9af1e8551b6c421a6a4ab0e2" + + "9105f24de20ff463c1c91fcf3bf662cdde4783d4799f" + + "787cb7c08869b4104ccc588420deeebea22a7e900cc8" + + "b68620d2212c374604e3487ca08f1ff3ae12bdc63951" + + "4d0ec8612a2d3c519f084d9a00cbbe3b53d071e9b09e" + + "71e610b036aa24104ab47ad1939edcb3db65f7fedea6" + + "2bbf781c5410d3f22a7a3a56ffefb2238af8627363bd" + + "f2ed97c1f89784a1aecdb43384f11d2acc64443c7fc2" + + "99cef0400421a53ae", + ), + addrs: []btcaddr.Address{ + newAddressPubKey( + hexToBytes( + "04cb9c3c222c5f7a" + + "7d3b9bd152f363a0b6d54c9eb312c4d4f9af" + + "1e8551b6c421a6a4ab0e29105f24de20ff46" + + "3c1c91fcf3bf662cdde4783d4799f787cb7c" + + "08869b", + ), + ), + newAddressPubKey( + hexToBytes( + "04ccc588420deeeb" + + "ea22a7e900cc8b68620d2212c374604e3487" + + "ca08f1ff3ae12bdc639514d0ec8612a2d3c5" + + "19f084d9a00cbbe3b53d071e9b09e71e610b" + + "036aa2", + ), + ), + newAddressPubKey( + hexToBytes( + "04ab47ad1939edcb" + + "3db65f7fedea62bbf781c5410d3f22a7a3a5" + + "6ffefb2238af8627363bdf2ed97c1f89784a" + + "1aecdb43384f11d2acc64443c7fc299cef04" + + "00421a", + ), + ), + }, + reqSigs: 2, + class: MultiSigTy, + }, + // The below are nonstandard script due to things such as invalid pubkeys, failure to parse, and not being of a standard form. + { + name: "p2pk with uncompressed pk missing OP_CHECKSIG", + script: hexToBytes( + "410411db93e1dcdb8a016b49840f8c53b" + + "c1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddf" + + "b84ccf9744464f82e160bfa9b8b64f9d4c03f999b864" + + "3f656b412a3", + ), + addrs: nil, + reqSigs: 0, + class: NonStandardTy, + }, + { + name: "valid signature from a sigscript - no addresses", + script: hexToBytes( + "47304402204e45e16932b8af514961a1d" + + "3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd41022" + + "0181522ec8eca07de4860a4acdd12909d831cc56cbba" + + "c4622082221a8768d1d0901", + ), + addrs: nil, + reqSigs: 0, + class: NonStandardTy, + }, + // Note the technically the pubkey is the second item on the stack, but since the address extraction intentionally only works with standard PkScripts, this should not return any addresses. + { + name: "valid sigscript to reedeem p2pk - no addresses", + script: hexToBytes( + "493046022100ddc69738bf2336318e4e0" + + "41a5a77f305da87428ab1606f023260017854350ddc0" + + "22100817af09d2eec36862d16009852b7e3a0f6dd765" + + "98290b7834e1453660367e07a014104cd4240c198e12" + + "523b6f9cb9f5bed06de1ba37e96a1bbd13745fcf9d11" + + "c25b1dff9a519675d198804ba9962d3eca2d5937d58e" + + "5a75a71042d40388a4d307f887d", + ), + addrs: nil, + reqSigs: 0, + class: NonStandardTy, + }, + // from real tx 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 0 invalid public keys + { + name: "1 of 3 multisig with invalid pubkeys", + script: hexToBytes( + "51411c2200007353455857696b696c656" + + "16b73204361626c6567617465204261636b75700a0a6" + + "361626c65676174652d3230313031323034313831312" + + "e377a0a0a446f41776e6c6f61642074686520666f6c6" + + "c6f77696e67207472616e73616374696f6e732077697" + + "468205361746f736869204e616b616d6f746f2773206" + + "46f776e6c6f61416420746f6f6c2077686963680a636" + + "16e20626520666f756e6420696e207472616e7361637" + + "4696f6e2036633533636439383731313965663739376" + + "435616463636453ae", + ), + addrs: []btcaddr.Address{}, + reqSigs: 1, + class: MultiSigTy, + }, + // from real tx: 691dd277dc0e90a462a3d652a1171686de49cf19067cd33c7df0392833fb986a, vout 44 invalid public keys + { + name: "1 of 3 multisig with invalid pubkeys 2", + script: hexToBytes( + "514134633365633235396337346461636" + + "536666430383862343463656638630a6336366263313" + + "93936633862393461333831316233363536313866653" + + "16539623162354136636163636539393361333938386" + + "134363966636336643664616266640a3236363363666" + + "13963663463303363363039633539336333653931666" + + "56465373032392131323364643432643235363339643" + + "338613663663530616234636434340a00000053ae", + ), + addrs: []btcaddr.Address{}, + reqSigs: 1, + class: MultiSigTy, + }, + { + name: "empty script", + script: []byte{}, + addrs: nil, + reqSigs: 0, + class: NonStandardTy, + }, + { + name: "script that does not parse", + script: []byte{OP_DATA_45}, + addrs: nil, + reqSigs: 0, + class: NonStandardTy, + }, + } + t.Logf("Running %d tests.", len(tests)) + for i, test := range tests { + class, addrs, reqSigs, e := ExtractPkScriptAddrs( + test.script, &chaincfg.MainNetParams, + ) + if e != nil { + t.Log(e) + } + if !reflect.DeepEqual(addrs, test.addrs) { + t.Errorf( + "ExtractPkScriptAddrs #%d (%s) unexpected "+ + "addresses\ngot %v\nwant %v", i, test.name, + addrs, test.addrs, + ) + continue + } + if reqSigs != test.reqSigs { + t.Errorf( + "ExtractPkScriptAddrs #%d (%s) unexpected "+ + "number of required signatures - got %d, "+ + "want %d", i, test.name, reqSigs, test.reqSigs, + ) + continue + } + if class != test.class { + t.Errorf( + "ExtractPkScriptAddrs #%d (%s) unexpected "+ + "script type - got %s, want %s", i, test.name, + class, test.class, + ) + continue + } + } +} + +// TestCalcScriptInfo ensures the CalcScriptInfo provides the expected results for various valid and invalid script pairs. +func TestCalcScriptInfo(t *testing.T) { + t.Parallel() + tests := []struct { + name string + sigScript string + pkScript string + witness []string + bip16 bool + segwit bool + scriptInfo ScriptInfo + scriptInfoErr error + }{ + { + // Invented scripts, the hashes do not match Truncated version of test below: + name: "pkscript doesn't parse", + sigScript: "1 81 DATA_8 2DUP EQUAL NOT VERIFY ABS " + + "SWAP ABS EQUAL", + pkScript: "HASH160 DATA_20 0xfe441065b6532231de2fac56" + + "3152205ec4f59c", + bip16: true, + scriptInfoErr: scriptError(ErrMalformedPush, ""), + }, + { + name: "sigScript doesn't parse", + // Truncated version of p2sh script below. + sigScript: "1 81 DATA_8 2DUP EQUAL NOT VERIFY ABS " + + "SWAP ABS", + pkScript: "HASH160 DATA_20 0xfe441065b6532231de2fac56" + + "3152205ec4f59c74 EQUAL", + bip16: true, + scriptInfoErr: scriptError(ErrMalformedPush, ""), + }, + { + // Invented scripts, the hashes do not match + name: "p2sh standard script", + sigScript: "1 81 DATA_25 DUP HASH160 DATA_20 0x010203" + + "0405060708090a0b0c0d0e0f1011121314 EQUALVERIFY " + + "CHECKSIG", + pkScript: "HASH160 DATA_20 0xfe441065b6532231de2fac56" + + "3152205ec4f59c74 EQUAL", + bip16: true, + scriptInfo: ScriptInfo{ + PkScriptClass: ScriptHashTy, + NumInputs: 3, + ExpectedInputs: 3, // nonstandard p2sh. + SigOps: 1, + }, + }, + { + // from 567a53d1ce19ce3d07711885168484439965501536d0d0294c5d46d46c10e53b from the blockchain. + name: "p2sh nonstandard script", + sigScript: "1 81 DATA_8 2DUP EQUAL NOT VERIFY ABS " + + "SWAP ABS EQUAL", + pkScript: "HASH160 DATA_20 0xfe441065b6532231de2fac56" + + "3152205ec4f59c74 EQUAL", + bip16: true, + scriptInfo: ScriptInfo{ + PkScriptClass: ScriptHashTy, + NumInputs: 3, + ExpectedInputs: -1, // nonstandard p2sh. + SigOps: 0, + }, + }, + { + // Script is invented, numbers all fake. + name: "multisig script", + // Extra 0 arg on the end for OP_CHECKMULTISIG bug. + sigScript: "1 1 1 0", + pkScript: "3 " + + "DATA_33 0x0102030405060708090a0b0c0d0e0f1011" + + "12131415161718191a1b1c1d1e1f2021 DATA_33 " + + "0x0102030405060708090a0b0c0d0e0f101112131415" + + "161718191a1b1c1d1e1f2021 DATA_33 0x010203040" + + "5060708090a0b0c0d0e0f101112131415161718191a1" + + "b1c1d1e1f2021 3 CHECKMULTISIG", + bip16: true, + scriptInfo: ScriptInfo{ + PkScriptClass: MultiSigTy, + NumInputs: 4, + ExpectedInputs: 4, + SigOps: 3, + }, + }, + // { + // // A v0 p2wkh spend. + // name: "p2wkh script", + // pkScript: "OP_0 DATA_20 0x365ab47888e150ff46f8d51bce36dcd680f1283f", + // witness: []string{ + // "3045022100ee9fe8f9487afa977" + + // "6647ebcf0883ce0cd37454d7ce19889d34ba2c9" + + // "9ce5a9f402200341cb469d0efd3955acb9e46" + + // "f568d7e2cc10f9084aaff94ced6dc50a59134ad01", + // "03f0000d0639a22bfaf217e4c9428" + + // "9c2b0cc7fa1036f7fd5d9f61a9d6ec153100e", + // }, + // segwit: true, + // scriptInfo: ScriptInfo{ + // PkScriptClass: WitnessV0PubKeyHashTy, + // NumInputs: 2, + // ExpectedInputs: 2, + // SigOps: 1, + // }, + // }, + { + // Nested p2sh v0 + name: "p2wkh nested inside p2sh", + pkScript: "HASH160 DATA_20 " + + "0xb3a84b564602a9d68b4c9f19c2ea61458ff7826c EQUAL", + sigScript: "DATA_22 0x0014ad0ffa2e387f07e7ead14dc56d5a97dbd6ff5a23", + witness: []string{ + "3045022100cb1c2ac1ff1d57d" + + "db98f7bdead905f8bf5bcc8641b029ce8eef25" + + "c75a9e22a4702203be621b5c86b771288706be5" + + "a7eee1db4fceabf9afb7583c1cc6ee3f8297b21201", + "03f0000d0639a22bfaf217e4c9" + + "4289c2b0cc7fa1036f7fd5d9f61a9d6ec153100e", + }, + segwit: true, + bip16: true, + scriptInfo: ScriptInfo{ + PkScriptClass: ScriptHashTy, + NumInputs: 3, + ExpectedInputs: 3, + SigOps: 1, + }, + }, + // { + // // A v0 p2wsh spend. + // name: "p2wsh spend of a p2wkh witness script", + // pkScript: "0 DATA_32 0xe112b88a0cd87ba387f44" + + // "9d443ee2596eb353beb1f0351ab2cba8909d875db23", + // witness: []string{ + // "3045022100cb1c2ac1ff1d57d" + + // "db98f7bdead905f8bf5bcc8641b029ce8eef25" + + // "c75a9e22a4702203be621b5c86b771288706be5" + + // "a7eee1db4fceabf9afb7583c1cc6ee3f8297b21201", + // "03f0000d0639a22bfaf217e4c9" + + // "4289c2b0cc7fa1036f7fd5d9f61a9d6ec153100e", + // "76a914064977cb7b4a2e0c9680df0ef696e9e0e296b39988ac", + // }, + // segwit: true, + // scriptInfo: ScriptInfo{ + // PkScriptClass: WitnessV0ScriptHashTy, + // NumInputs: 3, + // ExpectedInputs: 3, + // SigOps: 1, + // }, + // }, + } + for _, test := range tests { + sigScript := mustParseShortForm(test.sigScript) + pkScript := mustParseShortForm(test.pkScript) + var witness wire.TxWitness + for _, witElement := range test.witness { + wit, e := hex.DecodeString(witElement) + if e != nil { + t.Fatalf( + "unable to decode witness "+ + "element: %v", e, + ) + } + witness = append(witness, wit) + } + var si *ScriptInfo + var e error + si, e = CalcScriptInfo(sigScript, pkScript, test.bip16) + if e = tstCheckScriptError(e, test.scriptInfoErr); e != nil { + t.Errorf("scriptinfo test %q: %v", test.name, e) + continue + } + if *si != test.scriptInfo { + t.Errorf( + "%s: scriptinfo doesn't match expected. "+ + "got: %q expected %q", test.name, *si, + test.scriptInfo, + ) + continue + } + } +} + +// bogusAddress implements the util.Address interface so the tests can ensure unsupported address types are handled +// properly. +type bogusAddress struct{} + +// EncodeAddress simply returns an empty string. It exists to satisfy the util.Address interface. +func (b *bogusAddress) EncodeAddress() string { + return "" +} + +// ScriptAddress simply returns an empty byte slice. It exists to satisfy the util.Address interface. +func (b *bogusAddress) ScriptAddress() []byte { + return nil +} + +// IsForNet lies blatantly to satisfy the util.Address interface. +func (b *bogusAddress) IsForNet(chainParams *chaincfg.Params) bool { + return true // why not? +} + +// String simply returns an empty string. It exists to satisfy the util.Address interface. +func (b *bogusAddress) String() string { + return "" +} + +// TestPayToAddrScript ensures the PayToAddrScript function generates the correct scripts for the various types of +// addresses. +func TestPayToAddrScript(t *testing.T) { + t.Parallel() + // 1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX + p2pkhMain, e := btcaddr.NewPubKeyHash( + hexToBytes( + "e34cce70c86"+ + "373273efcc54ce7d2a491bb4a0e84", + ), &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf("Unable to create public key hash address: %v", e) + } + // Taken from transaction: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d + p2shMain, e := btcaddr.NewScriptHashFromHash( + hexToBytes( + "e8c300"+ + "c87986efa84c37c0519929019ef86eb5b4", + ), &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf("Unable to create script hash address: %v", e) + } + // mainnet p2pk 13CG6SJ3yHUXo4Cr2RY4THLLJrNFuG3gUg + p2pkCompressedMain, e := btcaddr.NewPubKey( + hexToBytes( + "02192d"+ + "74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4", + ), + &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf( + "Unable to create pubkey address (compressed): %v", + e, + ) + } + p2pkCompressed2Main, e := btcaddr.NewPubKey( + hexToBytes( + "03b0b"+ + "d634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65", + ), + &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf( + "Unable to create pubkey address (compressed 2): %v", + e, + ) + } + p2pkUncompressedMain, e := btcaddr.NewPubKey( + hexToBytes( + "0411"+ + "db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5"+ + "cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b4"+ + "12a3", + ), &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf( + "Unable to create pubkey address (uncompressed): %v", + e, + ) + } + // Errors used in the tests below defined here for convenience and to keep the horizontal test size shorter. + errUnsupportedAddress := scriptError(ErrUnsupportedAddress, "") + tests := []struct { + in btcaddr.Address + expected string + err error + }{ + // pay-to-pubkey-hash address on mainnet + { + p2pkhMain, + "DUP HASH160 DATA_20 0xe34cce70c86373273efcc54ce7d2a4" + + "91bb4a0e8488 CHECKSIG", + nil, + }, + // pay-to-script-hash address on mainnet + { + p2shMain, + "HASH160 DATA_20 0xe8c300c87986efa84c37c0519929019ef8" + + "6eb5b4 EQUAL", + nil, + }, + // pay-to-pubkey address on mainnet. compressed key. + { + p2pkCompressedMain, + "DATA_33 0x02192d74d0cb94344c9569c2e77901573d8d7903c3" + + "ebec3a957724895dca52c6b4 CHECKSIG", + nil, + }, + // pay-to-pubkey address on mainnet. compressed key (other way). + { + p2pkCompressed2Main, + "DATA_33 0x03b0bd634234abbb1ba1e986e884185c61cf43e001" + + "f9137f23c2c409273eb16e65 CHECKSIG", + nil, + }, + // pay-to-pubkey address on mainnet. uncompressed key. + { + p2pkUncompressedMain, + "DATA_65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e" + + "97b1482ecad7b148a6909a5cb2e0eaddfb84ccf97444" + + "64f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 " + + "CHECKSIG", + nil, + }, + // Supported address types with nil pointers. + {(*btcaddr.PubKeyHash)(nil), "", errUnsupportedAddress}, + {(*btcaddr.ScriptHash)(nil), "", errUnsupportedAddress}, + {(*btcaddr.PubKey)(nil), "", errUnsupportedAddress}, + // Unsupported address type. + {&bogusAddress{}, "", errUnsupportedAddress}, + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + var pkScript []byte + pkScript, e = PayToAddrScript(test.in) + if e = tstCheckScriptError(e, test.err); e != nil { + t.Errorf( + "PayToAddrScript #%d unexpected error - "+ + "got %v, want %v", i, e, test.err, + ) + continue + } + expected := mustParseShortForm(test.expected) + if !bytes.Equal(pkScript, expected) { + t.Errorf( + "PayToAddrScript #%d got: %x\nwant: %x", + i, pkScript, expected, + ) + continue + } + } +} + +// TestMultiSigScript ensures the MultiSigScript function returns the expected scripts and errors. +func TestMultiSigScript(t *testing.T) { + t.Parallel() + // mainnet p2pk 13CG6SJ3yHUXo4Cr2RY4THLLJrNFuG3gUg + p2pkCompressedMain, e := btcaddr.NewPubKey( + hexToBytes( + "02192d"+ + "74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4", + ), + &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf( + "Unable to create pubkey address (compressed): %v", + e, + ) + } + p2pkCompressed2Main, e := btcaddr.NewPubKey( + hexToBytes( + "03b0b"+ + "d634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65", + ), + &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf( + "Unable to create pubkey address (compressed 2): %v", + e, + ) + } + p2pkUncompressedMain, e := btcaddr.NewPubKey( + hexToBytes( + "0411"+ + "db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5"+ + "cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b4"+ + "12a3", + ), &chaincfg.MainNetParams, + ) + if e != nil { + t.Fatalf( + "Unable to create pubkey address (uncompressed): %v", + e, + ) + } + tests := []struct { + keys []*btcaddr.PubKey + nrequired int + expected string + err error + }{ + { + []*btcaddr.PubKey{ + p2pkCompressedMain, + p2pkCompressed2Main, + }, + 1, + "1 DATA_33 0x02192d74d0cb94344c9569c2e77901573d8d7903c" + + "3ebec3a957724895dca52c6b4 DATA_33 0x03b0bd634" + + "234abbb1ba1e986e884185c61cf43e001f9137f23c2c4" + + "09273eb16e65 2 CHECKMULTISIG", + nil, + }, + { + []*btcaddr.PubKey{ + p2pkCompressedMain, + p2pkCompressed2Main, + }, + 2, + "2 DATA_33 0x02192d74d0cb94344c9569c2e77901573d8d7903c" + + "3ebec3a957724895dca52c6b4 DATA_33 0x03b0bd634" + + "234abbb1ba1e986e884185c61cf43e001f9137f23c2c4" + + "09273eb16e65 2 CHECKMULTISIG", + nil, + }, + { + []*btcaddr.PubKey{ + p2pkCompressedMain, + p2pkCompressed2Main, + }, + 3, + "", + scriptError(ErrTooManyRequiredSigs, ""), + }, + { + []*btcaddr.PubKey{ + p2pkUncompressedMain, + }, + 1, + "1 DATA_65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382" + + "e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf97444" + + "64f82e160bfa9b8b64f9d4c03f999b8643f656b412a3 " + + "1 CHECKMULTISIG", + nil, + }, + { + []*btcaddr.PubKey{ + p2pkUncompressedMain, + }, + 2, + "", + scriptError(ErrTooManyRequiredSigs, ""), + }, + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + var script []byte + script, e = MultiSigScript(test.keys, test.nrequired) + if e = tstCheckScriptError(e, test.err); e != nil { + t.Errorf("MultiSigScript #%d: %v", i, e) + continue + } + expected := mustParseShortForm(test.expected) + if !bytes.Equal(script, expected) { + t.Errorf( + "MultiSigScript #%d got: %x\nwant: %x", + i, script, expected, + ) + continue + } + } +} + +// TestCalcMultiSigStats ensures the CalcMutliSigStats function returns the expected errors. +func TestCalcMultiSigStats(t *testing.T) { + t.Parallel() + tests := []struct { + name string + script string + err error + }{ + { + name: "short script", + script: "0x046708afdb0fe5548271967f1a67130b7105cd6a828" + + "e03909a67962e0ea1f61d", + err: scriptError(ErrMalformedPush, ""), + }, + { + name: "stack underflow", + script: "RETURN DATA_41 0x046708afdb0fe5548271967f1a" + + "67130b7105cd6a828e03909a67962e0ea1f61deb649f6" + + "bc3f4cef308", + err: scriptError(ErrNotMultisigScript, ""), + }, + { + name: "multisig script", + script: "0 DATA_72 0x30450220106a3e4ef0b51b764a2887226" + + "2ffef55846514dacbdcbbdd652c849d395b4384022100" + + "e03ae554c3cbb40600d31dd46fc33f25e47bf8525b1fe" + + "07282e3b6ecb5f3bb2801 CODESEPARATOR 1 DATA_33 " + + "0x0232abdc893e7f0631364d7fd01cb33d24da45329a0" + + "0357b3a7886211ab414d55a 1 CHECKMULTISIG", + err: nil, + }, + } + var e error + for i, test := range tests { + script := mustParseShortForm(test.script) + _, _, e = CalcMultiSigStats(script) + if e = tstCheckScriptError(e, test.err); e != nil { + t.Errorf( + "CalcMultiSigStats #%d (%s): %v", i, test.name, + e, + ) + continue + } + } +} + +// scriptClassTests houses several test scripts used to ensure various class determination is working as expected. It's +// defined as a test global versus inside a function scope since this spans both the standard tests and the consensus +// tests (pay-to-script-hash is part of consensus). +var scriptClassTests = []struct { + name string + script string + class ScriptClass +}{ + { + name: "Pay Pubkey", + script: "DATA_65 0x0411db93e1dcdb8a016b49840f8c53bc1eb68a382e" + + "97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e16" + + "0bfa9b8b64f9d4c03f999b8643f656b412a3 CHECKSIG", + class: PubKeyTy, + }, + // tx 599e47a8114fe098103663029548811d2651991b62397e057f0c863c2bc9f9ea + { + name: "Pay PubkeyHash", + script: "DUP HASH160 DATA_20 0x660d4ef3a743e3e696ad990364e555" + + "c271ad504b EQUALVERIFY CHECKSIG", + class: PubKeyHashTy, + }, + // part of tx 6d36bc17e947ce00bb6f12f8e7a56a1585c5a36188ffa2b05e10b4743273a74b parts have been elided. (bitcoin + // core's checks for multisig type doesn't have codesep either). + { + name: "multisig", + script: "1 DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da4" + + "5329a00357b3a7886211ab414d55a 1 CHECKMULTISIG", + class: MultiSigTy, + }, + // tx e5779b9e78f9650debc2893fd9636d827b26b4ddfa6a8172fe8708c924f5c39d + { + name: "P2SH", + script: "HASH160 DATA_20 0x433ec2ac1ffa1b7b7d027f564529c57197f" + + "9ae88 EQUAL", + class: ScriptHashTy, + }, + { + // Nulldata with no data at all. + name: "nulldata no data", + script: "RETURN", + class: NullDataTy, + }, + { + // Nulldata with single zero push. + name: "nulldata zero", + script: "RETURN 0", + class: NullDataTy, + }, + { + // Nulldata with small integer push. + name: "nulldata small int", + script: "RETURN 1", + class: NullDataTy, + }, + { + // Nulldata with max small integer push. + name: "nulldata max small int", + script: "RETURN 16", + class: NullDataTy, + }, + { + // Nulldata with small data push. + name: "nulldata small data", + script: "RETURN DATA_8 0x046708afdb0fe554", + class: NullDataTy, + }, + { + // Canonical nulldata with 60-byte data push. + name: "canonical nulldata 60-byte push", + script: "RETURN 0x3c 0x046708afdb0fe5548271967f1a67130b7105cd" + + "6a828e03909a67962e0ea1f61deb649f6bc3f4cef3046708afdb" + + "0fe5548271967f1a67130b7105cd6a", + class: NullDataTy, + }, + { + // Non-canonical nulldata with 60-byte data push. + name: "non-canonical nulldata 60-byte push", + script: "RETURN PUSHDATA1 0x3c 0x046708afdb0fe5548271967f1a67" + + "130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" + + "046708afdb0fe5548271967f1a67130b7105cd6a", + class: NullDataTy, + }, + { + // Nulldata with max allowed data to be considered standard. + name: "nulldata max standard push", + script: "RETURN PUSHDATA1 0x50 0x046708afdb0fe5548271967f1a67" + + "130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" + + "046708afdb0fe5548271967f1a67130b7105cd6a828e03909a67" + + "962e0ea1f61deb649f6bc3f4cef3", + class: NullDataTy, + }, + { + // Nulldata with more than max allowed data to be considered standard (so therefore nonstandard) + name: "nulldata exceed max standard push", + script: "RETURN PUSHDATA1 0x51 0x046708afdb0fe5548271967f1a67" + + "130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" + + "046708afdb0fe5548271967f1a67130b7105cd6a828e03909a67" + + "962e0ea1f61deb649f6bc3f4cef308", + class: NonStandardTy, + }, + { + // Almost nulldata, but add an additional opcode after the data to make it nonstandard. + name: "almost nulldata", + script: "RETURN 4 TRUE", + class: NonStandardTy, + }, + // The next few are almost multisig (it is the more complex script type) but with various changes to make it fail. + { + // Multisig but invalid nsigs. + name: "strange 1", + script: "DUP DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da45" + + "329a00357b3a7886211ab414d55a 1 CHECKMULTISIG", + class: NonStandardTy, + }, + { + // Multisig but invalid pubkey. + name: "strange 2", + script: "1 1 1 CHECKMULTISIG", + class: NonStandardTy, + }, + { + // Multisig but no matching npubkeys opcode. + name: "strange 3", + script: "1 DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da4532" + + "9a00357b3a7886211ab414d55a DATA_33 0x0232abdc893e7f0" + + "631364d7fd01cb33d24da45329a00357b3a7886211ab414d55a " + + "CHECKMULTISIG", + class: NonStandardTy, + }, + { + // Multisig but with multisigverify. + name: "strange 4", + script: "1 DATA_33 0x0232abdc893e7f0631364d7fd01cb33d24da4532" + + "9a00357b3a7886211ab414d55a 1 CHECKMULTISIGVERIFY", + class: NonStandardTy, + }, + { + // Multisig but wrong length. + name: "strange 5", + script: "1 CHECKMULTISIG", + class: NonStandardTy, + }, + { + name: "doesn't parse", + script: "DATA_5 0x01020304", + class: NonStandardTy, + }, + { + name: "multisig script with wrong number of pubkeys", + script: "2 " + + "DATA_33 " + + "0x027adf5df7c965a2d46203c781bd4dd8" + + "21f11844136f6673af7cc5a4a05cd29380 " + + "DATA_33 " + + "0x02c08f3de8ee2de9be7bd770f4c10eb0" + + "d6ff1dd81ee96eedd3a9d4aeaf86695e80 " + + "3 CHECKMULTISIG", + class: NonStandardTy, + }, + // New standard segwit script templates. + // { + // // A pay to witness pub key hash pk script. + // name: "Pay To Witness PubkeyHash", + // script: "0 DATA_20 0x1d0f172a0ecb48aee1be1f2687d2963ae33f71a1", + // class: WitnessV0PubKeyHashTy, + // }, + // { + // // A pay to witness scripthash pk script. + // name: "Pay To Witness Scripthash", + // script: "0 DATA_32 0x9f96ade4b41d5433f4eda31e1738ec2b36f6e7d1420d94a6af99801a88f7f7ff", + // class: WitnessV0ScriptHashTy, + // }, +} + +// TestScriptClass ensures all the scripts in scriptClassTests have the expected class. +func TestScriptClass(t *testing.T) { + t.Parallel() + for _, test := range scriptClassTests { + script := mustParseShortForm(test.script) + class := GetScriptClass(script) + if class != test.class { + t.Errorf( + "%s: expected %s got %s (script %x)", test.name, + test.class, class, script, + ) + continue + } + } +} + +// TestStringifyClass ensures the script class string returns the expected string for each script class. +func TestStringifyClass(t *testing.T) { + t.Parallel() + tests := []struct { + name string + class ScriptClass + stringed string + }{ + { + name: "nonstandardty", + class: NonStandardTy, + stringed: "nonstandard", + }, + { + name: "pubkey", + class: PubKeyTy, + stringed: "pubkey", + }, + { + name: "pubkeyhash", + class: PubKeyHashTy, + stringed: "pubkeyhash", + }, + // { + // name: "witnesspubkeyhash", + // class: WitnessV0PubKeyHashTy, + // stringed: "witness_v0_keyhash", + // }, + { + name: "scripthash", + class: ScriptHashTy, + stringed: "scripthash", + }, + // { + // name: "witnessscripthash", + // class: WitnessV0ScriptHashTy, + // stringed: "witness_v0_scripthash", + // }, + { + name: "multisigty", + class: MultiSigTy, + stringed: "multisig", + }, + { + name: "nulldataty", + class: NullDataTy, + stringed: "nulldata", + }, + { + name: "broken", + class: ScriptClass(255), + stringed: "Invalid", + }, + } + for _, test := range tests { + typeString := test.class.String() + if typeString != test.stringed { + t.Errorf( + "%s: got %#q, want %#q", test.name, + typeString, test.stringed, + ) + } + } +} + +// TestNullDataScript tests whether NullDataScript returns a valid script. +func TestNullDataScript(t *testing.T) { + tests := []struct { + name string + data []byte + expected []byte + err error + class ScriptClass + }{ + { + name: "small int", + data: hexToBytes("01"), + expected: mustParseShortForm("RETURN 1"), + err: nil, + class: NullDataTy, + }, + { + name: "max small int", + data: hexToBytes("10"), + expected: mustParseShortForm("RETURN 16"), + err: nil, + class: NullDataTy, + }, + { + name: "data of size before OP_PUSHDATA1 is needed", + data: hexToBytes( + "0102030405060708090a0b0c0d0e0f10111" + + "2131415161718", + ), + expected: mustParseShortForm( + "RETURN 0x18 0x01020304" + + "05060708090a0b0c0d0e0f101112131415161718", + ), + err: nil, + class: NullDataTy, + }, + { + name: "just right", + data: hexToBytes( + "000102030405060708090a0b0c0d0e0f101" + + "112131415161718191a1b1c1d1e1f202122232425262" + + "728292a2b2c2d2e2f303132333435363738393a3b3c3" + + "d3e3f404142434445464748494a4b4c4d4e4f", + ), + expected: mustParseShortForm( + "RETURN PUSHDATA1 0x50 " + + "0x000102030405060708090a0b0c0d0e0f101112131" + + "415161718191a1b1c1d1e1f20212223242526272829" + + "2a2b2c2d2e2f303132333435363738393a3b3c3d3e3" + + "f404142434445464748494a4b4c4d4e4f", + ), + err: nil, + class: NullDataTy, + }, + { + name: "too big", + data: hexToBytes( + "000102030405060708090a0b0c0d0e0f101" + + "112131415161718191a1b1c1d1e1f202122232425262" + + "728292a2b2c2d2e2f303132333435363738393a3b3c3" + + "d3e3f404142434445464748494a4b4c4d4e4f50", + ), + expected: nil, + err: scriptError(ErrTooMuchNullData, ""), + class: NonStandardTy, + }, + } + var e error + for i, test := range tests { + var script []byte + script, e = NullDataScript(test.data) + if e = tstCheckScriptError(e, test.err); e != nil { + t.Errorf( + "NullDataScript: #%d (%s): %v", i, test.name, + e, + ) + continue + } + // Chk that the expected result was returned. + if !bytes.Equal(script, test.expected) { + t.Errorf( + "NullDataScript: #%d (%s) wrong result\n"+ + "got: %x\nwant: %x", i, test.name, script, + test.expected, + ) + continue + } + // Chk that the script has the correct type. + scriptType := GetScriptClass(script) + if scriptType != test.class { + t.Errorf( + "GetScriptClass: #%d (%s) wrong result -- "+ + "got: %v, want: %v", i, test.name, scriptType, + test.class, + ) + continue + } + } +} diff --git a/pkg/txsizes/size.go b/pkg/txsizes/size.go new file mode 100644 index 0000000..bad5709 --- /dev/null +++ b/pkg/txsizes/size.go @@ -0,0 +1,155 @@ +// Package txsizes Copyright (c) 2016 The btcsuite developers +package txsizes + +import ( + "github.com/p9c/p9/pkg/blockchain" + h "github.com/p9c/p9/pkg/util/helpers" + "github.com/p9c/p9/pkg/wire" +) + +// Worst case script and input/output size estimates. +const ( + // RedeemP2PKHSigScriptSize is the worst case (largest) serialize size of a transaction input script that redeems a + // compressed P2PKH output. It is calculated as: + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33 + // P2PKHPkScriptSize is the size of a transaction output script that pays to a compressed pubkey hash. It is + // calculated as: + // + // - OP_DUP + // - OP_HASH160 + // - OP_DATA_20 + // - 20 bytes pubkey hash + // - OP_EQUALVERIFY + // - OP_CHECKSIG + P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1 + // RedeemP2PKHInputSize is the worst case (largest) serialize size of a transaction input redeeming a compressed + // P2PKH output. It is calculated as: + // + // - 32 bytes previous tx + // - 4 bytes output index + // - 1 byte compact int encoding value 107 + // - 107 bytes signature script + // - 4 bytes sequence + RedeemP2PKHInputSize = 32 + 4 + 1 + RedeemP2PKHSigScriptSize + 4 + // P2PKHOutputSize is the serialize size of a transaction output with a P2PKH output script. It is calculated as: + // + // - 8 bytes output value + // - 1 byte compact int encoding value 25 + // - 25 bytes P2PKH output script + P2PKHOutputSize = 8 + 1 + P2PKHPkScriptSize + // // P2WPKHPkScriptSize is the size of a transaction output script that pays to a + // // witness pubkey hash. It is calculated as: + // // + // // - OP_0 + // // - OP_DATA_20 + // // - 20 bytes pubkey hash + // P2WPKHPkScriptSize = 1 + 1 + 20 + // // P2WPKHOutputSize is the serialize size of a transaction output with a P2WPKH output script. It is calculated as: + // // + // // - 8 bytes output value + // // - 1 byte compact int encoding value 22 + // // - 22 bytes P2PKH output script + // P2WPKHOutputSize = 8 + 1 + P2WPKHPkScriptSize + // // RedeemP2WPKHScriptSize is the size of a transaction input script that spends + // // a pay-to-witness-public-key hash (P2WPKH). The redeem script for P2WPKH + // // spends MUST be empty. + // RedeemP2WPKHScriptSize = 0 + // // RedeemP2WPKHInputSize is the worst case size of a transaction input redeeming a P2WPKH output. It is calculated + // // as: + // // + // // - 32 bytes previous tx + // // - 4 bytes output index + // // - 1 byte encoding empty redeem script + // // - 0 bytes redeem script + // // - 4 bytes sequence + // RedeemP2WPKHInputSize = 32 + 4 + 1 + RedeemP2WPKHScriptSize + 4 + // // RedeemNestedP2WPKHScriptSize is the worst case size of a transaction input + // // script that redeems a pay-to-witness-key hash nested in P2SH (P2SH-P2WPKH). + // // It is calculated as: + // // + // // - 1 byte compact int encoding value 22 + // // - OP_0 + // // - 1 byte compact int encoding value 20 + // // - 20 byte key hash + // RedeemNestedP2WPKHScriptSize = 1 + 1 + 1 + 20 + // // RedeemNestedP2WPKHInputSize is the worst case size of a transaction input redeeming a P2SH-P2WPKH output. It is + // // calculated as: + // // + // // - 32 bytes previous tx + // // - 4 bytes output index + // // - 1 byte compact int encoding value 23 + // // - 23 bytes redeem script (scriptSig) + // // - 4 bytes sequence + // RedeemNestedP2WPKHInputSize = 32 + 4 + 1 + RedeemNestedP2WPKHScriptSize + 4 + // // RedeemP2WPKHInputWitnessWeight is the worst case weight of a witness for + // // spending P2WPKH and nested P2WPKH outputs. It is calculated as: + // // + // // - 1 wu compact int encoding value 2 (number of items) + // // - 1 wu compact int encoding value 73 + // // - 72 wu DER signature + 1 wu sighash + // // - 1 wu compact int encoding value 33 + // // - 33 wu serialized compressed pubkey + // RedeemP2WPKHInputWitnessWeight = 1 + 1 + 73 + 1 + 33 +) + +// EstimateSerializeSize returns a worst case serialize size estimate for a signed transaction that spends inputCount +// number of compressed P2PKH outputs and contains each transaction output from txOuts. The estimated size is +// incremented for an additional P2PKH change output if addChangeOutput is true. +func EstimateSerializeSize(inputCount int, txOuts []*wire.TxOut, addChangeOutput bool) int { + changeSize := 0 + outputCount := len(txOuts) + if addChangeOutput { + changeSize = P2PKHOutputSize + outputCount++ + } + // 8 additional bytes are for version and locktime + return 8 + wire.VarIntSerializeSize(uint64(inputCount)) + + wire.VarIntSerializeSize(uint64(outputCount)) + + inputCount*RedeemP2PKHInputSize + + h.SumOutputSerializeSizes(txOuts) + + changeSize +} + +// EstimateVirtualSize returns a worst case virtual size estimate for a signed transaction that spends the given number +// of P2PKH, P2WPKH and (nested) P2SH-P2WPKH outputs, and contains each transaction output from txOuts. The estimate is +// incremented for an additional P2PKH change output if addChangeOutput is true. +func EstimateVirtualSize(numP2PKHIns, numP2WPKHIns, numNestedP2WPKHIns int, + txOuts []*wire.TxOut, addChangeOutput bool, +) int { + changeSize := 0 + // outputCount := len(txOuts) + if addChangeOutput { + // We are always using P2WPKH as change output. + changeSize = P2PKHOutputSize + // outputCount++ + } + // Version 4 bytes + LockTime 4 bytes + Serialized var int size for the number of transaction inputs and outputs + + // size of redeem scripts + the size out the serialized outputs and change. + baseSize := 8 + + wire.VarIntSerializeSize( + uint64(numP2PKHIns+numP2WPKHIns+numNestedP2WPKHIns), + ) + + wire.VarIntSerializeSize(uint64(len(txOuts))) + + numP2PKHIns*RedeemP2PKHInputSize + + // numP2WPKHIns*RedeemP2PKHInputSize + + // numNestedP2WPKHIns*RedeemNestedP2WPKHInputSize + + h.SumOutputSerializeSizes(txOuts) + + changeSize + // If this transaction has any witness inputs, we must count the witness data. + witnessWeight := 0 + // if numP2WPKHIns+numNestedP2WPKHIns > 0 { + // // Additional 2 weight units for segwit marker + flag. + // witnessWeight = 2 + + // wire.VarIntSerializeSize( + // uint64(numP2WPKHIns+numNestedP2WPKHIns)) + + // numP2WPKHIns*RedeemP2WPKHInputWitnessWeight + + // numNestedP2WPKHIns*RedeemP2WPKHInputWitnessWeight + // } + // We add 3 to the witness weight to make sure the result is always rounded up. + return baseSize + (witnessWeight+3)/blockchain.WitnessScaleFactor +} diff --git a/pkg/txsizes/size_test.go b/pkg/txsizes/size_test.go new file mode 100644 index 0000000..7e3b864 --- /dev/null +++ b/pkg/txsizes/size_test.go @@ -0,0 +1,164 @@ +package txsizes_test + +import ( + "bytes" + "encoding/hex" + "testing" + + . "github.com/p9c/p9/pkg/txsizes" + "github.com/p9c/p9/pkg/wire" +) + +const ( + p2pkhScriptSize = P2PKHPkScriptSize + p2shScriptSize = 23 +) + +func makeInts(value int, n int) []int { + v := make([]int, n) + for i := range v { + v[i] = value + } + return v +} +func TestEstimateSerializeSize(t *testing.T) { + tests := []struct { + InputCount int + OutputScriptLengths []int + AddChangeOutput bool + ExpectedSizeEstimate int + }{ + 0: {1, []int{}, false, 159}, + 1: {1, []int{p2pkhScriptSize}, false, 193}, + 2: {1, []int{}, true, 193}, + 3: {1, []int{p2pkhScriptSize}, true, 227}, + 4: {1, []int{p2shScriptSize}, false, 191}, + 5: {1, []int{p2shScriptSize}, true, 225}, + 6: {2, []int{}, false, 308}, + 7: {2, []int{p2pkhScriptSize}, false, 342}, + 8: {2, []int{}, true, 342}, + 9: {2, []int{p2pkhScriptSize}, true, 376}, + 10: {2, []int{p2shScriptSize}, false, 340}, + 11: {2, []int{p2shScriptSize}, true, 374}, + // 0xfd is discriminant for 16-bit compact ints, compact int + // total size increases from 1 byte to 3. + 12: {1, makeInts(p2pkhScriptSize, 0xfc), false, 8727}, + 13: {1, makeInts(p2pkhScriptSize, 0xfd), false, 8727 + P2PKHOutputSize + 2}, + 14: {1, makeInts(p2pkhScriptSize, 0xfc), true, 8727 + P2PKHOutputSize + 2}, + 15: {0xfc, []int{}, false, 37558}, + 16: {0xfd, []int{}, false, 37558 + RedeemP2PKHInputSize + 2}, + } + for i, test := range tests { + outputs := make([]*wire.TxOut, 0, len(test.OutputScriptLengths)) + for _, l := range test.OutputScriptLengths { + outputs = append(outputs, &wire.TxOut{PkScript: make([]byte, l)}) + } + actualEstimate := EstimateSerializeSize(test.InputCount, outputs, test.AddChangeOutput) + if actualEstimate != test.ExpectedSizeEstimate { + t.Errorf("Test %d: Got %v: Expected %v", i, actualEstimate, test.ExpectedSizeEstimate) + } + } +} +func TestEstimateVirtualSize(t *testing.T) { + type estimateVSizeTest struct { + tx func() (*wire.MsgTx, error) + p2wpkhIns int + nestedp2wpkhIns int + p2pkhIns int + change bool + result int + } + // TODO(halseth): add tests for more combination out inputs/outputs. + tests := []estimateVSizeTest{ + // Spending P2WPKH to two outputs. Example adapted from example in BIP-143. + { + tx: func() (*wire.MsgTx, error) { + txHex := "01000000000101ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac0247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000" + b, e := hex.DecodeString(txHex) + if e != nil { + return nil, e + } + tx := &wire.MsgTx{} + e = tx.Deserialize(bytes.NewReader(b)) + if e != nil { + return nil, e + } + return tx, nil + }, + p2wpkhIns: 1, + result: 147, + }, + { + // Spending P2SH-P2WPKH to two outputs. Example adapted from example in BIP-143. + tx: func() (*wire.MsgTx, error) { + txHex := "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000" + b, e := hex.DecodeString(txHex) + if e != nil { + return nil, e + } + tx := &wire.MsgTx{} + e = tx.Deserialize(bytes.NewReader(b)) + if e != nil { + return nil, e + } + return tx, nil + }, + nestedp2wpkhIns: 1, + result: 170, + }, + { + // Spendin P2WPKH to on output, adding one change output. We reuse + // the transaction spending to two outputs, removing one of them. + tx: func() (*wire.MsgTx, error) { + txHex := "01000000000101ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac0247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000" + b, e := hex.DecodeString(txHex) + if e != nil { + return nil, e + } + tx := &wire.MsgTx{} + e = tx.Deserialize(bytes.NewReader(b)) + if e != nil { + return nil, e + } + // Only keep the first output. + tx.TxOut = []*wire.TxOut{tx.TxOut[0]} + return tx, nil + }, + p2wpkhIns: 1, + change: true, + result: 144, + }, + { + // Spending one P2PKH to two P2PKH outputs (no witness data). + tx: func() (*wire.MsgTx, error) { + txHex := "0100000001a4c91c9720157a5ee582a7966471d9c70d0a860fa7757b4c42a535a12054a4c9000000006c493046022100d49c452a00e5b1213ac84d92269510a05a584a4d0949bd7d0ad4e3408ac8e80a022100bf98707ffaf1eb9dff146f7da54e68651c0a27e3653ec3882b7a95202328579c01210332d98672a4246fe917b9c724c339e757d46b1ffde3fb27fdc680b4bb29b6ad59ffffffff02a0860100000000001976a9144fb55ee0524076acd4c14e7773561e4c298c8e2788ac20688a0b000000001976a914cb7f6bb8e95a2cd06423932cfbbce73d16a18df088ac00000000" + b, e := hex.DecodeString(txHex) + if e != nil { + return nil, e + } + tx := &wire.MsgTx{} + e = tx.Deserialize(bytes.NewReader(b)) + if e != nil { + return nil, e + } + return tx, nil + }, + p2pkhIns: 1, + result: 227, + }, + } + for _, test := range tests { + tx, e := test.tx() + if e != nil { + t.Fatalf("unable to get test tx: %v", e) + } + est := EstimateVirtualSize(test.p2pkhIns, test.p2wpkhIns, + test.nestedp2wpkhIns, tx.TxOut, test.change, + ) + if est != test.result { + t.Fatalf("expected estimated vsize to be %d, "+ + "instead got %d", test.result, est, + ) + } + } +} diff --git a/pkg/upnp/log.go b/pkg/upnp/log.go new file mode 100644 index 0000000..e6d217d --- /dev/null +++ b/pkg/upnp/log.go @@ -0,0 +1,43 @@ +package upnp + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/upnp/upnp.go b/pkg/upnp/upnp.go new file mode 100644 index 0000000..11a3983 --- /dev/null +++ b/pkg/upnp/upnp.go @@ -0,0 +1,415 @@ +package upnp + +// UPNP code taken from Taipei Torrent license is below: +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// Just enough UPnP to be able to forward ports +import ( + "bytes" + "encoding/xml" + "errors" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +// NAT is an interface representing a NAT traversal options for example UPNP or NAT-PMP. It provides methods to query +// and manipulate this traversal to allow access to services. +type NAT interface { + // GetExternalAddress - Get the external address from outside the NAT. + GetExternalAddress() (addr net.IP, e error) + // AddPortMapping - Add a port mapping for protocol ( + // "udp" or "tcp") from external port to internal port with description lasting + // for timeout. + AddPortMapping( + protocol string, externalPort, internalPort int, + description string, timeout int, + ) (mappedExternalPort int, e error) + // DeletePortMapping - Remove a previously added port mapping from external + // port to internal port. + DeletePortMapping( + protocol string, externalPort, + internalPort int, + ) (e error) +} +type upnpNAT struct { + serviceURL string + ourIP string +} + +// Discover searches the local network for a UPnP router returning a NAT for the network if so, nil if not. +func Discover() (nat NAT, e error) { + var ssdp *net.UDPAddr + ssdp, e = net.ResolveUDPAddr("udp4", "239.255.255.250:1900") + if e != nil { + E.Ln(e) + return + } + var conn net.PacketConn + conn, e = net.ListenPacket("udp4", ":0") + if e != nil { + E.Ln(e) + return + } + socket := conn.(*net.UDPConn) + defer func() { + if e = socket.Close(); E.Chk(e) { + } + }() + e = socket.SetDeadline(time.Now().Add(3 * time.Second)) + if E.Chk(e) { + return + } + st := "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n" + buf := bytes.NewBufferString( + "M-SEARCH * HTTP/1.1\r\n" + + "HOST: 239.255.255.250:1900\r\n" + + st + + "MAN: \"ssdp:discover\"\r\n" + + "MX: 2\r\n\r\n", + ) + message := buf.Bytes() + answerBytes := make([]byte, 1024) + for i := 0; i < 3; i++ { + _, e = socket.WriteToUDP(message, ssdp) + if e != nil { + E.Ln(e) + return + } + var n int + n, _, e = socket.ReadFromUDP(answerBytes) + if e != nil { + E.Ln(e) + continue + // socket.Close() + // return + } + answer := string(answerBytes[0:n]) + if !strings.Contains(answer, "\r\n"+st) { + continue + } + // HTTP header field names are case-insensitive. http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 + locString := "\r\nlocation: " + locIndex := strings.Index(strings.ToLower(answer), locString) + if locIndex < 0 { + continue + } + loc := answer[locIndex+len(locString):] + endIndex := strings.Index(loc, "\r\n") + if endIndex < 0 { + continue + } + locURL := loc[0:endIndex] + var serviceURL string + serviceURL, e = getServiceURL(locURL) + if e != nil { + E.Ln(e) + return + } + var ourIP string + ourIP, e = getOurIP() + if e != nil { + E.Ln(e) + return + } + nat = &upnpNAT{serviceURL: serviceURL, ourIP: ourIP} + return + } + e = errors.New("UPnP port routeable failed") + return +} + +// service represents the Service type in an UPnP xml description. Only the parts we care about are present and thus the +// xml may have more fields than present in the structure. +type service struct { + ServiceType string `xml:"serviceType"` + ControlURL string `xml:"controlURL"` +} + +// deviceList represents the deviceList type in an UPnP xml description. Only the parts we care about are present and +// thus the xml may have more fields than present in the structure. +type deviceList struct { + XMLName xml.Name `xml:"deviceList"` + Device []device `xml:"device"` +} + +// serviceList represents the serviceList type in an UPnP xml description. Only the parts we care about are present and +// thus the xml may have more fields than present in the structure. +type serviceList struct { + XMLName xml.Name `xml:"serviceList"` + Service []service `xml:"service"` +} + +// device represents the device type in an UPnP xml description. Only the parts we care about are present and thus the +// xml may have more fields than present in the structure. +type device struct { + XMLName xml.Name `xml:"device"` + DeviceType string `xml:"deviceType"` + DeviceList deviceList `xml:"deviceList"` + ServiceList serviceList `xml:"serviceList"` +} + +// specVersion represents the specVersion in a UPnP xml description. Only the parts we care about are present and thus +// the xml may have more fields than present in the structure. +type specVersion struct { + XMLName xml.Name `xml:"specVersion"` + Major int `xml:"major"` + Minor int `xml:"minor"` +} + +// root represents the Root document for a UPnP xml description. Only the parts we care about are present and thus the +// xml may have more fields than present in the structure. +type root struct { + XMLName xml.Name `xml:"root"` + SpecVersion specVersion + Device device +} + +// getChildDevice searches the children of device for a device with the given type. +func getChildDevice(d *device, deviceType string) *device { + for i := range d.DeviceList.Device { + if d.DeviceList.Device[i].DeviceType == deviceType { + return &d.DeviceList.Device[i] + } + } + return nil +} + +// getChildDevice searches the service list of device for a service with the given type. +func getChildService(d *device, serviceType string) *service { + for i := range d.ServiceList.Service { + if d.ServiceList.Service[i].ServiceType == serviceType { + return &d.ServiceList.Service[i] + } + } + return nil +} + +// getOurIP returns a best guess at what the local IP is. +func getOurIP() (ip string, e error) { + var hostname string + hostname, e = os.Hostname() + if e != nil { + E.Ln(e) + return + } + return net.LookupCNAME(hostname) +} + +// getServiceURL parses the xml description at the given root url to find the url for the WANIPConnection service to be +// used for port forwarding. +func getServiceURL(rootURL string) (url string, e error) { + var re *http.Response + re, e = http.Get(rootURL) + if e != nil { + E.Ln(e) + return + } + defer func() { + if e = re.Body.Close(); E.Chk(e) { + } + }() + if re.StatusCode >= 400 { + e = errors.New(string(rune(re.StatusCode))) + return + } + var r root + e = xml.NewDecoder(re.Body).Decode(&r) + if e != nil { + E.Ln(e) + return + } + a := &r.Device + if a.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" { + e = errors.New("no InternetGatewayDevice") + return + } + b := getChildDevice(a, "urn:schemas-upnp-org:device:WANDevice:1") + if b == nil { + e = errors.New("no WANDevice") + return + } + c := getChildDevice(b, "urn:schemas-upnp-org:device:WANConnectionDevice:1") + if c == nil { + e = errors.New("no WANConnectionDevice") + return + } + d := getChildService(c, "urn:schemas-upnp-org:service:WANIPConnection:1") + if d == nil { + e = errors.New("no WANIPConnection") + return + } + url = combineURL(rootURL, d.ControlURL) + return +} + +// combineURL appends subURL onto rootURL. +func combineURL(rootURL, subURL string) string { + protocolEnd := "://" + protoEndIndex := strings.Index(rootURL, protocolEnd) + a := rootURL[protoEndIndex+len(protocolEnd):] + rootIndex := strings.Index(a, "/") + return rootURL[0:protoEndIndex+len(protocolEnd)+rootIndex] + subURL +} + +// soapBody represents the element in a SOAP reply. fields we don't care about are elided. +type soapBody struct { + XMLName xml.Name `xml:"Body"` + Data []byte `xml:",innerxml"` +} + +// soapEnvelope represents the element in a SOAP reply. fields we don't care about are elided. +type soapEnvelope struct { + XMLName xml.Name `xml:"Envelope"` + Body soapBody `xml:"Body"` +} + +// soapRequests performs a soap request with the given parameters and returns the xml replied stripped of the soap +// headers. in the case that the request is unsuccessful the an error is returned. +func soapRequest(url, function, message string) (replyXML []byte, e error) { + fullMessage := "" + + "\r\n" + + "" + message + "" + var req *http.Request + req, e = http.NewRequest("POST", url, strings.NewReader(fullMessage)) + if e != nil { + E.Ln(e) + return nil, e + } + req.Header.Set("Content-Type", "text/xml ; charset=\"utf-8\"") + req.Header.Set("User-Agent", "Darwin/10.0.0, UPnP/1.0, MiniUPnPc/1.3") + // req.Header.Set("Transfer-Encoding", "chunked") + req.Header.Set("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#"+function+"\"") + req.Header.Set("Connection", "Close") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + var r *http.Response + r, e = http.DefaultClient.Do(req) + if e != nil { + E.Ln(e) + return nil, e + } + if r.Body != nil { + defer func() { + if e = r.Body.Close(); E.Chk(e) { + } + }() + } + if r.StatusCode >= 400 { + // L.Stderr(function, r.StatusCode) + e = errors.New("Error " + strconv.Itoa(r.StatusCode) + " for " + function) + r = nil + return + } + var reply soapEnvelope + e = xml.NewDecoder(r.Body).Decode(&reply) + if e != nil { + E.Ln(e) + return nil, e + } + return reply.Body.Data, nil +} + +// getExternalIPAddressResponse represents the XML response to a GetExternalIPAddress SOAP request. +type getExternalIPAddressResponse struct { + XMLName xml.Name `xml:"GetExternalIPAddressResponse"` + ExternalIPAddress string `xml:"NewExternalIPAddress"` +} + +// GetExternalAddress implements the NAT interface by fetching the external IP from the UPnP router. +func (n *upnpNAT) GetExternalAddress() (addr net.IP, e error) { + message := "\r\n" + var response []byte + response, e = soapRequest(n.serviceURL, "GetExternalIPAddress", message) + if e != nil { + E.Ln(e) + return nil, e + } + var reply getExternalIPAddressResponse + e = xml.Unmarshal(response, &reply) + if e != nil { + E.Ln(e) + return nil, e + } + addr = net.ParseIP(reply.ExternalIPAddress) + if addr == nil { + return nil, errors.New("unable to parse ip address") + } + return addr, nil +} + +// AddPortMapping implements the NAT interface by setting up a port forwarding from the UPnP router to the local machine +// with the given ports and protocol. +func (n *upnpNAT) AddPortMapping( + protocol string, + externalPort, internalPort int, + description string, + timeout int, +) (mappedExternalPort int, e error) { + // A single concatenation would break ARM compilation. + message := "\r\n" + + "" + strconv.Itoa(externalPort) + message += "" + strings.ToUpper(protocol) + "" + message += "" + strconv.Itoa(internalPort) + "" + + "" + n.ourIP + "" + + "1" + message += description + + "" + strconv.Itoa(timeout) + + "" + var response []byte + response, e = soapRequest(n.serviceURL, "AddPortMapping", message) + if e != nil { + E.Ln(e) + return + } + // TODO: check response to see if the port was forwarded + // + // If the port was not wildcard we don't get an reply with the port in it. Not sure about wildcard yet. miniupnpc + // just checks for error codes here. + mappedExternalPort = externalPort + _ = response + return +} + +// DeletePortMapping implements the NAT interface by removing up a port forwarding from the UPnP router to the local +// machine with the given ports and. +func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (e error) { + message := "\r\n" + + "" + strconv.Itoa(externalPort) + + "" + strings.ToUpper(protocol) + "" + + "" + var response []byte + response, e = soapRequest(n.serviceURL, "DeletePortMapping", message) + if e != nil { + E.Ln(e) + return + } + // TODO: check response to see if the port was deleted + _ = response + return +} diff --git a/pkg/util/LICENSE b/pkg/util/LICENSE new file mode 100755 index 0000000..054809b --- /dev/null +++ b/pkg/util/LICENSE @@ -0,0 +1,13 @@ +ISC License +Copyright (c) 2013-2017 The btcsuite developers +Copyright (c) 2016-2017 The Lightning Network Developers +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/pkg/util/README.md b/pkg/util/README.md new file mode 100755 index 0000000..51a4658 --- /dev/null +++ b/pkg/util/README.md @@ -0,0 +1,28 @@ +# btcutil + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/btcutil) + +Package btcutil provides bitcoin-specific convenience functions and types. + +A comprehensive suite of tests is provided to ensure proper functionality. +See `test_coverage.txt` for the gocov coverage report. Alternatively, if you are +running a POSIX OS, you can run the `cov_report.sh` script for a real-time +report. + +This package was developed for pod, an alternative full-node implementation of +bitcoin which is under active development by Conformal. Although it was +primarily written for pod, this package has intentionally been designed so it +can be used as a standalone package for any projects needing the functionality +provided. + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/btcutil +``` + +## License + +Package btcutil is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/util/_infernal_test.go b/pkg/util/_infernal_test.go new file mode 100644 index 0000000..750dacb --- /dev/null +++ b/pkg/util/_infernal_test.go @@ -0,0 +1,91 @@ +package util + +import ( + "github.com/p9c/p9/pkg/btcaddr" + "golang.org/x/crypto/ripemd160" + + "github.com/p9c/p9/pkg/appdata" + "github.com/p9c/p9/pkg/base58" + "github.com/p9c/p9/pkg/bech32" + ec "github.com/p9c/p9/pkg/ecc" +) + +// TstAppDataDir makes the internal appDataDir function available to the test package. +func TstAppDataDir(goos, appName string, roaming bool) string { + return appdata.GetDataDir(goos, appName, roaming) +} + +// TstAddressPubKeyHash makes an PubKeyHash, setting the unexported fields with the parameters hash and netID. +func TstAddressPubKeyHash(hash [ripemd160.Size]byte, + netID byte, +) *btcaddr.PubKeyHash { + return &btcaddr.PubKeyHash{ + Hash: hash, + NetID: netID, + } +} + +// TstAddressScriptHash makes an ScriptHash, setting the unexported fields with the parameters hash and netID. +func TstAddressScriptHash(hash [ripemd160.Size]byte, + netID byte, +) *btcaddr.ScriptHash { + return &btcaddr.ScriptHash{ + Hash: hash, + NetID: netID, + } +} + +// // TstAddressWitnessPubKeyHash creates an AddressWitnessPubKeyHash, initiating +// // the fields as given. +// func TstAddressWitnessPubKeyHash(version byte, program [20]byte, +// hrp string) *AddressWitnessPubKeyHash { +// return &AddressWitnessPubKeyHash{ +// hrp: hrp, +// witnessVersion: version, +// witnessProgram: program, +// } +// } +// +// // TstAddressWitnessScriptHash creates an AddressWitnessScriptHash, initiating +// // the fields as given. +// func TstAddressWitnessScriptHash(version byte, program [32]byte, +// hrp string) *AddressWitnessScriptHash { +// return &AddressWitnessScriptHash{ +// hrp: hrp, +// witnessVersion: version, +// witnessProgram: program, +// } +// } + +// TstAddressPubKey makes an PubKey, setting the unexported fields with the parameters. +func TstAddressPubKey(serializedPubKey []byte, pubKeyFormat btcaddr.PubKeyFormat, + netID byte, +) *btcaddr.PubKey { + pubKey, _ := ec.ParsePubKey(serializedPubKey, ec.S256()) + return &btcaddr.PubKey{ + PubKeyFormat: pubKeyFormat, + pubKey: pubKey, + pubKeyHashID: netID, + } +} + +// TstAddressSAddr returns the expected script address bytes for P2PKH and P2SH bitcoin addresses. +func TstAddressSAddr(addr string) []byte { + decoded := base58.Decode(addr) + return decoded[1 : 1+ripemd160.Size] +} + +// TstAddressSegwitSAddr returns the expected witness program bytes for bech32 +// encoded P2WPKH and P2WSH bitcoin addresses. +func TstAddressSegwitSAddr(addr string) []byte { + _, data, e := bech32.Decode(addr) + if e != nil { + return []byte{} + } + // First byte is version, rest is base 32 encoded data. + data, e = bech32.ConvertBits(data[1:], 5, 8, false) + if e != nil { + return []byte{} + } + return data +} diff --git a/pkg/util/atom/atom.go b/pkg/util/atom/atom.go new file mode 100644 index 0000000..5e7f157 --- /dev/null +++ b/pkg/util/atom/atom.go @@ -0,0 +1,194 @@ +package atom + +import ( + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "time" + + "go.uber.org/atomic" + + "github.com/p9c/p9/pkg/btcjson" + "github.com/p9c/p9/pkg/chainhash" +) + +// import all the atomics from uber atomic +type ( + Int32 struct{ *atomic.Int32 } + Int64 struct{ *atomic.Int64 } + Uint32 struct{ *atomic.Uint32 } + Uint64 struct{ *atomic.Uint64 } + Bool struct{ *atomic.Bool } + Float64 struct{ *atomic.Float64 } + Duration struct{ *atomic.Duration } + Value struct{ *atomic.Value } +) + +// The following are types added for handling cryptocurrency data for +// ParallelCoin + +// Time is an atomic wrapper around time.Time +// https://godoc.org/time#Time +type Time struct { + v *Int64 +} + +// NewTime creates a Time. +func NewTime(tt time.Time) *Time { + t := &Int64{atomic.NewInt64(tt.UnixNano())} + return &Time{v: t} +} + +// Load atomically loads the wrapped value. +func (at *Time) Load() time.Time { + return time.Unix(0, at.v.Load()) +} + +// Store atomically stores the passed value. +func (at *Time) Store(n time.Time) { + at.v.Store(n.UnixNano()) +} + +// Add atomically adds to the wrapped time.Duration and returns the new value. +func (at *Time) Add(n time.Time) time.Time { + return time.Unix(0, at.v.Add(n.UnixNano())) +} + +// Sub atomically subtracts from the wrapped time.Duration and returns the new value. +func (at *Time) Sub(n time.Time) time.Time { + return time.Unix(0, at.v.Sub(n.UnixNano())) +} + +// Swap atomically swaps the wrapped time.Duration and returns the old value. +func (at *Time) Swap(n time.Time) time.Time { + return time.Unix(0, at.v.Swap(n.UnixNano())) +} + +// CAS is an atomic compare-and-swap. +func (at *Time) CAS(old, new time.Time) bool { + return at.v.CAS(old.UnixNano(), new.UnixNano()) +} + +// Hash is an atomic wrapper around chainhash.Hash +// Note that there isn't really any reason to have CAS or arithmetic or +// comparisons as it is fine to do these non-atomically between Load / Store and +// they are (slightly) long operations) +type Hash struct { + *Value +} + +// NewHash creates a Hash. +func NewHash(tt chainhash.Hash) *Hash { + t := &Value{ + Value: &atomic.Value{}, + } + t.Store(tt) + return &Hash{Value: t} +} + +// Load atomically loads the wrapped value. +// The returned value copied so as to prevent mutation by concurrent users +// of the atomic, as arrays, slices and maps are pass-by-reference variables +func (at *Hash) Load() chainhash.Hash { + o := at.Value.Load().(chainhash.Hash) + var v chainhash.Hash + copy(v[:], o[:]) + return v +} + +// Store atomically stores the passed value. +// The passed value is copied so further mutations are not propagated. +func (at *Hash) Store(h chainhash.Hash) { + var v chainhash.Hash + copy(v[:], h[:]) + at.Value.Store(v) +} + +// Swap atomically swaps the wrapped chainhash.Hash and returns the old value. +func (at *Hash) Swap(n chainhash.Hash) chainhash.Hash { + o := at.Value.Load().(chainhash.Hash) + at.Value.Store(n) + return o +} + +// Address is an atomic wrapper around util.Address +type Address struct { + *atomic.String + ForNet *chaincfg.Params +} + +// NewAddress creates a Hash. +func NewAddress(tt btcaddr.Address, forNet *chaincfg.Params) *Address { + t := atomic.NewString(tt.EncodeAddress()) + return &Address{String: t, ForNet: forNet} +} + +// Load atomically loads the wrapped value. +func (at *Address) Load() btcaddr.Address { + addr, e := btcaddr.Decode(at.String.Load(), at.ForNet) + if e != nil { + return nil + } + return addr +} + +// Store atomically stores the passed value. +// The passed value is copied so further mutations are not propagated. +func (at *Address) Store(h btcaddr.Address) { + at.String.Store(h.EncodeAddress()) +} + +// Swap atomically swaps the wrapped util.Address and returns the old value. +func (at *Address) Swap(n btcaddr.Address) btcaddr.Address { + o := at.Load() + at.Store(n) + return o +} + +// ListTransactionsResult is an atomic wrapper around +// []btcjson.ListTransactionsResult +type ListTransactionsResult struct { + v *Value +} + +// NewListTransactionsResult creates a btcjson.ListTransactionsResult. +func NewListTransactionsResult(ltr []btcjson.ListTransactionsResult) *ListTransactionsResult { + t := &Value{ + Value: &atomic.Value{}, + } + v := make([]btcjson.ListTransactionsResult, len(ltr)) + copy(v, ltr) + t.Store(v) + return &ListTransactionsResult{v: t} +} + +// Load atomically loads the wrapped value. +// Note that it is copied and the stored value remains as it is +func (at *ListTransactionsResult) Load() []btcjson.ListTransactionsResult { + ltr := at.v.Load().([]btcjson.ListTransactionsResult) + v := make([]btcjson.ListTransactionsResult, len(ltr)) + copy(v, ltr) + return v +} + +// Store atomically stores the passed value. +// Note that it is copied and the passed value remains as it is +func (at *ListTransactionsResult) Store(ltr []btcjson.ListTransactionsResult) { + v := make([]btcjson.ListTransactionsResult, len(ltr)) + copy(v, ltr) + at.v.Store(v) +} + +// Swap atomically swaps the wrapped chainhash.ListTransactionsResult and +// returns the old value. +func (at *ListTransactionsResult) Swap( + n []btcjson.ListTransactionsResult, +) []btcjson.ListTransactionsResult { + o := at.v.Load().([]btcjson.ListTransactionsResult) + at.v.Store(n) + return o +} + +// Len returns the length of the []btcjson.ListTransactionsResult +func (at *ListTransactionsResult) Len() int { + return len(at.v.Load().([]btcjson.ListTransactionsResult)) +} diff --git a/pkg/util/certgen.go b/pkg/util/certgen.go new file mode 100644 index 0000000..5d03d66 --- /dev/null +++ b/pkg/util/certgen.go @@ -0,0 +1,125 @@ +package util + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + _ "crypto/sha512" // Needed for RegisterHash in init + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "os" + "time" +) + +// NewTLSCertPair returns a new PEM-encoded x.509 certificate pair based on a 521-bit ECDSA private key. The machine's +// local interface addresses and all variants of IPv4 and IPv6 localhost are included as valid IP addresses. +func NewTLSCertPair(organization string, validUntil time.Time, extraHosts []string) (cert, key []byte, e error) { + now := time.Now() + if validUntil.Before(now) { + return nil, nil, errors.New("validUntil would create an already-expired certificate") + } + priv, e := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if e != nil { + return nil, nil, e + } + // end of ASN.1 time + endOfTime := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC) + if validUntil.After(endOfTime) { + validUntil = endOfTime + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, e := rand.Int(rand.Reader, serialNumberLimit) + if e != nil { + return nil, nil, fmt.Errorf("failed to generate serial number: %s", e) + } + host, e := os.Hostname() + if e != nil { + return nil, nil, e + } + ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")} + dnsNames := []string{host} + if host != "localhost" { + dnsNames = append(dnsNames, "localhost") + } + addIP := func(ipAddr net.IP) { + for _, ip := range ipAddresses { + if ip.Equal(ipAddr) { + return + } + } + ipAddresses = append(ipAddresses, ipAddr) + } + addHost := func(host string) { + for _, dnsName := range dnsNames { + if host == dnsName { + return + } + } + dnsNames = append(dnsNames, host) + } + addrs, e := interfaceAddrs() + if e != nil { + return nil, nil, e + } + for _, a := range addrs { + var ipAddr net.IP + ipAddr, _, e = net.ParseCIDR(a.String()) + if e == nil { + addIP(ipAddr) + } + } + for _, hostStr := range extraHosts { + host, _, e = net.SplitHostPort(hostStr) + if e != nil { + host = hostStr + } + if ip := net.ParseIP(host); ip != nil { + addIP(ip) + } else { + addHost(host) + } + } + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{organization}, + CommonName: host, + }, + NotBefore: now.Add(-time.Hour * 24), + NotAfter: validUntil, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | + x509.KeyUsageCertSign, + IsCA: true, // so can sign self. + BasicConstraintsValid: true, + DNSNames: dnsNames, + IPAddresses: ipAddresses, + } + derBytes, e := x509.CreateCertificate( + rand.Reader, &template, + &template, &priv.PublicKey, priv, + ) + if e != nil { + return nil, nil, fmt.Errorf("failed to create certificate: %v", e) + } + certBuf := &bytes.Buffer{} + e = pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if e != nil { + return nil, nil, fmt.Errorf("failed to encode certificate: %v", e) + } + keybytes, e := x509.MarshalECPrivateKey(priv) + if e != nil { + return nil, nil, fmt.Errorf("failed to marshal private key: %v", e) + } + keyBuf := &bytes.Buffer{} + e = pem.Encode(keyBuf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes}) + if e != nil { + return nil, nil, fmt.Errorf("failed to encode private key: %v", e) + } + return certBuf.Bytes(), keyBuf.Bytes(), nil +} diff --git a/pkg/util/certgen_test.go b/pkg/util/certgen_test.go new file mode 100644 index 0000000..9c5bba3 --- /dev/null +++ b/pkg/util/certgen_test.go @@ -0,0 +1,109 @@ +package util_test + +import ( + "crypto/x509" + "encoding/pem" + "net" + "testing" + "time" + + "github.com/p9c/p9/pkg/util" + // "github.com/davecgh/go-spew/spew" +) + +// TestNewTLSCertPair ensures the NewTLSCertPair function works as expected. +func TestNewTLSCertPair(t *testing.T) { + // Certs don't support sub-second precision, so truncate it now to ensure the checks later don't fail due to + // nanosecond precision differences. + validUntil := time.Unix(time.Now().Add(10*365*24*time.Hour).Unix(), 0) + org := "test autogenerated cert" + extraHosts := []string{"testtlscert.bogus", "localhost", "127.0.0.1"} + cert, key, e := util.NewTLSCertPair(org, validUntil, extraHosts) + if e != nil { + t.Fatalf("failed with unexpected error: %v", e) + } + // Ensure the PEM-encoded cert that is returned can be decoded. + pemCert, _ := pem.Decode(cert) + if pemCert == nil { + t.Fatalf("pem.Decode was unable to decode the certificate") + } + // Ensure the PEM-encoded key that is returned can be decoded. + pemKey, _ := pem.Decode(key) + if pemKey == nil { + t.Fatalf("pem.Decode was unable to decode the key") + } + // Ensure the DER-encoded key bytes can be successfully parsed. + _, e = x509.ParseECPrivateKey(pemKey.Bytes) + if e != nil { + t.Fatalf("failed with unexpected error: %v", e) + } + // Ensure the DER-encoded cert bytes can be successfully into an X.509 certificate. + x509Cert, e := x509.ParseCertificate(pemCert.Bytes) + if e != nil { + t.Fatalf("failed with unexpected error: %v", e) + } + // Ensure the specified organization is correct. + x509Orgs := x509Cert.Subject.Organization + if len(x509Orgs) == 0 || x509Orgs[0] != org { + x509Org := "" + if len(x509Orgs) > 0 { + x509Org = x509Orgs[0] + } + t.Fatalf("generated cert organization field mismatch, got "+ + "'%v', want '%v'", x509Org, org, + ) + } + // Ensure the specified valid until value is correct. + if !x509Cert.NotAfter.Equal(validUntil) { + t.Fatalf("generated cert valid until field mismatch, got %v, "+ + "want %v", x509Cert.NotAfter, validUntil, + ) + } + // Ensure the specified extra hosts are present. + for _, host := range extraHosts { + if e := x509Cert.VerifyHostname(host); E.Chk(e) { + t.Fatalf("failed to verify extra host '%s'", host) + } + } + // Ensure that the Common Name is also the first SAN DNS name. + cn := x509Cert.Subject.CommonName + san0 := x509Cert.DNSNames[0] + if cn != san0 { + t.Errorf("common name %s does not match first SAN %s", cn, san0) + } + // Ensure there are no duplicate hosts or IPs. + hostCounts := make(map[string]int) + for _, host := range x509Cert.DNSNames { + hostCounts[host]++ + } + ipCounts := make(map[string]int) + for _, ip := range x509Cert.IPAddresses { + ipCounts[string(ip)]++ + } + for host, count := range hostCounts { + if count != 1 { + t.Errorf("host %s appears %d times in certificate", host, count) + } + } + for ipStr, count := range ipCounts { + if count != 1 { + t.Errorf("ip %s appears %d times in certificate", net.IP(ipStr), count) + } + } + // Ensure the cert can be use for the intended purposes. + if !x509Cert.IsCA { + t.Fatal("generated cert is not a certificate authority") + } + if x509Cert.KeyUsage&x509.KeyUsageKeyEncipherment == 0 { + t.Fatal("generated cert can't be used for key encipherment") + } + if x509Cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { + t.Fatal("generated cert can't be used for digital signatures") + } + if x509Cert.KeyUsage&x509.KeyUsageCertSign == 0 { + t.Fatal("generated cert can't be used for signing other certs") + } + if !x509Cert.BasicConstraintsValid { + t.Fatal("generated cert does not have valid basic constraints") + } +} diff --git a/pkg/util/cfgutil/amount.go b/pkg/util/cfgutil/amount.go new file mode 100644 index 0000000..098ccb9 --- /dev/null +++ b/pkg/util/cfgutil/amount.go @@ -0,0 +1,40 @@ +package cfgutil + +import ( + "github.com/p9c/p9/pkg/amt" + "strconv" + "strings" +) + +// AmountFlag embeds a util.Amount and implements the flags.Marshaler and Unmarshaler interfaces so it can be used as a +// config struct field. +type AmountFlag struct { + amt.Amount +} + +// NewAmountFlag creates an AmountFlag with a default util.Amount. +func NewAmountFlag(defaultValue amt.Amount) *AmountFlag { + return &AmountFlag{defaultValue} +} + +// MarshalFlag satisifes the flags.Marshaler interface. +func (a *AmountFlag) MarshalFlag() (string, error) { + return a.Amount.String(), nil +} + +// UnmarshalFlag satisifes the flags.Unmarshaler interface. +func (a *AmountFlag) UnmarshalFlag(value string) (e error) { + value = strings.TrimSuffix(value, " DUO") + valueF64, e := strconv.ParseFloat(value, 64) + if e != nil { + E.Ln(e) + return e + } + amount, e := amt.NewAmount(valueF64) + if e != nil { + E.Ln(e) + return e + } + a.Amount = amount + return nil +} diff --git a/pkg/util/cfgutil/explicitflags.go b/pkg/util/cfgutil/explicitflags.go new file mode 100644 index 0000000..dc32562 --- /dev/null +++ b/pkg/util/cfgutil/explicitflags.go @@ -0,0 +1,33 @@ +package cfgutil + +// ExplicitString is a string value implementing the flags.Marshaler and flags.Unmarshaler interfaces so it may be used +// as a config struct field. It records whether the value was explicitly set by the flags package. +// +// This is useful when behavior must be modified depending on whether a flag was set by the user or left as a default. +// +// Without recording this, it would be impossible to determine whether flag with a default value was unmodified or +// explicitly set to the default. +type ExplicitString struct { + Value string + explicitlySet bool +} + +// NewExplicitString creates a string flag with the provided default value. +func NewExplicitString(defaultValue string) *ExplicitString { + return &ExplicitString{Value: defaultValue, explicitlySet: false} +} + +// ExplicitlySet returns whether the flag was explicitly set through the flags.Unmarshaler interface. +func (es *ExplicitString) ExplicitlySet() bool { return es.explicitlySet } + +// MarshalFlag implements the flags.Marshaler interface. +func (es *ExplicitString) MarshalFlag() (string, error) { + return es.Value, nil +} + +// UnmarshalFlag implements the flags.Unmarshaler interface. +func (es *ExplicitString) UnmarshalFlag(value string) (e error) { + es.Value = value + es.explicitlySet = true + return nil +} diff --git a/pkg/util/cfgutil/file.go b/pkg/util/cfgutil/file.go new file mode 100644 index 0000000..8458400 --- /dev/null +++ b/pkg/util/cfgutil/file.go @@ -0,0 +1,18 @@ +package cfgutil + +import ( + "os" +) + +// FileExists reports whether the named file or directory exists. +func FileExists(filePath string) (bool, error) { + _, e := os.Stat(filePath) + if e != nil { + E.Ln(e) + if os.IsNotExist(e) { + return false, nil + } + return false, e + } + return true, nil +} diff --git a/pkg/util/cfgutil/log.go b/pkg/util/cfgutil/log.go new file mode 100644 index 0000000..e82dc5d --- /dev/null +++ b/pkg/util/cfgutil/log.go @@ -0,0 +1,43 @@ +package cfgutil + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/util/cfgutil/normalization.go b/pkg/util/cfgutil/normalization.go new file mode 100644 index 0000000..2c4ddc2 --- /dev/null +++ b/pkg/util/cfgutil/normalization.go @@ -0,0 +1,45 @@ +package cfgutil + +import ( + "net" +) + +// NormalizeAddress returns the normalized form of the address, adding a default port if necessary. An error is returned +// if the address, even without a port, is not valid. +func NormalizeAddress(addr string, defaultPort string) (hostport string, e error) { + // If the first SplitHostPort errors because of a missing port and not for an invalid host, add the port. If the + // second SplitHostPort fails, then a port is not missing and the original error should be returned. + host, port, origErr := net.SplitHostPort(addr) + if origErr == nil { + return net.JoinHostPort(host, port), nil + } + addr = net.JoinHostPort(addr, defaultPort) + _, _, e = net.SplitHostPort(addr) + if e != nil { + E.Ln(e) + return "", origErr + } + return addr, nil +} + +// NormalizeAddresses returns a new slice with all the passed peer addresses normalized with the given default port, and +// all duplicates removed. +func NormalizeAddresses(addrs []string, defaultPort string) ([]string, error) { + var ( + normalized = make([]string, 0, len(addrs)) + seenSet = make(map[string]struct{}) + ) + for _, addr := range addrs { + normalizedAddr, e := NormalizeAddress(addr, defaultPort) + if e != nil { + E.Ln(e) + return nil, e + } + _, seen := seenSet[normalizedAddr] + if !seen { + normalized = append(normalized, normalizedAddr) + seenSet[normalizedAddr] = struct{}{} + } + } + return normalized, nil +} diff --git a/pkg/util/doc.go b/pkg/util/doc.go new file mode 100644 index 0000000..b99a86b --- /dev/null +++ b/pkg/util/doc.go @@ -0,0 +1,35 @@ +/*Package util provides bitcoin-specific convenience functions and types. + +Block Overview + +A Block defines a bitcoin block that provides easier and more efficient manipulation of raw wire protocol blocks. It +also memoizes hashes for the block and its transactions on their first access so subsequent accesses don't have to +repeat the relatively expensive hashing operations. + +Tx Overview + +A Tx defines a bitcoin transaction that provides more efficient manipulation of raw wire protocol transactions. It +memoizes the hash for the transaction on its first access so subsequent accesses don't have to repeat the relatively +expensive hashing operations. + +Address Overview + +The Address interface provides an abstraction for a Bitcoin address. While the most common type is a pay-to-pubkey-hash, +Bitcoin already supports others and may well support more in the future. This package currently provides implementations +for the pay-to-pubkey, pay-to-pubkey-hash, and pay-to-script-hash address types. To decode/encode an address: + + + // NOTE: The default network is only used for address types which do not already contain that information. At this + // time, that is only pay-to-pubkey addresses. + addrString := "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962" + + "e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d57" + + "8a4c702b6bf11d5f" + defaultNet := &chaincfg.MainNetParams + addr, e := util.DecodeAddress(addrString, defaultNet) + if e != nil { + fmt.Println(e) + return + } + fmt.Println(addr.EncodeAddress()) +*/ +package util diff --git a/pkg/util/example_test.go b/pkg/util/example_test.go new file mode 100644 index 0000000..4f22dcc --- /dev/null +++ b/pkg/util/example_test.go @@ -0,0 +1,64 @@ +package util_test + +import ( + "fmt" + "github.com/p9c/p9/pkg/amt" + "math" +) + +func ExampleAmount() { + a := amt.Amount(0) + fmt.Println("Zero Satoshi:", a) + a = amt.Amount(1e8) + fmt.Println("100,000,000 Satoshis:", a) + a = amt.Amount(1e5) + fmt.Println("100,000 Satoshis:", a) + // Output: + // Zero Satoshi: 0 DUO + // 100,000,000 Satoshis: 1 DUO + // 100,000 Satoshis: 0.001 DUO +} +func ExampleNewAmount() { + amountOne, e := amt.NewAmount(1) + if e != nil { + fmt.Println(e) + return + } + fmt.Println(amountOne) // Output 1 + amountFraction, e := amt.NewAmount(0.01234567) + if e != nil { + fmt.Println(e) + return + } + fmt.Println(amountFraction) // Output 2 + amountZero, e := amt.NewAmount(0) + if e != nil { + fmt.Println(e) + return + } + fmt.Println(amountZero) // Output 3 + amountNaN, e := amt.NewAmount(math.NaN()) + if e != nil { + fmt.Println(e) + return + } + fmt.Println(amountNaN) // Output 4 + // Output: 1 DUO + // 0.01234567 DUO + // 0 DUO + // invalid bitcoin amount +} +func ExampleAmount_unitConversions() { + amount := amt.Amount(44433322211100) + fmt.Println("Satoshi to kDUO:", amount.Format(amt.KiloDUO)) + fmt.Println("Satoshi to DUO:", amount) + fmt.Println("Satoshi to MilliDUO:", amount.Format(amt.MilliDUO)) + fmt.Println("Satoshi to MicroDUO:", amount.Format(amt.MicroDUO)) + fmt.Println("Satoshi to Satoshi:", amount.Format(amt.Satoshi)) + // Output: + // Satoshi to kDUO: 444.333222111 kDUO + // Satoshi to DUO: 444333.222111 DUO + // Satoshi to MilliDUO: 444333222.111 mDUO + // Satoshi to MicroDUO: 444333222111 μDUO + // Satoshi to Satoshi: 44433322211100 Satoshi +} diff --git a/pkg/util/gobin/gobin.go b/pkg/util/gobin/gobin.go new file mode 100644 index 0000000..23492c8 --- /dev/null +++ b/pkg/util/gobin/gobin.go @@ -0,0 +1,28 @@ +package gobin + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +func Get() (gobinary string, e error) { + // search the environment variables for a GOROOT, if it exists we know we can run Go + env := os.Environ() + envMap := make(map[string]string) + for i := range env { + split := strings.Split(env[i], "=") + if len(split) < 2 { + continue + } + envMap[split[0]] = split[1] + } + goroot, ok := envMap["GOROOT"] + if ok { + gobinary = filepath.Join(goroot, "bin", "go") + } else { + e = errors.New("no GOROOT found, no Go binary available") + } + return +} diff --git a/pkg/util/gobin/log.go b/pkg/util/gobin/log.go new file mode 100644 index 0000000..37268f3 --- /dev/null +++ b/pkg/util/gobin/log.go @@ -0,0 +1,43 @@ +package gobin + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/util/hdkeychain/README.md b/pkg/util/hdkeychain/README.md new file mode 100755 index 0000000..5016bac --- /dev/null +++ b/pkg/util/hdkeychain/README.md @@ -0,0 +1,59 @@ +# hdkeychain + +[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/p9c/p9/util/hdkeychain) + +Package hdkeychain provides an API for bitcoin hierarchical deterministic +extended keys (BIP0032). + +A comprehensive suite of tests is provided to ensure proper functionality. +See `test_coverage.txt` for the gocov coverage report. Alternatively, if you are +running a POSIX OS, you can run the `cov_report.sh` script for a real-time +report. + +## Feature Overview + +- Full BIP0032 implementation +- Single type for private and public extended keys +- Convenient cryptograpically secure seed generation +- Simple creation of master nodes +- Support for multi-layer derivation +- Easy serialization and deserialization for both private and public extended + keys +- Support for custom networks by registering them with chaincfg +- Obtaining the underlying EC pubkeys, EC privkeys, and associated bitcoin + addresses ties in seamlessly with existing btcec and util types which provide + powerful tools for working with them to do things like sign transations and + generate payment scripts +- Uses the ec package which is highly optimized for secp256k1 +- Code examples including: + - Generating a cryptographically secure random seed and deriving a master + node from it + - Default HD wallet layout as described by BIP0032 + - Audits use case as described by BIP0032 +- Comprehensive test coverage including the BIP0032 test vectors +- Benchmarks + +## Installation and Updating + +```bash +$ go get -u github.com/p9c/p9/util/hdkeychain +``` + +## Examples + +- [NewMaster Example](http://godoc.org/github.com/p9c/p9/util/hdkeychain#example-NewMaster) + Demonstrates how to generate a cryptographically random seed then use it to + create a new master node (extended key). + +- [Default Wallet Layout Example](http://godoc.org/github.com/p9c/p9/util/hdkeychain#example-package--DefaultWalletLayout) + Demonstrates the default hierarchical deterministic wallet layout as described + in BIP0032. + +- [Audits Use Case Example](http://godoc.org/github.com/p9c/p9/util/hdkeychain#example-package--Audits) + Demonstrates the audits use case in BIP0032. + +## License + +Package hdkeychain is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/pkg/util/hdkeychain/bench_test.go b/pkg/util/hdkeychain/bench_test.go new file mode 100644 index 0000000..e469c0f --- /dev/null +++ b/pkg/util/hdkeychain/bench_test.go @@ -0,0 +1,72 @@ +package hdkeychain_test + +import ( + "testing" + + "github.com/p9c/p9/pkg/util/hdkeychain" +) + +// bip0032MasterPriv1 is the master private extended key from the first set of test vectors in BIP0032. +const bip0032MasterPriv1 = "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbP" + + "y6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + +// BenchmarkDeriveHardened benchmarks how long it takes to derive a hardened child from a master private extended key. +func BenchmarkDeriveHardened(b *testing.B) { + b.StopTimer() + masterKey, e := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if e != nil { + b.Errorf("Failed to decode master seed: %v", e) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + _, _ = masterKey.Child(hdkeychain.HardenedKeyStart) + } +} + +// BenchmarkDeriveNormal benchmarks how long it takes to derive a normal (non-hardened) child from a master private +// extended key. +func BenchmarkDeriveNormal(b *testing.B) { + b.StopTimer() + masterKey, e := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if e != nil { + b.Errorf("Failed to decode master seed: %v", e) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + _, _ = masterKey.Child(0) + } +} + +// BenchmarkPrivToPub benchmarks how long it takes to convert a private extended key to a public extended key. +func BenchmarkPrivToPub(b *testing.B) { + b.StopTimer() + masterKey, e := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if e != nil { + b.Errorf("Failed to decode master seed: %v", e) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + _, _ = masterKey.Neuter() + } +} + +// BenchmarkDeserialize benchmarks how long it takes to deserialize a private extended key. +func BenchmarkDeserialize(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = hdkeychain.NewKeyFromString(bip0032MasterPriv1) + + } +} + +// BenchmarkSerialize benchmarks how long it takes to serialize a private extended key. +func BenchmarkSerialize(b *testing.B) { + b.StopTimer() + masterKey, e := hdkeychain.NewKeyFromString(bip0032MasterPriv1) + if e != nil { + b.Errorf("Failed to decode master seed: %v", e) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = masterKey.String() + } +} diff --git a/pkg/util/hdkeychain/doc.go b/pkg/util/hdkeychain/doc.go new file mode 100644 index 0000000..f0b1ad8 --- /dev/null +++ b/pkg/util/hdkeychain/doc.go @@ -0,0 +1,62 @@ +/*Package hdkeychain provides an API for bitcoin hierarchical deterministic extended keys (BIP0032). + +Overview + +The ability to implement hierarchical deterministic wallets depends on the ability to create and derive hierarchical +deterministic extended keys. At a high level, this package provides support for those hierarchical deterministic +extended keys by providing an ExtendedKey type and supporting functions. Each extended key can either be a private or +public extended key which itself is capable of deriving a child extended key. + +Determining the Extended Key Type + +Whether an extended key is a private or public extended key can be determined with the IsPrivate function. Transaction +Signing Keys and Payment Addresses In order to create and sign transactions, or provide others with addresses to send +funds to, the underlying key and address material must be accessible. This package provides the ECPubKey, ECPrivKey, and +Address functions for this purpose. + +The Master Node + +As previously mentioned, the extended keys are hierarchical meaning they are used to form a tree. The root of that tree +is called the master node and this package provides the NewMaster function to create it from a cryptographically random +seed. The GenerateSeed function is provided as a convenient way to create a random seed for use with the NewMaster +function. + +Deriving Children + +Once you have created a tree root (or have deserialized an extended key as discussed later), the child extended keys can +be derived by using the Child function. The Child function supports deriving both normal (non-hardened) and hardened +child extended keys. In order to derive a hardened extended key, use the HardenedKeyStart constant + the hardened key +number as the index to the Child function. This provides the ability to cascade the keys into a tree and hence generate +the hierarchical deterministic key chains. + +Normal vs Hardened Child Extended Keys + +A private extended key can be used to derive both hardened and non-hardened (normal) child private and public extended +keys. A public extended key can only be used to derive non-hardened child public extended keys. As enumerated in BIP0032 +"knowledge of the extended public key plus any non-hardened private key descending from it is equivalent to knowing the +extended private key (and thus every private and public key descending from it). This means that extended public keys +must be treated more carefully than regular public keys. It is also the reason for the existence of hardened keys, and +why they are used for the account level in the tree. This way, a leak of an account-specific (or below) private key +never risks compromising the master or other accounts." + +Neutering a Private Extended Key + +A private extended key can be converted to a new instance of the corresponding public extended key with the Neuter +function. The original extended key is not modified. A public extended key is still capable of deriving non-hardened +child public extended keys. + +Serializing and Deserializing Extended Keys + +Extended keys are serialized and deserialized with the String and NewKeyFromString functions. The serialized key is a +Base58-encoded string which looks like the following: + + public key: xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw + private key: xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7 + +Network + +Extended keys are much like normal Bitcoin addresses in that they have version bytes which tie them to a specific +network. The SetNet and IsForNet functions are provided to set and determinine which network an extended key is +associated with. +*/ +package hdkeychain diff --git a/pkg/util/hdkeychain/example_test.go b/pkg/util/hdkeychain/example_test.go new file mode 100644 index 0000000..473aebf --- /dev/null +++ b/pkg/util/hdkeychain/example_test.go @@ -0,0 +1,126 @@ +package hdkeychain_test + +import ( + "fmt" + + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/util/hdkeychain" +) + +// This example demonstrates how to generate a cryptographically random seed then use it to create a new master node +// (extended key). +func ExampleNewMaster() { + // Generate a random seed at the recommended length. + seed, e := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if e != nil { + return + } + // Generate a new master node using the seed. + key, e := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if e != nil { + return + } + // Show that the generated master node extended key is private. + fmt.Println("Private Extended Key?:", key.IsPrivate()) + // Output: + // Private Extended Key?: true +} + +// This example demonstrates the default hierarchical deterministic wallet layout as described in BIP0032. +func Example_defaultWalletLayout() { + // The default wallet layout described in BIP0032 is: + // + // Each account is composed of two keypair chains: an internal and an external one. The external keychain is used to + // generate new public addresses, while the internal keychain is used for all other operations (change addresses, + // generation addresses, ..., anything that doesn't need to be communicated). + // + // * m/iH/0/k + // + // corresponds to the k'th keypair of the external chain of account number i of the HDW derived from master m. + // + // * m/iH/1/k + // + // corresponds to the k'th keypair of the internal chain of account number i of the HDW derived from master m. + // + // Ordinarily this would either be read from some encrypted source and be decrypted or generated as the NewMaster + // example shows, but for the purposes of this example, the private extended key for the master node is being hard + // coded here. + master := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + // Start by getting an extended key instance for the master node. This gives the path: m + masterKey, e := hdkeychain.NewKeyFromString(master) + if e != nil { + return + } + // Derive the extended key for account 0. This gives the path: m/0H + acct0, e := masterKey.Child(hdkeychain.HardenedKeyStart + 0) + if e != nil { + return + } + // Derive the extended key for the account 0 external chain. This gives the path: m/0H/0 + acct0Ext, e := acct0.Child(0) + if e != nil { + return + } + // Derive the extended key for the account 0 internal chain. This gives the path: m/0H/1 + acct0Int, e := acct0.Child(1) + if e != nil { + return + } + // At this point, acct0Ext and acct0Int are ready to derive the keys for the external and internal wallet chains. + // Derive the 10th extended key for the account 0 external chain. This gives the path: m/0H/0/10 + acct0Ext10, e := acct0Ext.Child(10) + if e != nil { + return + } + // Derive the 1st extended key for the account 0 internal chain. This gives the path: m/0H/1/0 + acct0Int0, e := acct0Int.Child(0) + if e != nil { + return + } + // Get and show the address associated with the extended keys for the main bitcoin network. + acct0ExtAddr, e := acct0Ext10.Address(&chaincfg.MainNetParams) + if e != nil { + return + } + acct0IntAddr, e := acct0Int0.Address(&chaincfg.MainNetParams) + if e != nil { + return + } + fmt.Println("Account 0 External Address 10:", acct0ExtAddr) + fmt.Println("Account 0 Internal Address 0:", acct0IntAddr) + // Output: + // Account 0 External Address 10: aV29NZpQZkh7ByDPhP4NR7nzx56crLAvTF + // Account 0 Internal Address 0: aUWmaTQVFwTV6wwQYvyyQRAvWeQmAorqAV +} + +// This example demonstrates the audits use case in BIP0032. +func Example_audits() { + // The audits use case described in BIP0032 is:// + // + // In case an auditor needs full access to the list of incoming and outgoing payments, one can share all account + // public extended keys. This will allow the auditor to see all transactions from and to the wallet, in all + // accounts, but not a single secret key. + // + // * N(m/*) + // + // corresponds to the neutered master extended key (also called the master public extended key) Ordinarily this + // would either be read from some encrypted source and be decrypted or generated as the NewMaster example shows, but + // for the purposes of this example, the private extended key for the master node is being hard coded here. + master := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + // Start by getting an extended key instance for the master node. This gives the path: + // + // m + masterKey, e := hdkeychain.NewKeyFromString(master) + if e != nil { + return + } + // Neuter the master key to generate a master public extended key. This gives the path: N(m/*) + masterPubKey, e := masterKey.Neuter() + if e != nil { + return + } + // Share the master public extended key with the auditor. + fmt.Println("Audit key N(m/*):", masterPubKey) + // Output: + // Audit key N(m/*): xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8 +} diff --git a/pkg/util/hdkeychain/extendedkey.go b/pkg/util/hdkeychain/extendedkey.go new file mode 100644 index 0000000..65b0ba9 --- /dev/null +++ b/pkg/util/hdkeychain/extendedkey.go @@ -0,0 +1,519 @@ +package hdkeychain + +// References: +// [BIP32]: BIP0032 - Hierarchical Deterministic Wallets https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "github.com/p9c/p9/pkg/btcaddr" + "math/big" + + "github.com/p9c/p9/pkg/base58" + "github.com/p9c/p9/pkg/chaincfg" + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" +) + +const ( + // RecommendedSeedLen is the recommended length in bytes for a seed to a master node. + RecommendedSeedLen = 32 // 256 bits + // HardenedKeyStart is the index at which a hardened key starts. Each extended key has 2^31 normal child keys and + // 2^31 hardned child keys. Thus the range for normal child keys is [0, 2^31 - 1] and the range for hardened child + // keys is [2^31, 2^32 - 1]. + HardenedKeyStart = 0x80000000 // 2^31 + // MinSeedBytes is the minimum number of bytes allowed for a seed to a master node. + MinSeedBytes = 16 // 128 bits + // MaxSeedBytes is the maximum number of bytes allowed for a seed to a master node. + MaxSeedBytes = 64 // 512 bits + // serializedKeyLen is the length of a serialized public or private extended key. It consists of 4 bytes version, 1 + // byte depth, 4 bytes fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes public/private key data. + serializedKeyLen = 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes + // maxUint8 is the max positive integer which can be serialized in a uint8 + maxUint8 = 1<<8 - 1 +) + +var ( + // ErrDeriveHardFromPublic describes an error in which the caller attempted to derive a hardened extended key from a + // public key. + ErrDeriveHardFromPublic = errors.New( + "cannot derive a hardened key " + + "from a public key", + ) + // ErrDeriveBeyondMaxDepth describes an error in which the caller has attempted to derive more than 255 keys from a + // root key. + ErrDeriveBeyondMaxDepth = errors.New( + "cannot derive a key with more than " + + "255 indices in its path", + ) + // ErrNotPrivExtKey describes an error in which the caller attempted to extract a private key from a public extended + // key. + ErrNotPrivExtKey = errors.New( + "unable to create private keys from a " + + "public extended key", + ) + // ErrInvalidChild describes an error in which the child at a specific index is invalid due to the derived key + // falling outside of the valid range for secp256k1 private keys. This error indicates the caller should simply + // ignore the invalid child extended key at this index and increment to the next index. + ErrInvalidChild = errors.New("the extended key at this index is invalid") + // ErrUnusableSeed describes an error in which the provided seed is not usable due to the derived key falling + // outside of the valid range for secp256k1 private keys. This error indicates the caller must choose another seed. + ErrUnusableSeed = errors.New("unusable seed") + // ErrInvalidSeedLen describes an error in which the provided seed or seed length is not in the allowed range. + ErrInvalidSeedLen = fmt.Errorf( + "seed length must be between %d and %d "+ + "bits", MinSeedBytes*8, MaxSeedBytes*8, + ) + // ErrBadChecksum describes an error in which the checksum encoded with a serialized extended key does not match the + // calculated value. + ErrBadChecksum = errors.New("bad extended key checksum") + // ErrInvalidKeyLen describes an error in which the provided serialized key is not the expected length. + ErrInvalidKeyLen = errors.New( + "the provided serialized extended key " + + "length is invalid", + ) + // masterKey is the master key used along with a random seed used to generate the master node in the hierarchical + // tree. + masterKey = []byte("Parallelcoin seed") +) + +// ExtendedKey houses all the information needed to support a hierarchical deterministic extended key. See the package +// overview documentation for more details on how to use extended keys. +type ExtendedKey struct { + key []byte // This will be the pubkey for extended pub keys + pubKey []byte // This will only be set for extended priv keys + chainCode []byte + depth uint8 + parentFP []byte + childNum uint32 + version []byte + isPrivate bool +} + +// NewExtendedKey returns a new instance of an extended key with the given fields. No error checking is performed here +// as it's only intended to be a convenience method used to create a populated struct. +// +// This function should only by used by applications that need to create custom ExtendedKeys. All other applications +// should just use NewMaster, Child, or Neuter. +func NewExtendedKey( + version, key, chainCode, parentFP []byte, depth uint8, + childNum uint32, isPrivate bool, +) *ExtendedKey { + // NOTE: The pubKey field is intentionally left nil so it is only computed and memoized as required. + return &ExtendedKey{ + key: key, + chainCode: chainCode, + depth: depth, + parentFP: parentFP, + childNum: childNum, + version: version, + isPrivate: isPrivate, + } +} + +// pubKeyBytes returns bytes for the serialized compressed public key associated with this extended key in an efficient +// manner including memoization as necessary. +// +// When the extended key is already a public key, the key is simply returned as is since it's already in the correct +// form. However, when the extended key is a private key, the public key will be calculated and memoized so future +// accesses can simply return the cached result. +func (k *ExtendedKey) pubKeyBytes() []byte { + // Just return the key if it's already an extended public key. + if !k.isPrivate { + return k.key + } + // This is a private extended key, so calculate and memoize the public key if needed. + if len(k.pubKey) == 0 { + pkx, pky := ec.S256().ScalarBaseMult(k.key) + pubKey := ec.PublicKey{Curve: ec.S256(), X: pkx, Y: pky} + k.pubKey = pubKey.SerializeCompressed() + } + return k.pubKey +} + +// IsPrivate returns whether or not the extended key is a private extended key. A private extended key can be used to +// derive both hardened and non-hardened child private and public extended keys. A public extended key can only be used +// to derive non-hardened child public extended keys. +func (k *ExtendedKey) IsPrivate() bool { + return k.isPrivate +} + +// Depth returns the current derivation level with respect to the root. The root key has depth zero, and the field has a +// maximum of 255 due to how depth is serialized. +func (k *ExtendedKey) Depth() uint8 { + return k.depth +} + +// ParentFingerprint returns a fingerprint of the parent extended key from which this one was derived. +func (k *ExtendedKey) ParentFingerprint() uint32 { + return binary.BigEndian.Uint32(k.parentFP) +} + +// Child returns a derived child extended key at the given index. When this extended key is a private extended key (as +// determined by the IsPrivate function), a private extended key will be derived. Otherwise, the derived extended key +// will be also be a public extended key. +// +// When the index is greater to or equal than the HardenedKeyStart constant, the derived extended key will be a hardened +// extended key. It is only possible to derive a hardended extended key from a private extended key. Consequently, this +// function will return ErrDeriveHardFromPublic if a hardened child extended key is requested from a public extended +// key. +// +// A hardened extended key is useful since, as previously mentioned, it requires a parent private extended key to +// derive. In other words, normal child extended public keys can be derived from a parent public extended key (no +// knowledge of the parent private key) whereas hardened extended keys may not be. NOTE: There is an extremely small +// chance (< 1 in 2^127) the specific child index does not derive to a usable child. The ErrInvalidChild error will be +// returned if this should occur, and the caller is expected to ignore the invalid child and simply increment to the +// next index. +func (k *ExtendedKey) Child(i uint32) (*ExtendedKey, error) { + // Prevent derivation of children beyond the max allowed depth. + if k.depth == maxUint8 { + return nil, ErrDeriveBeyondMaxDepth + } + // There are four scenarios that could happen here: + // + // 1) Private extended key -> Hardened child private extended key + // + // 2) Private extended key -> Non-hardened child private extended key + // + // 3) Public extended key -> Non-hardened child public extended key + // + // 4) Public extended key -> Hardened child public extended key (INVALID!) + // + // Case #4 is invalid, so error out early. + // + // A hardened child extended key may not be created from a public extended key. + isChildHardened := i >= HardenedKeyStart + if !k.isPrivate && isChildHardened { + return nil, ErrDeriveHardFromPublic + } + // The data used to derive the child key depends on whether or not the child is hardened per [BIP32]. + // + // For hardened children: + // + // 0x00 || ser256(parentKey) || ser32(i) + // + // For normal children: + // + // serP(parentPubKey) || ser32(i) + keyLen := 33 + data := make([]byte, keyLen+4) + if isChildHardened { + // Case #1. + // + // When the child is a hardened child, the key is known to be a private key due to the above early return. Pad it with a leading zero as required by [BIP32] for deriving the child.(data[1:], k.key) + } else { + // Case #2 or #3. + // + // This is either a public or private extended key, but in either case, the data which is used to derive the child key starts with the secp256k1 compressed public key bytes. + copy(data, k.pubKeyBytes()) + } + binary.BigEndian.PutUint32(data[keyLen:], i) + // Take the HMAC-SHA512 of the current key's chain code and the derived data: + // + // I = HMAC-SHA512(Key = chainCode, Data = data) + hmac512 := hmac.New(sha512.New, k.chainCode) + _, e := hmac512.Write(data) + if e != nil { + E.Ln(e) + } + ilr := hmac512.Sum(nil) + // Split "I" into two 32-byte sequences Il and Ir where: + // + // Il = intermediate key used to derive the child + // + // Ir = child chain code + il := ilr[:len(ilr)/2] + childChainCode := ilr[len(ilr)/2:] + // Both derived public or private keys rely on treating the left 32-byte sequence calculated above (Il) as a 256-bit + // integer that must be within the valid range for a secp256k1 private key. There is a small chance (< 1 in 2^127) + // this condition will not hold, and in that case, a child extended key can't be created for this index and the + // caller should simply increment to the next index. + ilNum := new(big.Int).SetBytes(il) + if ilNum.Cmp(ec.S256().N) >= 0 || ilNum.Sign() == 0 { + return nil, ErrInvalidChild + } + // The algorithm used to derive the child key depends on whether or not a private or public child is being derived. + // + // For private children: + // + // childKey = parse256(Il) + parentKey + // + // For public children: + // + // childKey = serP(point(parse256(Il)) + parentKey) + var isPrivate bool + var childKey []byte + if k.isPrivate { + // Case #1 or #2. + // + // Add the parent private key to the intermediate private key to derive the final child key. + // + // childKey = parse256(Il) + parenKey + keyNum := new(big.Int).SetBytes(k.key) + ilNum.Add(ilNum, keyNum) + ilNum.Mod(ilNum, ec.S256().N) + childKey = ilNum.Bytes() + isPrivate = true + } else { + // Case #3. + // + // Calculate the corresponding intermediate public key for intermediate private key. + ilx, ily := ec.S256().ScalarBaseMult(il) + if ilx.Sign() == 0 || ily.Sign() == 0 { + return nil, ErrInvalidChild + } + // Convert the serialized compressed parent public key into X and Y coordinates so it can be added to the + // intermediate public key. + pubKey, e := ec.ParsePubKey(k.key, ec.S256()) + if e != nil { + E.Ln(e) + return nil, e + } + // Add the intermediate public key to the parent public key to derive the final child key. childKey = + // serP(point(parse256(Il)) + parentKey) + childX, childY := ec.S256().Add(ilx, ily, pubKey.X, pubKey.Y) + pk := ec.PublicKey{Curve: ec.S256(), X: childX, Y: childY} + childKey = pk.SerializeCompressed() + } + // The fingerprint of the parent for the derived child is the first 4 bytes of the RIPEMD160(SHA256(parentPubKey)). + parentFP := btcaddr.Hash160(k.pubKeyBytes())[:4] + return NewExtendedKey( + k.version, childKey, childChainCode, parentFP, + k.depth+1, i, isPrivate, + ), nil +} + +// Neuter returns a new extended public key from this extended private key. The same extended key will be returned +// unaltered if it is already an extended public key. +// +// As the name implies, an extended public key does not have access to the private key, so it is not capable of signing +// transactions or deriving child extended private keys. However, it is capable of deriving further child extended +// public keys. +func (k *ExtendedKey) Neuter() (*ExtendedKey, error) { + // Already an extended public key. + if !k.isPrivate { + return k, nil + } + // Get the associated public extended key version bytes. + version, e := chaincfg.HDPrivateKeyToPublicKeyID(k.version) + if e != nil { + E.Ln(e) + return nil, e + } + // Convert it to an extended public key. The key for the new extended key will simply be the pubkey of the current + // extended private key. + // + // This is the function N((k,c)) -> (K, c) from [BIP32]. + return NewExtendedKey( + version, k.pubKeyBytes(), k.chainCode, k.parentFP, + k.depth, k.childNum, false, + ), nil +} + +// ECPubKey converts the extended key to a ec public key and returns it. +func (k *ExtendedKey) ECPubKey() (*ec.PublicKey, error) { + return ec.ParsePubKey(k.pubKeyBytes(), ec.S256()) +} + +// ECPrivKey converts the extended key to a ec private key and returns it. As you might imagine this is only possible if +// the extended key is a private extended key (as determined by the IsPrivate function). The ErrNotPrivExtKey error will +// be returned if this function is called on a public extended key. +func (k *ExtendedKey) ECPrivKey() (*ec.PrivateKey, error) { + if !k.isPrivate { + return nil, ErrNotPrivExtKey + } + privKey, _ := ec.PrivKeyFromBytes(ec.S256(), k.key) + return privKey, nil +} + +// Address converts the extended key to a standard bitcoin pay-to-pubkey-hash address for the passed network. +func (k *ExtendedKey) Address(net *chaincfg.Params) (*btcaddr.PubKeyHash, error) { + pkHash := btcaddr.Hash160(k.pubKeyBytes()) + return btcaddr.NewPubKeyHash(pkHash, net) +} + +// paddedAppend appends the src byte slice to dst, returning the new slice. If the length of the source is smaller than +// the passed size, leading zero bytes are appended to the dst slice before appending src. +func paddedAppend(size uint, dst, src []byte) []byte { + for i := 0; i < int(size)-len(src); i++ { + dst = append(dst, 0) + } + return append(dst, src...) +} + +// String returns the extended key as a human-readable base58-encoded string. +func (k *ExtendedKey) String() string { + if len(k.key) == 0 { + return "zeroed extended key" + } + var childNumBytes [4]byte + binary.BigEndian.PutUint32(childNumBytes[:], k.childNum) + // The serialized format is: + // + // version (4) || depth (1) || parent fingerprint (4)) || child num (4) || chain code (32) || key data (33) || + // checksum (4) + serializedBytes := make([]byte, 0, serializedKeyLen+4) + serializedBytes = append(serializedBytes, k.version...) + serializedBytes = append(serializedBytes, k.depth) + serializedBytes = append(serializedBytes, k.parentFP...) + serializedBytes = append(serializedBytes, childNumBytes[:]...) + serializedBytes = append(serializedBytes, k.chainCode...) + if k.isPrivate { + serializedBytes = append(serializedBytes, 0x00) + serializedBytes = paddedAppend(32, serializedBytes, k.key) + } else { + serializedBytes = append(serializedBytes, k.pubKeyBytes()...) + } + checkSum := chainhash.DoubleHashB(serializedBytes)[:4] + serializedBytes = append(serializedBytes, checkSum...) + return base58.Encode(serializedBytes) +} + +// IsForNet returns whether or not the extended key is associated with the passed bitcoin network. +func (k *ExtendedKey) IsForNet(net *chaincfg.Params) bool { + return bytes.Equal(k.version, net.HDPrivateKeyID[:]) || + bytes.Equal(k.version, net.HDPublicKeyID[:]) +} + +// SetNet associates the extended key, and any child keys yet to be derived from it, with the passed network. +func (k *ExtendedKey) SetNet(net *chaincfg.Params) { + if k.isPrivate { + k.version = net.HDPrivateKeyID[:] + } else { + k.version = net.HDPublicKeyID[:] + } +} + +// zero sets all bytes in the passed slice to zero. This is used to explicitly clear private key material from memory. +func zero(b []byte) { + lenb := len(b) + for i := 0; i < lenb; i++ { + b[i] = 0 + } +} + +// Zero manually clears all fields and bytes in the extended key. This can be used to explicitly clear key material from +// memory for enhanced security against memory scraping. This function only clears this particular key and not any +// children that have already been derived. +func (k *ExtendedKey) Zero() { + zero(k.key) + zero(k.pubKey) + zero(k.chainCode) + zero(k.parentFP) + k.version = nil + k.key = nil + k.depth = 0 + k.childNum = 0 + k.isPrivate = false +} + +// NewMaster creates a new master node for use in creating a hierarchical deterministic key chain. The seed must be +// between 128 and 512 bits and should be generated by a cryptographically secure random generation source. +// +// NOTE: There is an extremely small chance (< 1 in 2^127) the provided seed will derive to an unusable secret key. The +// ErrUnusable error will be returned if this should occur, so the caller must check for it and generate a new seed +// accordingly. +func NewMaster(seed []byte, net *chaincfg.Params) (*ExtendedKey, error) { + // Per [BIP32], the seed must be in range [MinSeedBytes, MaxSeedBytes]. + if len(seed) < MinSeedBytes || len(seed) > MaxSeedBytes { + return nil, ErrInvalidSeedLen + } + // First take the HMAC-SHA512 of the master key and the seed data: + // + // I = HMAC-SHA512(Key = "Parallelcoin seed", Data = S) + hmac512 := hmac.New(sha512.New, masterKey) + _, e := hmac512.Write(seed) + if e != nil { + E.Ln(e) + } + lr := hmac512.Sum(nil) + // Split "I" into two 32-byte sequences Il and Ir where: + // + // Il = master secret key + // + // Ir = master chain code + secretKey := lr[:len(lr)/2] + chainCode := lr[len(lr)/2:] + // Ensure the key in usable. + secretKeyNum := new(big.Int).SetBytes(secretKey) + if secretKeyNum.Cmp(ec.S256().N) >= 0 || secretKeyNum.Sign() == 0 { + return nil, ErrUnusableSeed + } + parentFP := []byte{0x00, 0x00, 0x00, 0x00} + return NewExtendedKey( + net.HDPrivateKeyID[:], secretKey, chainCode, + parentFP, 0, 0, true, + ), nil +} + +// NewKeyFromString returns a new extended key instance from a base58-encoded extended key. +func NewKeyFromString(key string) (*ExtendedKey, error) { + // The base58-decoded extended key must consist of a serialized payload plus an additional 4 bytes for the checksum. + decoded := base58.Decode(key) + if len(decoded) != serializedKeyLen+4 { + return nil, ErrInvalidKeyLen + } + // The serialized format is: + // + // version (4) || depth (1) || parent fingerprint (4)) || child num (4) || chain code (32) || key data (33) || + // checksum (4) + // + // Split the payload and checksum up and ensure the checksum matches. + payload := decoded[:len(decoded)-4] + checkSum := decoded[len(decoded)-4:] + expectedCheckSum := chainhash.DoubleHashB(payload)[:4] + if !bytes.Equal(checkSum, expectedCheckSum) { + return nil, ErrBadChecksum + } + // Deserialize each of the payload fields. + version := payload[:4] + depth := payload[4:5][0] + parentFP := payload[5:9] + childNum := binary.BigEndian.Uint32(payload[9:13]) + chainCode := payload[13:45] + keyData := payload[45:78] + // The key data is a private key if it starts with 0x00. Serialized compressed pubkeys either start with 0x02 or + // 0x03. + isPrivate := keyData[0] == 0x00 + if isPrivate { + // Ensure the private key is valid. It must be within the range of the order of the secp256k1 curve and not be + // 0. + keyData = keyData[1:] + keyNum := new(big.Int).SetBytes(keyData) + if keyNum.Cmp(ec.S256().N) >= 0 || keyNum.Sign() == 0 { + return nil, ErrUnusableSeed + } + } else { + // Ensure the public key parses correctly and is actually on the secp256k1 curve. + _, e := ec.ParsePubKey(keyData, ec.S256()) + if e != nil { + E.Ln(e) + return nil, e + } + } + return NewExtendedKey( + version, keyData, chainCode, parentFP, depth, + childNum, isPrivate, + ), nil +} + +// GenerateSeed returns a cryptographically secure random seed that can be used as the input for the NewMaster function +// to generate a new master node. The length is in bytes and it must be between 16 and 64 (128 to 512 bits). The +// recommended length is 32 (256 bits) as defined by the RecommendedSeedLen constant. +func GenerateSeed(length uint8) ([]byte, error) { + // Per [BIP32], the seed must be in range [MinSeedBytes, MaxSeedBytes]. + if length < MinSeedBytes || length > MaxSeedBytes { + return nil, ErrInvalidSeedLen + } + buf := make([]byte, length) + _, e := rand.Read(buf) + if e != nil { + E.Ln(e) + return nil, e + } + return buf, nil +} diff --git a/pkg/util/hdkeychain/extendedkey_test.go b/pkg/util/hdkeychain/extendedkey_test.go new file mode 100644 index 0000000..75e11af --- /dev/null +++ b/pkg/util/hdkeychain/extendedkey_test.go @@ -0,0 +1,1032 @@ +package hdkeychain + +// References: +// [BIP32]: BIP0032 - Hierarchical Deterministic Wallets https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +import ( + "bytes" + "errors" + "math" + "reflect" + "testing" + + "github.com/p9c/p9/pkg/chaincfg" +) + +// // TestBIP0032Vectors tests the vectors provided by [BIP32] to ensure the derivation works as intended. +// func TestBIP0032Vectors(// t *testing.T) { +// // The master seeds for each of the two test vectors in [BIP32]. +// testVec1MasterHex := "000102030405060708090a0b0c0d0e0f" +// testVec2MasterHex := "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542" +// testVec3MasterHex := "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be" +// hkStart := uint32(0x80000000) +// tests := []struct { +// name string +// master string +// path []uint32 +// wantPub string +// wantPriv string +// net *chaincfg.Params +// }{ +// // Test vector 1 +// { +// name: "test vector 1 chain m", +// master: testVec1MasterHex, +// path: []uint32{}, +// wantPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", +// wantPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 1 chain m/0H", +// master: testVec1MasterHex, +// path: []uint32{hkStart}, +// wantPub: "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", +// wantPriv: "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 1 chain m/0H/1", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1}, +// wantPub: "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", +// wantPriv: "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 1 chain m/0H/1/2H", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1, hkStart + 2}, +// wantPub: "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", +// wantPriv: "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 1 chain m/0H/1/2H/2", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1, hkStart + 2, 2}, +// wantPub: "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", +// wantPriv: "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 1 chain m/0H/1/2H/2/1000000000", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1, hkStart + 2, 2, 1000000000}, +// wantPub: "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", +// wantPriv: "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", +// net: &chaincfg.MainNetParams, +// }, +// // Test vector 2 +// { +// name: "test vector 2 chain m", +// master: testVec2MasterHex, +// path: []uint32{}, +// wantPub: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", +// wantPriv: "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 2 chain m/0", +// master: testVec2MasterHex, +// path: []uint32{0}, +// wantPub: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", +// wantPriv: "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 2 chain m/0/2147483647H", +// master: testVec2MasterHex, +// path: []uint32{0, hkStart + 2147483647}, +// wantPub: "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", +// wantPriv: "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 2 chain m/0/2147483647H/1", +// master: testVec2MasterHex, +// path: []uint32{0, hkStart + 2147483647, 1}, +// wantPub: "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", +// wantPriv: "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 2 chain m/0/2147483647H/1/2147483646H", +// master: testVec2MasterHex, +// path: []uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646}, +// wantPub: "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", +// wantPriv: "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 2 chain m/0/2147483647H/1/2147483646H/2", +// master: testVec2MasterHex, +// path: []uint32{0, hkStart + 2147483647, 1, hkStart + 2147483646, 2}, +// wantPub: "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", +// wantPriv: "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", +// net: &chaincfg.MainNetParams, +// }, +// // Test vector 3 +// { +// name: "test vector 3 chain m", +// master: testVec3MasterHex, +// path: []uint32{}, +// wantPub: "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13", +// wantPriv: "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6", +// net: &chaincfg.MainNetParams, +// }, +// { +// name: "test vector 3 chain m/0H", +// master: testVec3MasterHex, +// path: []uint32{hkStart}, +// wantPub: "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", +// wantPriv: "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L", +// net: &chaincfg.MainNetParams, +// }, +// // Test vector 1 - Testnet +// { +// name: "test vector 1 chain m - testnet", +// master: testVec1MasterHex, +// path: []uint32{}, +// wantPub: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", +// wantPriv: "tprv8ZgxMBicQKsPeDgjzdC36fs6bMjGApWDNLR9erAXMs5skhMv36j9MV5ecvfavji5khqjWaWSFhN3YcCUUdiKH6isR4Pwy3U5y5egddBr16m", +// net: &chaincfg.TestNet3Params, +// }, +// { +// name: "test vector 1 chain m/0H - testnet", +// master: testVec1MasterHex, +// path: []uint32{hkStart}, +// wantPub: "tpubD8eQVK4Kdxg3gHrF62jGP7dKVCoYiEB8dFSpuTawkL5YxTus5j5pf83vaKnii4bc6v2NVEy81P2gYrJczYne3QNNwMTS53p5uzDyHvnw2jm", +// wantPriv: "tprv8bxNLu25VazNnppTCP4fyhyCvBHcYtzE3wr3cwYeL4HA7yf6TLGEUdS4QC1vLT63TkjRssqJe4CvGNEC8DzW5AoPUw56D1Ayg6HY4oy8QZ9", +// net: &chaincfg.TestNet3Params, +// }, +// { +// name: "test vector 1 chain m/0H/1 - testnet", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1}, +// wantPub: "tpubDApXh6cD2fZ7WjtgpHd8yrWyYaneiFuRZa7fVjMkgxsmC1QzoXW8cgx9zQFJ81Jx4deRGfRE7yXA9A3STsxXj4CKEZJHYgpMYikkas9DBTP", +// wantPriv: "tprv8e8VYgZxtHsSdGrtvdxYaSrryZGiYviWzGWtDDKTGh5NMXAEB8gYSCLHpFCywNs5uqV7ghRjimALQJkRFZnUrLHpzi2pGkwqLtbubgWuQ8q", +// net: &chaincfg.TestNet3Params, +// }, +// { +// name: "test vector 1 chain m/0H/1/2H - testnet", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1, hkStart + 2}, +// wantPub: "tpubDDRojdS4jYQXNugn4t2WLrZ7mjfAyoVQu7MLk4eurqFCbrc7cHLZX8W5YRS8ZskGR9k9t3PqVv68bVBjAyW4nWM9pTGRddt3GQftg6MVQsm", +// wantPriv: "tprv8gjmbDPpbAirVSezBEMuwSu1Ci9EpUJWKokZTYccSZSomNMLytWyLdtDNHRbucNaRJWWHANf9AzEdWVAqahfyRjVMKbNRhBmxAM8EJr7R15", +// net: &chaincfg.TestNet3Params, +// }, +// { +// name: "test vector 1 chain m/0H/1/2H/2 - testnet", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1, hkStart + 2, 2}, +// wantPub: "tpubDFfCa4Z1v25WTPAVm9EbEMiRrYwucPocLbEe12BPBGooxxEUg42vihy1DkRWyftztTsL23snYezF9uXjGGwGW6pQjEpcTpmsH6ajpf4CVPn", +// wantPriv: "tprv8iyAReWmmePqZv8hsVZzpx4KHXRyT4chmHdriW95m11R8Tyi3fDLYDM93bq4NGn1V6eCu5cE3zSQ6hPd31F2ApKXkZgTyn1V78pHjkq1V2v", +// net: &chaincfg.TestNet3Params, +// }, +// { +// name: "test vector 1 chain m/0H/1/2H/2/1000000000 - testnet", +// master: testVec1MasterHex, +// path: []uint32{hkStart, 1, hkStart + 2, 2, 1000000000}, +// wantPub: "tpubDHNy3kAG39ThyiwwsgoKY4iRenXDRtce8qdCFJZXPMCJg5dsCUHayp84raLTpvyiNA9sXPob5rgqkKvkN8S7MMyXbnEhGJMW64Cf4vFAoaF", +// wantPriv: "tprv8kgvuL81tmn36Fv9z38j8f4K5m1HGZRjZY2QxnXDy5PuqbP6a5TzoKWCgTcGHBu66W3TgSbAu2yX6sPza5FkHmy564Sh6gmCPUNeUt4yj2x", +// net: &chaincfg.TestNet3Params, +// }, +// } +// tests: +// for i, test := range tests { +// masterSeed, e := hex.DecodeString(test.master) +// if e != nil { +// t.Errorf("DecodeString #%d (%s): unexpected error: %v", +// i, test.name, e) +// continue +// } +// extKey, e := NewMaster(masterSeed, test.net) +// if e != nil { +// t.Errorf("NewMaster #%d (%s): unexpected error when "+ +// "creating new master key: %v", i, test.name, +// e) +// continue +// } +// for _, childNum := range test.path { +// var e error +// extKey, e = extKey.Child(childNum) +// if e != nil { +// t.Errorf("e: %v", e) +// continue tests +// } +// } +// if extKey.Depth() != uint8(len(test.path)) { +// t.Errorf("Depth of key %d should match fixture path: %v", +// extKey.Depth(), len(test.path)) +// continue +// } +// privStr := extKey.String() +// if privStr != test.wantPriv { +// t.Errorf("Serialize #%d (%s): mismatched serialized "+ +// "private extended key -- got: %s, want: %s", i, +// test.name, privStr, test.wantPriv) +// continue +// } +// pubKey, e := extKey.Neuter() +// if e != nil { +// t.Errorf("Neuter #%d (%s): unexpected error: %v ", i, +// test.name, e) +// continue +// } +// // Neutering a second time should have no effect. +// pubKey, e = pubKey.Neuter() +// if e != nil { +// t.Errorf("Neuter #%d (%s): unexpected error: %v", i, +// test.name, e) +// return +// } +// pubStr := pubKey.String() +// if pubStr != test.wantPub { +// t.Errorf("Neuter #%d (%s): mismatched serialized "+ +// "public extended key -- got: %s, want: %s", i, +// test.name, pubStr, test.wantPub) +// continue +// } +// } +// } + +// TestPrivateDerivation tests several vectors which derive private keys from other private keys works as intended. +func TestPrivateDerivation(t *testing.T) { + // The private extended keys for test vectors in [BIP32]. + testVec1MasterPrivKey := "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" + testVec2MasterPrivKey := "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U" + tests := []struct { + name string + master string + path []uint32 + wantPriv string + }{ + // Test vector 1 + { + name: "test vector 1 chain m", + master: testVec1MasterPrivKey, + path: []uint32{}, + wantPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + }, + { + name: "test vector 1 chain m/0", + master: testVec1MasterPrivKey, + path: []uint32{0}, + wantPriv: "xprv9uHRZZhbkedL37eZEnyrNsQPFZYRAvjy5rt6M1nbEkLSo378x1CQQLo2xxBvREwiK6kqf7GRNvsNEchwibzXaV6i5GcsgyjBeRguXhKsi4R", + }, + { + name: "test vector 1 chain m/0/1", + master: testVec1MasterPrivKey, + path: []uint32{0, 1}, + wantPriv: "xprv9ww7sMFLzJMzy7bV1qs7nGBxgKYrgcm3HcJvGb4yvNhT9vxXC7eX7WVULzCfxucFEn2TsVvJw25hH9d4mchywguGQCZvRgsiRaTY1HCqN8G", + }, + { + name: "test vector 1 chain m/0/1/2", + master: testVec1MasterPrivKey, + path: []uint32{0, 1, 2}, + wantPriv: "xprv9xrdP7iD2L1YZCgR9AecDgpDMZSTzP5KCfUykGXgjBxLgp1VFHsEeL3conzGAkbc1MigG1o8YqmfEA2jtkPdf4vwMaGJC2YSDbBTPAjfRUi", + }, + { + name: "test vector 1 chain m/0/1/2/2", + master: testVec1MasterPrivKey, + path: []uint32{0, 1, 2, 2}, + wantPriv: "xprvA2J8Hq4eiP7xCEBP7gzRJGJnd9CHTkEU6eTNMrZ6YR7H5boik8daFtDZxmJDfdMSKHwroCfAfsBKWWidRfBQjpegy6kzXSkQGGoMdWKz5Xh", + }, + { + name: "test vector 1 chain m/0/1/2/2/1000000000", + master: testVec1MasterPrivKey, + path: []uint32{0, 1, 2, 2, 1000000000}, + wantPriv: "xprvA3XhazxncJqJsQcG85Gg61qwPQKiobAnWjuPpjKhExprZjfse6nErRwTMwGe6uGWXPSykZSTiYb2TXAm7Qhwj8KgRd2XaD21Styu6h6AwFz", + }, + // Test vector 2 + { + name: "test vector 2 chain m", + master: testVec2MasterPrivKey, + path: []uint32{}, + wantPriv: "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", + }, + { + name: "test vector 2 chain m/0", + master: testVec2MasterPrivKey, + path: []uint32{0}, + wantPriv: "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", + }, + { + name: "test vector 2 chain m/0/2147483647", + master: testVec2MasterPrivKey, + path: []uint32{0, 2147483647}, + wantPriv: "xprv9wSp6B7cXJWXZRpDbxkFg3ry2fuSyUfvboJ5Yi6YNw7i1bXmq9QwQ7EwMpeG4cK2pnMqEx1cLYD7cSGSCtruGSXC6ZSVDHugMsZgbuY62m6", + }, + { + name: "test vector 2 chain m/0/2147483647/1", + master: testVec2MasterPrivKey, + path: []uint32{0, 2147483647, 1}, + wantPriv: "xprv9ysS5br6UbWCRCJcggvpUNMyhVWgD7NypY9gsVTMYmuRtZg8izyYC5Ey4T931WgWbfJwRDwfVFqV3b29gqHDbuEpGcbzf16pdomk54NXkSm", + }, + { + name: "test vector 2 chain m/0/2147483647/1/2147483646", + master: testVec2MasterPrivKey, + path: []uint32{0, 2147483647, 1, 2147483646}, + wantPriv: "xprvA2LfeWWwRCxh4iqigcDMnUf2E3nVUFkntc93nmUYBtb9rpSPYWa8MY3x9ZHSLZkg4G84UefrDruVK3FhMLSJsGtBx883iddHNuH1LNpRrEp", + }, + { + name: "test vector 2 chain m/0/2147483647/1/2147483646/2", + master: testVec2MasterPrivKey, + path: []uint32{0, 2147483647, 1, 2147483646, 2}, + wantPriv: "xprvA48ALo8BDjcRET68R5RsPzF3H7WeyYYtHcyUeLRGBPHXu6CJSGjwW7dWoeUWTEzT7LG3qk6Eg6x2ZoqD8gtyEFZecpAyvchksfLyg3Zbqam", + }, + // Custom tests to trigger specific conditions. + { + // Seed 000000000000000000000000000000da. + name: "Derived privkey with zero high byte m/0", + master: "xprv9s21ZrQH143K4FR6rNeqEK4EBhRgLjWLWhA3pw8iqgAKk82ypz58PXbrzU19opYcxw8JDJQF4id55PwTsN1Zv8Xt6SKvbr2KNU5y8jN8djz", + path: []uint32{0}, + wantPriv: "xprv9uC5JqtViMmgcAMUxcsBCBFA7oYCNs4bozPbyvLfddjHou4rMiGEHipz94xNaPb1e4f18TRoPXfiXx4C3cDAcADqxCSRSSWLvMBRWPctSN9", + }, + } +tests: + for i, test := range tests { + extKey, e := NewKeyFromString(test.master) + if e != nil { + t.Errorf( + "NewKeyFromString #%d (%s): unexpected error "+ + "creating extended key: %v", i, test.name, + e, + ) + continue + } + for _, childNum := range test.path { + var e error + extKey, e = extKey.Child(childNum) + if e != nil { + t.Errorf("e: %v", e) + continue tests + } + } + privStr := extKey.String() + if privStr != test.wantPriv { + t.Errorf( + "Child #%d (%s): mismatched serialized "+ + "private extended key -- got: %s, want: %s", i, + test.name, privStr, test.wantPriv, + ) + continue + } + } +} + +// TestPublicDerivation tests several vectors which derive public keys from other public keys works as intended. +func TestPublicDerivation(t *testing.T) { + // The public extended keys for test vectors in [BIP32]. + testVec1MasterPubKey := "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8" + testVec2MasterPubKey := "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB" + tests := []struct { + name string + master string + path []uint32 + wantPub string + }{ + // Test vector 1 + { + name: "test vector 1 chain m", + master: testVec1MasterPubKey, + path: []uint32{}, + wantPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + }, + { + name: "test vector 1 chain m/0", + master: testVec1MasterPubKey, + path: []uint32{0}, + wantPub: "xpub68Gmy5EVb2BdFbj2LpWrk1M7obNuaPTpT5oh9QCCo5sRfqSHVYWex97WpDZzszdzHzxXDAzPLVSwybe4uPYkSk4G3gnrPqqkV9RyNzAcNJ1", + }, + { + name: "test vector 1 chain m/0/1", + master: testVec1MasterPubKey, + path: []uint32{0, 1}, + wantPub: "xpub6AvUGrnEpfvJBbfx7sQ89Q8hEMPM65UteqEX4yUbUiES2jHfjexmfJoxCGSwFMZiPBaKQT1RiKWrKfuDV4vpgVs4Xn8PpPTR2i79rwHd4Zr", + }, + { + name: "test vector 1 chain m/0/1/2", + master: testVec1MasterPubKey, + path: []uint32{0, 1, 2}, + wantPub: "xpub6BqyndF6rhZqmgktFCBcapkwubGxPqoAZtQaYewJHXVKZcLdnqBVC8N6f6FSHWUghjuTLeubWyQWfJdk2G3tGgvgj3qngo4vLTnnSjAZckv", + }, + { + name: "test vector 1 chain m/0/1/2/2", + master: testVec1MasterPubKey, + path: []uint32{0, 1, 2, 2}, + wantPub: "xpub6FHUhLbYYkgFQiFrDiXRfQFXBB2msCxKTsNyAExi6keFxQ8sHfwpogY3p3s1ePSpUqLNYks5T6a3JqpCGszt4kxbyq7tUoFP5c8KWyiDtPp", + }, + { + name: "test vector 1 chain m/0/1/2/2/1000000000", + master: testVec1MasterPubKey, + path: []uint32{0, 1, 2, 2, 1000000000}, + wantPub: "xpub6GX3zWVgSgPc5tgjE6ogT9nfwSADD3tdsxpzd7jJoJMqSY12Be6VQEFwDCp6wAQoZsH2iq5nNocHEaVDxBcobPrkZCjYW3QUmoDYzMFBDu9", + }, + // Test vector 2 + { + name: "test vector 2 chain m", + master: testVec2MasterPubKey, + path: []uint32{}, + wantPub: "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + }, + { + name: "test vector 2 chain m/0", + master: testVec2MasterPubKey, + path: []uint32{0}, + wantPub: "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + }, + { + name: "test vector 2 chain m/0/2147483647", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647}, + wantPub: "xpub6ASAVgeWMg4pmutghzHG3BohahjwNwPmy2DgM6W9wGegtPrvNgjBwuZRD7hSDFhYfunq8vDgwG4ah1gVzZysgp3UsKz7VNjCnSUJJ5T4fdD", + }, + { + name: "test vector 2 chain m/0/2147483647/1", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647, 1}, + wantPub: "xpub6CrnV7NzJy4VdgP5niTpqWJiFXMAca6qBm5Hfsry77SQmN1HGYHnjsZSujoHzdxf7ZNK5UVrmDXFPiEW2ecwHGWMFGUxPC9ARipss9rXd4b", + }, + { + name: "test vector 2 chain m/0/2147483647/1/2147483646", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647, 1, 2147483646}, + wantPub: "xpub6FL2423qFaWzHCvBndkN9cbkn5cysiUeFq4eb9t9kE88jcmY63tNuLNRzpHPdAM4dUpLhZ7aUm2cJ5zF7KYonf4jAPfRqTMTRBNkQL3Tfta", + }, + { + name: "test vector 2 chain m/0/2147483647/1/2147483646/2", + master: testVec2MasterPubKey, + path: []uint32{0, 2147483647, 1, 2147483646, 2}, + wantPub: "xpub6H7WkJf547AiSwAbX6xsm8Bmq9M9P1Gjequ5SipsjipWmtXSyp4C3uwzewedGEgAMsDy4jEvNTWtxLyqqHY9C12gaBmgUdk2CGmwachwnWK", + }, + } +tests: + for i, test := range tests { + extKey, e := NewKeyFromString(test.master) + if e != nil { + t.Errorf( + "NewKeyFromString #%d (%s): unexpected error "+ + "creating extended key: %v", i, test.name, + e, + ) + continue + } + for _, childNum := range test.path { + var e error + extKey, e = extKey.Child(childNum) + if e != nil { + t.Errorf("e: %v", e) + continue tests + } + } + pubStr := extKey.String() + if pubStr != test.wantPub { + t.Errorf( + "Child #%d (%s): mismatched serialized "+ + "public extended key -- got: %s, want: %s", i, + test.name, pubStr, test.wantPub, + ) + continue + } + } +} + +// TestGenenerateSeed ensures the GenerateSeed function works as intended. +func TestGenenerateSeed(t *testing.T) { + wantErr := errors.New("seed length must be between 128 and 512 bits") + tests := []struct { + name string + length uint8 + e error + }{ + // Test various valid lengths. + {name: "16 bytes", length: 16}, + {name: "17 bytes", length: 17}, + {name: "20 bytes", length: 20}, + {name: "32 bytes", length: 32}, + {name: "64 bytes", length: 64}, + // Test invalid lengths. + {name: "15 bytes", length: 15, e: wantErr}, + {name: "65 bytes", length: 65, e: wantErr}, + } + for i, test := range tests { + seed, e := GenerateSeed(test.length) + if !reflect.DeepEqual(e, test.e) { + t.Errorf( + "GenerateSeed #%d (%s): unexpected error -- "+ + "want %v, got %v", i, test.name, test.e, e, + ) + continue + } + if test.e == nil && len(seed) != int(test.length) { + t.Errorf( + "GenerateSeed #%d (%s): length mismatch -- "+ + "got %d, want %d", i, test.name, len(seed), + test.length, + ) + continue + } + } +} + +// // TestExtendedKeyAPI ensures the API on the ExtendedKey type works as intended. +// func TestExtendedKeyAPI(// t *testing.T) { +// tests := []struct { +// name string +// extKey string +// isPrivate bool +// parentFP uint32 +// privKey string +// privKeyErr error +// pubKey string +// address string +// }{ +// { +// name: "test vector 1 master node private", +// extKey: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", +// isPrivate: true, +// parentFP: 0, +// privKey: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", +// pubKey: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2", +// address: "15mKKb2eos1hWa6tisdPwwDC1a5J1y9nma", +// }, +// { +// name: "test vector 1 chain m/0H/1/2H public", +// extKey: "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", +// isPrivate: false, +// parentFP: 3203769081, +// privKeyErr: ErrNotPrivExtKey, +// pubKey: "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2", +// address: "1NjxqbA9aZWnh17q1UW3rB4EPu79wDXj7x", +// }, +// } +// for i, test := range tests { +// key, e := NewKeyFromString(test.extKey) +// if e != nil { +// t.Errorf("NewKeyFromString #%d (%s): unexpected "+ +// "error: %v", i, test.name, e) +// continue +// } +// if key.IsPrivate() != test.isPrivate { +// t.Errorf("IsPrivate #%d (%s): mismatched key type -- "+ +// "want private %v, got private %v", i, test.name, +// test.isPrivate, key.IsPrivate()) +// continue +// } +// parentFP := key.ParentFingerprint() +// if parentFP != test.parentFP { +// t.Errorf("ParentFingerprint #%d (%s): mismatched "+ +// "parent fingerprint -- want %d, got %d", i, +// test.name, test.parentFP, parentFP) +// continue +// } +// serializedKey := key.String() +// if serializedKey != test.extKey { +// t.Errorf("String #%d (%s): mismatched serialized key "+ +// "-- want %s, got %s", i, test.name, test.extKey, +// serializedKey) +// continue +// } +// privKey, e := key.ECPrivKey() +// if !reflect.DeepEqual(e, test.privKeyErr) { +// t.Errorf("ECPrivKey #%d (%s): mismatched error: want "+ +// "%v, got %v", i, test.name, test.privKeyErr, e) +// continue +// } +// if test.privKeyErr == nil { +// privKeyStr := hex.EncodeToString(privKey.Serialize()) +// if privKeyStr != test.privKey { +// t.Errorf("ECPrivKey #%d (%s): mismatched "+ +// "private key -- want %s, got %s", i, +// test.name, test.privKey, privKeyStr) +// continue +// } +// } +// pubKey, e := key.ECPubKey() +// if e != nil { +// t.Errorf("ECPubKey #%d (%s): unexpected error: %v", i, +// test.name, e) +// continue +// } +// pubKeyStr := hex.EncodeToString(pubKey.SerializeCompressed()) +// if pubKeyStr != test.pubKey { +// t.Errorf("ECPubKey #%d (%s): mismatched public key -- "+ +// "want %s, got %s", i, test.name, test.pubKey, +// pubKeyStr) +// continue +// } +// addr, e := key.Address(&chaincfg.MainNetParams) +// if e != nil { +// t.Errorf("Address #%d (%s): unexpected error: %v", i, +// test.name, e) +// continue +// } +// if addr.EncodeAddress() != test.address { +// t.Errorf("Address #%d (%s): mismatched address -- want "+ +// "%s, got %s", i, test.name, test.address, +// addr.EncodeAddress()) +// continue +// } +// } +// } + +// TestNet ensures the network related APIs work as intended. +func TestNet(t *testing.T) { + tests := []struct { + name string + key string + origNet *chaincfg.Params + newNet *chaincfg.Params + newPriv string + newPub string + isPrivate bool + }{ + // Private extended keys. + { + name: "mainnet -> simnet", + key: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + origNet: &chaincfg.MainNetParams, + newNet: &chaincfg.SimNetParams, + newPriv: "sprv8Erh3X3hFeKunvVdAGQQtambRPapECWiTDtvsTGdyrhzhbYgnSZajRRWbihzvq4AM4ivm6uso31VfKaukwJJUs3GYihXP8ebhMb3F2AHu3P", + newPub: "spub4Tr3T2ab61tD1Qa6GHwRFiiKyRRJdfEZpSpXfqgFYCEyaPsqKysqHDjzSzMJSiUEGbcsG3w2SLMoTqn44B8x6u3MLRRkYfACTUBnHK79THk", + isPrivate: true, + }, + { + name: "simnet -> mainnet", + key: "sprv8Erh3X3hFeKunvVdAGQQtambRPapECWiTDtvsTGdyrhzhbYgnSZajRRWbihzvq4AM4ivm6uso31VfKaukwJJUs3GYihXP8ebhMb3F2AHu3P", + origNet: &chaincfg.SimNetParams, + newNet: &chaincfg.MainNetParams, + newPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: true, + }, + { + name: "mainnet -> regtest", + key: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + origNet: &chaincfg.MainNetParams, + newNet: &chaincfg.RegressionTestParams, + newPriv: "tprv8ZgxMBicQKsPeDgjzdC36fs6bMjGApWDNLR9erAXMs5skhMv36j9MV5ecvfavji5khqjWaWSFhN3YcCUUdiKH6isR4Pwy3U5y5egddBr16m", + newPub: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", + isPrivate: true, + }, + { + name: "regtest -> mainnet", + key: "tprv8ZgxMBicQKsPeDgjzdC36fs6bMjGApWDNLR9erAXMs5skhMv36j9MV5ecvfavji5khqjWaWSFhN3YcCUUdiKH6isR4Pwy3U5y5egddBr16m", + origNet: &chaincfg.RegressionTestParams, + newNet: &chaincfg.MainNetParams, + newPriv: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: true, + }, + // Public extended keys. + { + name: "mainnet -> simnet", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + origNet: &chaincfg.MainNetParams, + newNet: &chaincfg.SimNetParams, + newPub: "spub4Tr3T2ab61tD1Qa6GHwRFiiKyRRJdfEZpSpXfqgFYCEyaPsqKysqHDjzSzMJSiUEGbcsG3w2SLMoTqn44B8x6u3MLRRkYfACTUBnHK79THk", + isPrivate: false, + }, + { + name: "simnet -> mainnet", + key: "spub4Tr3T2ab61tD1Qa6GHwRFiiKyRRJdfEZpSpXfqgFYCEyaPsqKysqHDjzSzMJSiUEGbcsG3w2SLMoTqn44B8x6u3MLRRkYfACTUBnHK79THk", + origNet: &chaincfg.SimNetParams, + newNet: &chaincfg.MainNetParams, + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: false, + }, + { + name: "mainnet -> regtest", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + origNet: &chaincfg.MainNetParams, + newNet: &chaincfg.RegressionTestParams, + newPub: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", + isPrivate: false, + }, + { + name: "regtest -> mainnet", + key: "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp", + origNet: &chaincfg.RegressionTestParams, + newNet: &chaincfg.MainNetParams, + newPub: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + isPrivate: false, + }, + } + for i, test := range tests { + extKey, e := NewKeyFromString(test.key) + if e != nil { + t.Errorf( + "NewKeyFromString #%d (%s): unexpected error "+ + "creating extended key: %v", i, test.name, + e, + ) + continue + } + if !extKey.IsForNet(test.origNet) { + t.Errorf( + "IsForNet #%d (%s): key is not for expected "+ + "network %v", i, test.name, test.origNet.Name, + ) + continue + } + extKey.SetNet(test.newNet) + if !extKey.IsForNet(test.newNet) { + t.Errorf( + "SetNet/IsForNet #%d (%s): key is not for "+ + "expected network %v", i, test.name, + test.newNet.Name, + ) + continue + } + if test.isPrivate { + privStr := extKey.String() + if privStr != test.newPriv { + t.Errorf( + "Serialize #%d (%s): mismatched serialized "+ + "private extended key -- got: %s, want: %s", i, + test.name, privStr, test.newPriv, + ) + continue + } + extKey, e = extKey.Neuter() + if e != nil { + t.Errorf( + "Neuter #%d (%s): unexpected error: %v ", i, + test.name, e, + ) + continue + } + } + pubStr := extKey.String() + if pubStr != test.newPub { + t.Errorf( + "Neuter #%d (%s): mismatched serialized "+ + "public extended key -- got: %s, want: %s", i, + test.name, pubStr, test.newPub, + ) + continue + } + } +} + +// TestErrors performs some negative tests for various invalid cases to ensure the errors are handled properly. +func TestErrors(t *testing.T) { + // Should get an error when seed has too few bytes. + net := &chaincfg.MainNetParams + _, e := NewMaster(bytes.Repeat([]byte{0x00}, 15), net) + if e != ErrInvalidSeedLen { + t.Fatalf( + "NewMaster: mismatched error -- got: %v, want: %v", + e, ErrInvalidSeedLen, + ) + } + // Should get an error when seed has too many bytes. + _, e = NewMaster(bytes.Repeat([]byte{0x00}, 65), net) + if e != ErrInvalidSeedLen { + t.Fatalf( + "NewMaster: mismatched error -- got: %v, want: %v", + e, ErrInvalidSeedLen, + ) + } + // Generate a new key and neuter it to a public extended key. + seed, e := GenerateSeed(RecommendedSeedLen) + if e != nil { + t.Fatalf("GenerateSeed: unexpected error: %v", e) + } + extKey, e := NewMaster(seed, net) + if e != nil { + t.Fatalf("NewMaster: unexpected error: %v", e) + } + pubKey, e := extKey.Neuter() + if e != nil { + t.Fatalf("Neuter: unexpected error: %v", e) + } + // Deriving a hardened child extended key should fail from a public key. + _, e = pubKey.Child(HardenedKeyStart) + if e != ErrDeriveHardFromPublic { + t.Fatalf( + "Child: mismatched error -- got: %v, want: %v", + e, ErrDeriveHardFromPublic, + ) + } + // NewKeyFromString failure tests. + tests := []struct { + name string + key string + e error + neuter bool + neuterErr error + }{ + { + name: "invalid key length", + key: "xpub1234", + e: ErrInvalidKeyLen, + }, + { + name: "bad checksum", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EBygr15", + e: ErrBadChecksum, + }, + { + name: "pubkey not on curve", + key: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ1hr9Rwbk95YadvBkQXxzHBSngB8ndpW6QH7zhhsXZ2jHyZqPjk", + e: errors.New("invalid square root"), + }, + { + name: "unsupported version", + key: "xbad4LfUL9eKmA66w2GJdVMqhvDmYGJpTGjWRAtjHqoUY17sGaymoMV9Cm3ocn9Ud6Hh2vLFVC7KSKCRVVrqc6dsEdsTjRV1WUmkK85YEUujAPX", + e: nil, + neuter: true, + neuterErr: chaincfg.ErrUnknownHDKeyID, + }, + } + for i, test := range tests { + extKey, e := NewKeyFromString(test.key) + if !reflect.DeepEqual(e, test.e) { + t.Errorf( + "NewKeyFromString #%d (%s): mismatched error "+ + "-- got: %v, want: %v", i, test.name, e, + test.e, + ) + continue + } + if test.neuter { + _, e := extKey.Neuter() + if !reflect.DeepEqual(e, test.neuterErr) { + t.Errorf( + "Neuter #%d (%s): mismatched error "+ + "-- got: %v, want: %v", i, test.name, + e, test.neuterErr, + ) + continue + } + } + } +} + +// // TestZero ensures that zeroing an extended key works as intended. +// func TestZero(// t *testing.T) { +// tests := []struct { +// name string +// master string +// extKey string +// net *chaincfg.Params +// }{ +// // Test vector 1 +// { +// name: "test vector 1 chain m", +// master: "000102030405060708090a0b0c0d0e0f", +// extKey: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", +// net: &chaincfg.MainNetParams, +// }, +// // Test vector 2 +// { +// name: "test vector 2 chain m", +// master: "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", +// extKey: "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", +// net: &chaincfg.MainNetParams, +// }, +// } +// // Use a closure to test that a key is zeroed since the tests create +// // keys in different ways and need to test the same things multiple +// // times. +// testZeroed := func(i int, testName string, key *ExtendedKey) bool { +// // Zeroing a key should result in it no longer being private +// if key.IsPrivate() { +// t.Errorf("IsPrivate #%d (%s): mismatched key type -- "+ +// "want private %v, got private %v", i, testName, +// false, key.IsPrivate()) +// return false +// } +// parentFP := key.ParentFingerprint() +// if parentFP != 0 { +// t.Errorf("ParentFingerprint #%d (%s): mismatched "+ +// "parent fingerprint -- want %d, got %d", i, +// testName, 0, parentFP) +// return false +// } +// wantKey := "zeroed extended key" +// serializedKey := key.String() +// if serializedKey != wantKey { +// t.Errorf("String #%d (%s): mismatched serialized key "+ +// "-- want %s, got %s", i, testName, wantKey, +// serializedKey) +// return false +// } +// wantErr := ErrNotPrivExtKey +// _, e := key.ECPrivKey() +// if !reflect.DeepEqual(e, wantErr) { +// t.Errorf("ECPrivKey #%d (%s): mismatched error: want "+ +// "%v, got %v", i, testName, wantErr, e) +// return false +// } +// wantErr = errors.New("pubkey string is empty") +// _, e = key.ECPubKey() +// if !reflect.DeepEqual(e, wantErr) { +// t.Errorf("ECPubKey #%d (%s): mismatched error: want "+ +// "%v, got %v", i, testName, wantErr, e) +// return false +// } +// wantAddr := "1HT7xU2Ngenf7D4yocz2SAcnNLW7rK8d4E" +// addr, e := key.Address(&chaincfg.MainNetParams) +// if e != nil { +// t.Errorf("Addres s #%d (%s): unexpected error: %v", i, +// testName, e) +// return false +// } +// if addr.EncodeAddress() != wantAddr { +// t.Errorf("Address #%d (%s): mismatched address -- want "+ +// "%s, got %s", i, testName, wantAddr, +// addr.EncodeAddress()) +// return false +// } +// return true +// } +// for i, test := range tests { +// // Create new key from seed and get the neutered version. +// masterSeed, e := hex.DecodeString(test.master) +// if e != nil { +// t.Errorf("DecodeString #%d (%s): unexpected error: %v", +// i, test.name, e) +// continue +// } +// key, e := NewMaster(masterSeed, test.net) +// if e != nil { +// t.Errorf("NewMaster #%d (%s): unexpected error when "+ +// "creating new master key: %v", i, test.name, +// e) +// continue +// } +// neuteredKey, e := key.Neuter() +// if e != nil { +// t.Errorf("Neuter #%d (%s): unexpected error: %v", i, +// test.name, e) +// continue +// } +// // Ensure both non-neutered and neutered keys are zeroed properly. +// key.Zero() +// if !testZeroed(i, test.name+" from seed not neutered", key) { +// continue +// } +// neuteredKey.Zero() +// if !testZeroed(i, test.name+" from seed neutered", key) { +// continue +// } +// // Deserialize key and get the neutered version. +// key, e = NewKeyFromString(test.extKey) +// if e != nil { +// t.Errorf("NewKeyFromString #%d (%s): unexpected "+ +// "error: %v", i, test.name, e) +// continue +// } +// neuteredKey, e = key.Neuter() +// if e != nil { +// t.Errorf("Neuter #%d (%s): unexpected error: %v", i, +// test.name, e) +// continue +// } +// // Ensure both non-neutered and neutered keys are zeroed properly. +// key.Zero() +// if !testZeroed(i, test.name+" deserialized not neutered", key) { +// continue +// } +// neuteredKey.Zero() +// if !testZeroed(i, test.name+" deserialized neutered", key) { +// continue +// } +// } +// } + +// TestMaximumDepth ensures that attempting to retrieve a child key when already at the maximum depth is not allowed. The serialization of a BIP32 key uses uint8 to encode the depth. This implicitly bounds the depth of the tree to 255 derivations. Here we test that an error is returned after 'max uint8'. +func TestMaximumDepth(t *testing.T) { + net := &chaincfg.MainNetParams + extKey, e := NewMaster([]byte(`abcd1234abcd1234abcd1234abcd1234`), net) + if e != nil { + t.Fatalf("NewMaster: unexpected error: %v", e) + } + for i := uint8(0); i < math.MaxUint8; i++ { + if extKey.Depth() != i { + t.Fatalf( + "extendedkey depth %d should match expected value %d", + extKey.Depth(), i, + ) + } + var newKey *ExtendedKey + newKey, e = extKey.Child(1) + if e != nil { + t.Fatalf("Child: unexpected error: %v", e) + } + extKey = newKey + } + noKey, e := extKey.Child(1) + if e != ErrDeriveBeyondMaxDepth { + t.Fatalf( + "Child: mismatched error: want %v, got %v", + ErrDeriveBeyondMaxDepth, e, + ) + } + if noKey != nil { + t.Fatal("Child: deriving 256th key should not succeed") + } +} diff --git a/pkg/util/hdkeychain/log.go b/pkg/util/hdkeychain/log.go new file mode 100644 index 0000000..95e8640 --- /dev/null +++ b/pkg/util/hdkeychain/log.go @@ -0,0 +1,43 @@ +package hdkeychain + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/util/helpers/helpers.go b/pkg/util/helpers/helpers.go new file mode 100644 index 0000000..ede70fb --- /dev/null +++ b/pkg/util/helpers/helpers.go @@ -0,0 +1,24 @@ +// Package helpers provides convenience functions to simplify wallet code. This package is intended for internal wallet +// use only. +package helpers + +import ( + "github.com/p9c/p9/pkg/amt" + "github.com/p9c/p9/pkg/wire" +) + +// SumOutputValues sums up the list of TxOuts and returns an Amount. +func SumOutputValues(outputs []*wire.TxOut) (totalOutput amt.Amount) { + for _, txOut := range outputs { + totalOutput += amt.Amount(txOut.Value) + } + return totalOutput +} + +// SumOutputSerializeSizes sums up the serialized size of the supplied outputs. +func SumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} diff --git a/pkg/util/helpers/log.go b/pkg/util/helpers/log.go new file mode 100644 index 0000000..99e5b3a --- /dev/null +++ b/pkg/util/helpers/log.go @@ -0,0 +1,43 @@ +package helpers + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/util/lang/goApp.go b/pkg/util/lang/goApp.go new file mode 100644 index 0000000..fe340c5 --- /dev/null +++ b/pkg/util/lang/goApp.go @@ -0,0 +1,57 @@ +package lang + +func goAppDict() Com { + return Com{ + Component: "goApp", + Languages: []Language{ + { + Code: "en", + Definitions: []Text{ + { + ID: "DESCRIPTION", + Definition: "Parallelcoin Pod Suite -- All-in-one everything" + + " for Parallelcoin!", + }, + { + ID: "COPYRIGHT", + Definition: "Legacy portions derived from btcsuite/btcd under" + + " ISC licence. The remainder is already in your" + + " possession. Use it wisely.", + }, + { + ID: "NOSUBCMDREQ", + Definition: "no subcommand requested", + }, + { + ID: "STARTINGNODE", + Definition: "starting node", + }, + }, + }, + { + Code: "rs", + Definitions: []Text{ + { + ID: "DESCRIPTION", + Definition: "ParalelniNovcic Pod Programski Paket -- Sve-u-jednom garnitura" + + " za ParalelniNovcic!", + }, + { + ID: "COPYRIGHT", + Definition: "Izvedeni deovi iz btcsuite/btcd su pod" + + " ISC licencom. Sve ostalo je u vasem" + + " vlasnistvu. Koristite se DUO-m mudro!", + }, + { + ID: "NOSUBCMDREQ", + Definition: "nije pozvana pod komanda", + }, + { + ID: "STARTINGNODE", + Definition: "pokrece se ", + }, + }, + }, + }, + } +} diff --git a/pkg/util/lang/lang.go b/pkg/util/lang/lang.go new file mode 100644 index 0000000..21e8cd5 --- /dev/null +++ b/pkg/util/lang/lang.go @@ -0,0 +1,38 @@ +package lang + +type Text struct { + ID string + Definition string +} + +type Language struct { + Code string + Definitions []Text +} +type Com struct { + Component string + Languages []Language +} +type Dictionary []Com + +type Lexicon map[string]string + +var dict Dictionary + +func ExportLanguage(l string) *Lexicon { + lex := Lexicon{} + d := Dictionary{} + d = append(d, goAppDict()) + for _, c := range d { + for _, lang := range c.Languages { + for _, def := range lang.Definitions { + lex[c.Component+"_"+def.ID] = def.Definition + } + } + } + return &lex +} + +func (l *Lexicon) RenderText(id string) string { + return (*l)[id] +} diff --git a/pkg/util/lang/log.go b/pkg/util/lang/log.go new file mode 100644 index 0000000..fb91299 --- /dev/null +++ b/pkg/util/lang/log.go @@ -0,0 +1,43 @@ +package lang + +import ( + "github.com/p9c/p9/pkg/log" + "github.com/p9c/p9/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/util/legacy/keystore/keystore.go b/pkg/util/legacy/keystore/keystore.go new file mode 100644 index 0000000..8d6cbf0 --- /dev/null +++ b/pkg/util/legacy/keystore/keystore.go @@ -0,0 +1,2865 @@ +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha512" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "github.com/p9c/p9/pkg/btcaddr" + "github.com/p9c/p9/pkg/chaincfg" + "io" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "sync" + "time" + + "golang.org/x/crypto/ripemd160" + + "github.com/p9c/p9/pkg/chainhash" + ec "github.com/p9c/p9/pkg/ecc" + "github.com/p9c/p9/pkg/txscript" + "github.com/p9c/p9/pkg/util" + "github.com/p9c/p9/pkg/util/legacy/rename" + "github.com/p9c/p9/pkg/wire" +) + +// A bunch of constants +const ( + Filename = "wallet.bin" + // Length in bytes of KDF output. + kdfOutputBytes = 32 + // Maximum length in bytes of a comment that can have a size represented as a uint16. + maxCommentLen = (1 << 16) - 1 + defaultKdfComputeTime = 0.25 + defaultKdfMaxMem = 32 * 1024 * 1024 +) + +// Possible errors when dealing with key stores. +var ( + ErrAddressNotFound = errors.New("address not found") + ErrAlreadyEncrypted = errors.New("private key is already encrypted") + ErrChecksumMismatch = errors.New("checksum mismatch") + ErrDuplicate = errors.New("duplicate key or address") + ErrMalformedEntry = errors.New("malformed entry") + ErrWatchingOnly = errors.New("keystore is watching-only") + ErrLocked = errors.New("keystore is locked") + ErrWrongPassphrase = errors.New("wrong passphrase") +) +var fileID = [8]byte{0xba, 'W', 'A', 'L', 'L', 'E', 'T', 0x00} + +type entryHeader byte + +const ( + addrCommentHeader entryHeader = 1 << iota + txCommentHeader + deletedHeader + scriptHeader + addrHeader entryHeader = 0 +) + +// We want to use binaryRead and binaryWrite instead of binary.Read and binary.Write because those from the binary +// package do not return the number of bytes actually written or read. We need to return this value to correctly support +// the io.ReaderFrom and io.WriterTo interfaces. +func binaryRead(r io.Reader, order binary.ByteOrder, data interface{}) (n int64, e error) { + var read int + buf := make([]byte, binary.Size(data)) + if read, e = io.ReadFull(r, buf); E.Chk(e) { + return int64(read), e + } + return int64(read), binary.Read(bytes.NewBuffer(buf), order, data) +} + +// See comment for binaryRead(). +func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64, e error) { + buf := bytes.Buffer{} + if e = binary.Write(&buf, order, data); E.Chk(e) { + return 0, e + } + written, e := w.Write(buf.Bytes()) + return int64(written), e +} + +// pubkeyFromPrivkey creates an encoded pubkey based on a 32-byte privkey. The returned pubkey is 33 bytes if +// compressed, or 65 bytes if uncompressed. +func pubkeyFromPrivkey(privkey []byte, compress bool) (pubkey []byte) { + _, pk := ec.PrivKeyFromBytes(ec.S256(), privkey) + if compress { + return pk.SerializeCompressed() + } + return pk.SerializeUncompressed() +} +func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { + saltedpass := append(passphrase, salt...) + lutbl := make([]byte, memReqts) + // Seed for lookup table + seed := sha512.Sum512(saltedpass) + copy(lutbl[:sha512.Size], seed[:]) + for nByte := 0; nByte < (int(memReqts) - sha512.Size); nByte += sha512.Size { + hash := sha512.Sum512(lutbl[nByte : nByte+sha512.Size]) + copy(lutbl[nByte+sha512.Size:nByte+2*sha512.Size], hash[:]) + } + x := lutbl[cap(lutbl)-sha512.Size:] + seqCt := uint32(memReqts / sha512.Size) + nLookups := seqCt / 2 + for i := uint32(0); i < nLookups; i++ { + // Armory ignores endianness here. We assume LE. + newIdx := binary.LittleEndian.Uint32(x[cap(x)-4:]) % seqCt + // Index of hash result at newIdx + vIdx := newIdx * sha512.Size + v := lutbl[vIdx : vIdx+sha512.Size] + // XOR hash x with hash v + for j := 0; j < sha512.Size; j++ { + x[j] ^= v[j] + } + // Save new hash to x + hash := sha512.Sum512(x) + copy(x, hash[:]) + } + return x[:kdfOutputBytes] +} + +// kdf implements the key derivation function used by Armory based on the ROMix algorithm described in Colin Percival's +// paper "Stronger Key Derivation via Sequential Memory-Hard Functions" (http://www.tarsnap.com/scrypt/scrypt.pdf). +func kdf(passphrase []byte, params *kdfParameters) []byte { + masterKey := passphrase + for i := uint32(0); i < params.nIter; i++ { + masterKey = keyOneIter(masterKey, params.salt[:], params.mem) + } + return masterKey +} +func pad(size int, b []byte) []byte { + // Prevent a possible panic if the input exceeds the expected size. + if len(b) > size { + size = len(b) + } + p := make([]byte, size) + copy(p[size-len(b):], b) + return p +} + +// chainedPrivKey deterministically generates a new private key using a previous address and chaincode. privkey and +// chaincode must be 32 bytes long, and pubkey may either be 33 or 65 bytes. +func chainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { + if len(privkey) != 32 { + return nil, fmt.Errorf( + "invalid privkey length %d (must be 32)", + len(privkey), + ) + } + if len(chaincode) != 32 { + return nil, fmt.Errorf( + "invalid chaincode length %d (must be 32)", + len(chaincode), + ) + } + switch n := len(pubkey); n { + case ec.PubKeyBytesLenUncompressed, ec.PubKeyBytesLenCompressed: + // Correct length + default: + return nil, fmt.Errorf("invalid pubkey length %d", n) + } + xorbytes := make([]byte, 32) + chainMod := chainhash.DoubleHashB(pubkey) + for i := range xorbytes { + xorbytes[i] = chainMod[i] ^ chaincode[i] + } + chainXor := new(big.Int).SetBytes(xorbytes) + privint := new(big.Int).SetBytes(privkey) + t := new(big.Int).Mul(chainXor, privint) + b := t.Mod(t, ec.S256().N).Bytes() + return pad(32, b), nil +} + +// chainedPubKey deterministically generates a new public key using a previous public key and chaincode. pubkey must be +// 33 or 65 bytes, and chaincode must be 32 bytes long. +func chainedPubKey(pubkey, chaincode []byte) ([]byte, error) { + var compressed bool + switch n := len(pubkey); n { + case ec.PubKeyBytesLenUncompressed: + compressed = false + case ec.PubKeyBytesLenCompressed: + compressed = true + default: + // Incorrect serialized pubkey length + return nil, fmt.Errorf("invalid pubkey length %d", n) + } + if len(chaincode) != 32 { + return nil, fmt.Errorf( + "invalid chaincode length %d (must be 32)", + len(chaincode), + ) + } + xorbytes := make([]byte, 32) + chainMod := chainhash.DoubleHashB(pubkey) + for i := range xorbytes { + xorbytes[i] = chainMod[i] ^ chaincode[i] + } + oldPk, e := ec.ParsePubKey(pubkey, ec.S256()) + if e != nil { + E.Ln(e) + return nil, e + } + newX, newY := ec.S256().ScalarMult(oldPk.X, oldPk.Y, xorbytes) + // if e != nil { + // return nil, e + // } + newPk := &ec.PublicKey{ + Curve: ec.S256(), + X: newX, + Y: newY, + } + if compressed { + return newPk.SerializeCompressed(), nil + } + return newPk.SerializeUncompressed(), nil +} + +type ksVersion struct { + major byte + minor byte + bugfix byte + autoincrement byte +} + +// Enforce that ksVersion satisfies the io.ReaderFrom and io.WriterTo interfaces. +var _ io.ReaderFrom = &ksVersion{} +var _ io.WriterTo = &ksVersion{} + +// readerFromVersion is an io.ReaderFrom and io.WriterTo that can specify any particular key store file format for +// reading depending on the key store file ksVersion. +type readerFromVersion interface { + readFromVersion(ksVersion, io.Reader) (int64, error) + io.WriterTo +} + +func (v ksVersion) String() string { + str := fmt.Sprintf("%d.%d", v.major, v.minor) + if v.bugfix != 0x00 || v.autoincrement != 0x00 { + str += fmt.Sprintf(".%d", v.bugfix) + } + if v.autoincrement != 0x00 { + str += fmt.Sprintf(".%d", v.autoincrement) + } + return str +} +func (v ksVersion) Uint32() uint32 { + return uint32(v.major)<<6 | uint32(v.minor)<<4 | uint32(v.bugfix)<<2 | uint32(v.autoincrement) +} +func (v *ksVersion) ReadFrom(r io.Reader) (int64, error) { + // Read 4 bytes for the ksVersion. + var versBytes [4]byte + n, e := io.ReadFull(r, versBytes[:]) + if e != nil { + E.Ln(e) + return int64(n), e + } + v.major = versBytes[0] + v.minor = versBytes[1] + v.bugfix = versBytes[2] + v.autoincrement = versBytes[3] + return int64(n), nil +} +func (v *ksVersion) WriteTo(w io.Writer) (int64, error) { + // Write 4 bytes for the ksVersion. + versBytes := []byte{ + v.major, + v.minor, + v.bugfix, + v.autoincrement, + } + n, e := w.Write(versBytes) + return int64(n), e +} + +// LT returns whether v is an earlier ksVersion than v2. +func (v ksVersion) LT(v2 ksVersion) bool { + switch { + case v.major < v2.major: + return true + case v.minor < v2.minor: + return true + case v.bugfix < v2.bugfix: + return true + case v.autoincrement < v2.autoincrement: + return true + default: + return false + } +} + +// EQ returns whether v2 is an equal ksVersion to v. +func (v ksVersion) EQ(v2 ksVersion) bool { + switch { + case v.major != v2.major: + return false + case v.minor != v2.minor: + return false + case v.bugfix != v2.bugfix: + return false + case v.autoincrement != v2.autoincrement: + return false + default: + return true + } +} + +// GT returns whether v is a later ksVersion than v2. +func (v ksVersion) GT(v2 ksVersion) bool { + switch { + case v.major > v2.major: + return true + case v.minor > v2.minor: + return true + case v.bugfix > v2.bugfix: + return true + case v.autoincrement > v2.autoincrement: + return true + default: + return false + } +} + +// Various versions. +var ( + // VersArmory is the latest ksVersion used by Armory. + VersArmory = ksVersion{1, 35, 0, 0} + // Vers20LastBlocks is the ksVersion where key store files now hold the 20 most recently seen block hashes. + Vers20LastBlocks = ksVersion{1, 36, 0, 0} + // VersUnsetNeedsPrivkeyFlag is the bugfix ksVersion where the createPrivKeyNextUnlock address flag is correctly unset + // after creating and encrypting its private key after unlock. + // + // Otherwise, re-creating private keys will occur too early in the address chain and fail due to encrypting an + // already encrypted address. + // + // Key store versions at or before this ksVersion include a special case to allow the duplicate encrypt. + VersUnsetNeedsPrivkeyFlag = ksVersion{1, 36, 1, 0} + // VersCurrent is the current key store file ksVersion. + VersCurrent = VersUnsetNeedsPrivkeyFlag +) + +type varEntries struct { + store *Store + entries []io.WriterTo +} + +func (v *varEntries) WriteTo(w io.Writer) (n int64, e error) { + ss := v.entries + var written int64 + for _, s := range ss { + var e error + if written, e = s.WriteTo(w); E.Chk(e) { + return n + written, e + } + n += written + } + return n, nil +} +func (v *varEntries) ReadFrom(r io.Reader) (n int64, e error) { + var read int64 + // Remove any previous entries. + v.entries = nil + wts := v.entries + // Keep reading entries until an EOF is reached. + for { + var header entryHeader + if read, e = binaryRead(r, binary.LittleEndian, &header); E.Chk(e) { + // EOF here is not an error. + if e == io.EOF { + return n + read, nil + } + return n + read, e + } + n += read + var wt io.WriterTo + switch header { + case addrHeader: + var entry addrEntry + entry.addr.store = v.store + if read, e = entry.ReadFrom(r); E.Chk(e) { + return n + read, e + } + n += read + wt = &entry + case scriptHeader: + var entry scriptEntry + entry.script.store = v.store + if read, e = entry.ReadFrom(r); E.Chk(e) { + return n + read, e + } + n += read + wt = &entry + default: + return n, fmt.Errorf("unknown entry header: %d", uint8(header)) + } + if wt != nil { + wts = append(wts, wt) + v.entries = wts + } + } +} + +// Key stores use a custom network parameters type so it can be an io.ReaderFrom. +// +// Due to the way and order that key stores are currently serialized and how address reading requires the key store's +// network parameters, setting and failing on unknown key store networks must happen on the read itself and not after +// the fact. +// +// This is admittedly a hack, but with a bip32 keystore on the horizon I'm not too motivated to clean this up. +type netParams chaincfg.Params + +func (net *netParams) ReadFrom(r io.Reader) (int64, error) { + var buf [4]byte + uint32Bytes := buf[:4] + n, e := io.ReadFull(r, uint32Bytes) + n64 := int64(n) + if e != nil { + E.Ln(e) + return n64, e + } + switch wire.BitcoinNet(binary.LittleEndian.Uint32(uint32Bytes)) { + case wire.MainNet: + *net = netParams(chaincfg.MainNetParams) + case wire.TestNet3: + *net = netParams(chaincfg.TestNet3Params) + case wire.SimNet: + *net = netParams(chaincfg.SimNetParams) + default: + return n64, errors.New("unknown network") + } + return n64, nil +} +func (net *netParams) WriteTo(w io.Writer) (int64, error) { + var buf [4]byte + uint32Bytes := buf[:4] + binary.LittleEndian.PutUint32(uint32Bytes, uint32(net.Net)) + n, e := w.Write(uint32Bytes) + n64 := int64(n) + return n64, e +} + +// Stringified byte slices for use as map lookup keys. +type addressKey string + +// type transactionHashKey string +// type comment []byte + +func getAddressKey(addr btcaddr.Address) addressKey { + return addressKey(addr.ScriptAddress()) +} + +// Store represents an key store in memory. It implements the io.ReaderFrom and io.WriterTo interfaces to read from and +// write to any type of byte streams, including files. +type Store struct { + // TODO: Use atomic operations for dirty so the reader lock doesn't need to be grabbed. + dirty bool + path string + dir string + file string + mtx sync.RWMutex + vers ksVersion + net *netParams + flags walletFlags + createDate int64 + name [32]byte + desc [256]byte + highestUsed int64 + kdfParams kdfParameters + keyGenerator btcAddress + // These are non-standard and fit in the extra 1024 bytes between the root address and the appended entries. + recent recentBlocks + addrMap map[addressKey]walletAddress + // The rest of the fields in this struct are not serialized. + passphrase []byte + secret []byte + chainIdxMap map[int64]btcaddr.Address + importedAddrs []walletAddress + lastChainIdx int64 + missingKeysStart int64 +} + +// New creates and initializes a new Store. name's and desc's byte length must not exceed 32 and 256 bytes, +// respectively. All address private keys are encrypted with passphrase. The key store is returned locked. +func New( + dir string, desc string, passphrase []byte, net *chaincfg.Params, + createdAt *BlockStamp, +) (s *Store, e error) { + // Chk txsizes of inputs. + if len(desc) > 256 { + return nil, errors.New("desc exceeds 256 byte maximum size") + } + // Randomly-generate rootkey and chaincode. + rootkey := make([]byte, 32) + if _, e = rand.Read(rootkey); E.Chk(e) { + return nil, e + } + chaincode := make([]byte, 32) + if _, e = rand.Read(chaincode); E.Chk(e) { + return nil, e + } + // Compute AES key and encrypt root address. + kdfp, e := computeKdfParameters(defaultKdfComputeTime, defaultKdfMaxMem) + if e != nil { + E.Ln(e) + return nil, e + } + aeskey := kdf(passphrase, kdfp) + // Create and fill key store. + s = &Store{ + path: filepath.Join(dir, Filename), + dir: dir, + file: Filename, + vers: VersCurrent, + net: (*netParams)(net), + flags: walletFlags{ + useEncryption: true, + watchingOnly: false, + }, + createDate: time.Now().Unix(), + highestUsed: rootKeyChainIdx, + kdfParams: *kdfp, + recent: recentBlocks{ + lastHeight: createdAt.Height, + hashes: []*chainhash.Hash{ + createdAt.Hash, + }, + }, + addrMap: make(map[addressKey]walletAddress), + chainIdxMap: make(map[int64]btcaddr.Address), + lastChainIdx: rootKeyChainIdx, + missingKeysStart: rootKeyChainIdx, + secret: aeskey, + } + copy(s.desc[:], desc) + // Create new root address from key and chaincode. + root, e := newRootBtcAddress( + s, rootkey, nil, chaincode, + createdAt, + ) + if e != nil { + return nil, e + } + // Verify root address keypairs. + if e := root.verifyKeypairs(); E.Chk(e) { + return nil, e + } + if e := root.encrypt(aeskey); E.Chk(e) { + return nil, e + } + s.keyGenerator = *root + // Add root address to maps. + rootAddr := s.keyGenerator.Address() + s.addrMap[getAddressKey(rootAddr)] = &s.keyGenerator + s.chainIdxMap[rootKeyChainIdx] = rootAddr + // key store must be returned locked. + if e := s.Lock(); E.Chk(e) { + return nil, e + } + return s, nil +} + +// ReadFrom reads data from a io.Reader and saves it to a key store, returning the number of bytes read and any errors +// encountered. +func (s *Store) ReadFrom(r io.Reader) (n int64, e error) { + s.mtx.Lock() + defer s.mtx.Unlock() + var read int64 + s.net = &netParams{} + s.addrMap = make(map[addressKey]walletAddress) + s.chainIdxMap = make(map[int64]btcaddr.Address) + var id [8]byte + appendedEntries := varEntries{store: s} + s.keyGenerator.store = s + // Iterate through each entry needing to be read. If data implements io.ReaderFrom, use its ReadFrom func. + // Otherwise, data is a pointer to a fixed sized value. + datas := []interface{}{ + &id, + &s.vers, + s.net, + &s.flags, + make([]byte, 6), // Hash for Armory unique ID + &s.createDate, + &s.name, + &s.desc, + &s.highestUsed, + &s.kdfParams, + make([]byte, 256), + &s.keyGenerator, + newUnusedSpace(1024, &s.recent), + &appendedEntries, + } + for _, data := range datas { + var e error + switch d := data.(type) { + case readerFromVersion: + read, e = d.readFromVersion(s.vers, r) + case io.ReaderFrom: + read, e = d.ReadFrom(r) + default: + read, e = binaryRead(r, binary.LittleEndian, d) + } + n += read + if e != nil { + return n, e + } + } + if id != fileID { + return n, errors.New("unknown file ID") + } + // Add root address to address map. + rootAddr := s.keyGenerator.Address() + s.addrMap[getAddressKey(rootAddr)] = &s.keyGenerator + s.chainIdxMap[rootKeyChainIdx] = rootAddr + s.lastChainIdx = rootKeyChainIdx + // Fill unserializied fields. + wts := appendedEntries.entries + for _, wt := range wts { + switch e := wt.(type) { + case *addrEntry: + addr := e.addr.Address() + s.addrMap[getAddressKey(addr)] = &e.addr + if e.addr.Imported() { + s.importedAddrs = append(s.importedAddrs, &e.addr) + } else { + s.chainIdxMap[e.addr.chainIndex] = addr + if s.lastChainIdx < e.addr.chainIndex { + s.lastChainIdx = e.addr.chainIndex + } + } + // If the private keys have not been created yet, mark the + // earliest so all can be created on next key store unlock. + if e.addr.flags.createPrivKeyNextUnlock { + switch { + case s.missingKeysStart == rootKeyChainIdx: + fallthrough + case e.addr.chainIndex < s.missingKeysStart: + s.missingKeysStart = e.addr.chainIndex + } + } + case *scriptEntry: + addr := e.script.Address() + s.addrMap[getAddressKey(addr)] = &e.script + // script are always imported. + s.importedAddrs = append(s.importedAddrs, &e.script) + default: + return n, errors.New("unknown appended entry") + } + } + return n, nil +} + +// WriteTo serializes a key store and writes it to a io.Writer, returning the number of bytes written and any errors +// encountered. +func (s *Store) WriteTo(w io.Writer) (n int64, e error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.writeTo(w) +} +func (s *Store) writeTo(w io.Writer) (n int64, e error) { + var wts []io.WriterTo + var chainedAddrs = make([]io.WriterTo, len(s.chainIdxMap)-1) + var importedAddrs []io.WriterTo + for _, wAddr := range s.addrMap { + switch btcAddr := wAddr.(type) { + case *btcAddress: + e := &addrEntry{ + addr: *btcAddr, + } + copy(e.pubKeyHash160[:], btcAddr.AddrHash()) + if btcAddr.Imported() { + // No order for imported addresses. + importedAddrs = append(importedAddrs, e) + } else if btcAddr.chainIndex >= 0 { + // Chained addresses are sorted. This is + // kind of nice but probably isn't necessary. + chainedAddrs[btcAddr.chainIndex] = e + } + case *scriptAddress: + e := &scriptEntry{ + script: *btcAddr, + } + copy(e.scriptHash160[:], btcAddr.AddrHash()) + // scripts are always imported + importedAddrs = append(importedAddrs, e) + } + } + wts = append(chainedAddrs, importedAddrs...) + appendedEntries := varEntries{store: s, entries: wts} + // Iterate through each entry needing to be written. If data implements io.WriterTo, use its WriteTo func. + // Otherwise, data is a pointer to a fixed size value. + datas := []interface{}{ + &fileID, + &VersCurrent, + s.net, + &s.flags, + make([]byte, 6), // Hash for Armory unique ID + &s.createDate, + &s.name, + &s.desc, + &s.highestUsed, + &s.kdfParams, + make([]byte, 256), + &s.keyGenerator, + newUnusedSpace(1024, &s.recent), + &appendedEntries, + } + var written int64 + for _, data := range datas { + if s, ok := data.(io.WriterTo); ok { + written, e = s.WriteTo(w) + } else { + written, e = binaryWrite(w, binary.LittleEndian, data) + } + n += written + if e != nil { + return n, e + } + } + return n, nil +} + +// TODO: set this automatically. + +// MarkDirty is +func (s *Store) MarkDirty() { + s.mtx.Lock() + defer s.mtx.Unlock() + s.dirty = true +} + +// WriteIfDirty is +func (s *Store) WriteIfDirty() (e error) { + s.mtx.RLock() + if !s.dirty { + s.mtx.RUnlock() + return nil + } + // TempFile creates the file 0600, so no need to chmod it. + fi, e := ioutil.TempFile(s.dir, s.file) + if e != nil { + s.mtx.RUnlock() + return e + } + fiPath := fi.Name() + _, e = s.writeTo(fi) + if e != nil { + s.mtx.RUnlock() + if e = fi.Close(); E.Chk(e) { + } + return e + } + e = fi.Sync() + if e != nil { + s.mtx.RUnlock() + if e = fi.Close(); E.Chk(e) { + } + return e + } + if e = fi.Close(); E.Chk(e) { + } + e = rename.Atomic(fiPath, s.path) + s.mtx.RUnlock() + if e == nil { + s.mtx.Lock() + s.dirty = false + s.mtx.Unlock() + } + return e +} + +// OpenDir opens a new key store from the specified directory. +// +// If the file does not exist, the error from the os package will be returned, and can be checked with os.IsNotExist to +// differentiate missing file errors from others (including deserialization). +func OpenDir(dir string) (*Store, error) { + path := filepath.Join(dir, Filename) + fi, e := os.OpenFile(path, os.O_RDONLY, 0) + if e != nil { + return nil, e + } + defer func() { + if e = fi.Close(); E.Chk(e) { + } + }() + store := new(Store) + _, e = store.ReadFrom(fi) + if e != nil { + return nil, e + } + store.path = path + store.dir = dir + store.file = Filename + return store, nil +} + +// Unlock derives an AES key from passphrase and key store's KDF parameters and unlocks the root key of the key store. +// If the unlock was successful, the key store's secret key is saved, allowing the decryption of any encrypted private +// key. Any addresses created while the key store was locked without private keys are created at this time. +func (s *Store) Unlock(passphrase []byte) (e error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { + return ErrWatchingOnly + } + // Derive key from KDF parameters and passphrase. + key := kdf(passphrase, &s.kdfParams) + // Unlock root address with derived key. + if _, e = s.keyGenerator.unlock(key); E.Chk(e) { + return e + } + // If unlock was successful, save the passphrase and aes key. + s.passphrase = passphrase + s.secret = key + return s.createMissingPrivateKeys() +} + +// Lock performs a best try effort to remove and zero all secret keys associated with the key store. +func (s *Store) Lock() (e error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { + return ErrWatchingOnly + } + // Remove clear text passphrase from key store. + if s.isLocked() { + e = ErrLocked + } else { + zero(s.passphrase) + s.passphrase = nil + zero(s.secret) + s.secret = nil + } + // Remove clear text private keys from all address entries. + for _, addr := range s.addrMap { + if baddr, ok := addr.(*btcAddress); ok { + _ = baddr.lock() + } + } + return e +} + +// ChangePassphrase creates a new AES key from a new passphrase and re-encrypts all encrypted private keys with the new +// key. +func (s *Store) ChangePassphrase(new []byte) (e error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { + return ErrWatchingOnly + } + if s.isLocked() { + return ErrLocked + } + oldkey := s.secret + newkey := kdf(new, &s.kdfParams) + for _, wa := range s.addrMap { + // Only btcAddresses curently have private keys. + a, ok := wa.(*btcAddress) + if !ok { + continue + } + if e := a.changeEncryptionKey(oldkey, newkey); E.Chk(e) { + return e + } + } + // zero old secrets. + zero(s.passphrase) + zero(s.secret) + // Save new secrets. + s.passphrase = new + s.secret = newkey + return nil +} +func zero(b []byte) { + for i := range b { + b[i] = 0 + } +} + +// IsLocked returns whether a key store is unlocked (in which case the key is saved in memory), or locked. +func (s *Store) IsLocked() bool { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.isLocked() +} +func (s *Store) isLocked() bool { + return len(s.secret) != 32 +} + +// NextChainedAddress attempts to get the next chained address. If the key store is unlocked, the next pubkey and +// private key of the address chain are derived. +// +// If the key store is locke, only the next pubkey is derived, and the private key will be generated on next unlock. +func (s *Store) NextChainedAddress(bs *BlockStamp) (btcaddr.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + return s.nextChainedAddress(bs) +} +func (s *Store) nextChainedAddress(bs *BlockStamp) (btcaddr.Address, error) { + addr, e := s.nextChainedBtcAddress(bs) + if e != nil { + return nil, e + } + return addr.Address(), nil +} + +// ChangeAddress returns the next chained address from the key store, marking the address for a change transaction +// output. +func (s *Store) ChangeAddress(bs *BlockStamp) (btcaddr.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + addr, e := s.nextChainedBtcAddress(bs) + if e != nil { + return nil, e + } + addr.flags.change = true + // Create and return payment address for address hash. + return addr.Address(), nil +} +func (s *Store) nextChainedBtcAddress(bs *BlockStamp) (*btcAddress, error) { + // Attempt to get address hash of next chained address. + nextAPKH, ok := s.chainIdxMap[s.highestUsed+1] + if !ok { + if s.isLocked() { + // Chain pubkeys. + if e := s.extendLocked(bs); E.Chk(e) { + return nil, e + } + } else { + // Chain private and pubkeys. + if e := s.extendUnlocked(bs); E.Chk(e) { + return nil, e + } + } + // Should be added to the internal maps, try lookup again. + nextAPKH, ok = s.chainIdxMap[s.highestUsed+1] + if !ok { + return nil, errors.New("chain index map inproperly updated") + } + } + // Look up address. + addr, ok := s.addrMap[getAddressKey(nextAPKH)] + if !ok { + return nil, errors.New("cannot find generated address") + } + btcAddr, ok := addr.(*btcAddress) + if !ok { + return nil, errors.New("found non-pubkey chained address") + } + s.highestUsed++ + return btcAddr, nil +} + +// LastChainedAddress returns the most recently requested chained address from calling NextChainedAddress, or the root +// address if no chained addresses have been requested. +func (s *Store) LastChainedAddress() btcaddr.Address { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.chainIdxMap[s.highestUsed] +} + +// extendUnlocked grows address chain for an unlocked keystore. +func (s *Store) extendUnlocked(bs *BlockStamp) (e error) { + // Get last chained address. New chained addresses will be chained off of this address's chaincode and private key. + a := s.chainIdxMap[s.lastChainIdx] + waddr, ok := s.addrMap[getAddressKey(a)] + if !ok { + return errors.New("expected last chained address not found") + } + if s.isLocked() { + return ErrLocked + } + lastAddr, ok := waddr.(*btcAddress) + if !ok { + return errors.New("found non-pubkey chained address") + } + privkey, e := lastAddr.unlock(s.secret) + if e != nil { + return e + } + cc := lastAddr.chaincode[:] + privkey, e = chainedPrivKey(privkey, lastAddr.pubKeyBytes(), cc) + if e != nil { + return e + } + newAddr, e := newBtcAddress(s, privkey, nil, bs, true) + if e != nil { + return e + } + if e = newAddr.verifyKeypairs(); E.Chk(e) { + return e + } + if e = newAddr.encrypt(s.secret); E.Chk(e) { + return e + } + a = newAddr.Address() + s.addrMap[getAddressKey(a)] = newAddr + newAddr.chainIndex = lastAddr.chainIndex + 1 + s.chainIdxMap[newAddr.chainIndex] = a + s.lastChainIdx++ + copy(newAddr.chaincode[:], cc) + return nil +} + +// extendLocked creates one new address without a private key (allowing for extending the address chain from a locked +// key store) chained from the last used chained address and adds the address to the key store's internal bookkeeping +// structures. +func (s *Store) extendLocked(bs *BlockStamp) (e error) { + a := s.chainIdxMap[s.lastChainIdx] + waddr, ok := s.addrMap[getAddressKey(a)] + if !ok { + return errors.New("expected last chained address not found") + } + addr, ok := waddr.(*btcAddress) + if !ok { + return errors.New("found non-pubkey chained address") + } + cc := addr.chaincode[:] + nextPubkey, e := chainedPubKey(addr.pubKeyBytes(), cc) + if e != nil { + return e + } + newaddr, e := newBtcAddressWithoutPrivkey(s, nextPubkey, nil, bs) + if e != nil { + return e + } + a = newaddr.Address() + s.addrMap[getAddressKey(a)] = newaddr + newaddr.chainIndex = addr.chainIndex + 1 + s.chainIdxMap[newaddr.chainIndex] = a + s.lastChainIdx++ + copy(newaddr.chaincode[:], cc) + if s.missingKeysStart == rootKeyChainIdx { + s.missingKeysStart = newaddr.chainIndex + } + return nil +} +func (s *Store) createMissingPrivateKeys() (e error) { + idx := s.missingKeysStart + if idx == rootKeyChainIdx { + return nil + } + // Lookup previous address. + apkh, ok := s.chainIdxMap[idx-1] + if !ok { + return errors.New("missing previous chained address") + } + prevWAddr := s.addrMap[getAddressKey(apkh)] + if s.isLocked() { + return ErrLocked + } + prevAddr, ok := prevWAddr.(*btcAddress) + if !ok { + return errors.New("found non-pubkey chained address") + } + prevPrivKey, e := prevAddr.unlock(s.secret) + if e != nil { + return e + } + for i := idx; ; i++ { + // Get the next private key for the ith address in the address chain. + ithPrivKey, e := chainedPrivKey( + prevPrivKey, + prevAddr.pubKeyBytes(), prevAddr.chaincode[:], + ) + if e != nil { + return e + } + // Get the address with the missing private key, set, and encrypt. + apkh, ok := s.chainIdxMap[i] + if !ok { + // Finished. + break + } + waddr := s.addrMap[getAddressKey(apkh)] + addr, ok := waddr.(*btcAddress) + if !ok { + return errors.New("found non-pubkey chained address") + } + addr.privKeyCT = ithPrivKey + if e := addr.encrypt(s.secret); E.Chk(e) { + // Avoid bug: see comment for VersUnsetNeedsPrivkeyFlag. + if e != ErrAlreadyEncrypted || s.vers.LT(VersUnsetNeedsPrivkeyFlag) { + return e + } + } + addr.flags.createPrivKeyNextUnlock = false + // Set previous address and private key for next iteration. + prevAddr = addr + prevPrivKey = ithPrivKey + } + s.missingKeysStart = rootKeyChainIdx + return nil +} + +// Address returns an walletAddress structure for an address in a key store. This address may be typecast into other +// interfaces (like PubKeyAddress and ScriptAddress) if specific information e.g. keys is required. +func (s *Store) Address(a btcaddr.Address) (WalletAddress, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + // Look up address by address hash. + btcaddr, ok := s.addrMap[getAddressKey(a)] + if !ok { + return nil, ErrAddressNotFound + } + return btcaddr, nil +} + +// Net returns the bitcoin network parameters for this key store. +func (s *Store) Net() *chaincfg.Params { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.netParams() +} +func (s *Store) netParams() *chaincfg.Params { + return (*chaincfg.Params)(s.net) +} + +// SetSyncStatus sets the sync status for a single key store address. This may error if the address is not found in the +// key store. +// +// When marking an address as unsynced, only the type Unsynced matters. The value is ignored. +func (s *Store) SetSyncStatus(a btcaddr.Address, ss SyncStatus) (e error) { + s.mtx.Lock() + defer s.mtx.Unlock() + wa, ok := s.addrMap[getAddressKey(a)] + if !ok { + return ErrAddressNotFound + } + wa.setSyncStatus(ss) + return nil +} + +// SetSyncedWith marks already synced addresses in the key store to be in sync with the recently-seen block described by +// the blockstamp. +// +// Unsynced addresses are unaffected by this method and must be marked as in sync with MarkAddressSynced or +// MarkAllSynced to be considered in sync with bs. +// +// If bs is nil, the entire key store is marked unsynced. +func (s *Store) SetSyncedWith(bs *BlockStamp) { + s.mtx.Lock() + defer s.mtx.Unlock() + if bs == nil { + s.recent.hashes = s.recent.hashes[:0] + s.recent.lastHeight = s.keyGenerator.firstBlock + s.keyGenerator.setSyncStatus(Unsynced(s.keyGenerator.firstBlock)) + return + } + // Chk if we're trying to rollback the last seen history. + // + // If so, and this bs is already saved, remove anything after and return. Otherwise, remove previous hashes. + if bs.Height < s.recent.lastHeight { + maybeIdx := len(s.recent.hashes) - 1 - int(s.recent.lastHeight-bs.Height) + if maybeIdx >= 0 && maybeIdx < len(s.recent.hashes) && + *s.recent.hashes[maybeIdx] == *bs.Hash { + s.recent.lastHeight = bs.Height + // subslice out the removed hashes. + s.recent.hashes = s.recent.hashes[:maybeIdx] + return + } + s.recent.hashes = nil + } + if bs.Height != s.recent.lastHeight+1 { + s.recent.hashes = nil + } + s.recent.lastHeight = bs.Height + if len(s.recent.hashes) == 20 { + // Make room for the most recent hash. + copy(s.recent.hashes, s.recent.hashes[1:]) + // Set new block in the last position. + s.recent.hashes[19] = bs.Hash + } else { + s.recent.hashes = append(s.recent.hashes, bs.Hash) + } +} + +// SyncedTo returns details about the block that a wallet is marked at least synced through. The height is the height +// that rescans should start at when syncing a wallet back to the best chain. +// +// NOTE: If the hash of the synced block is not known, hash will be nil, and must be obtained from elsewhere. This must +// be explicitly checked before dereferencing the pointer. +func (s *Store) SyncedTo() (hash *chainhash.Hash, height int32) { + s.mtx.RLock() + defer s.mtx.RUnlock() + switch h, ok := s.keyGenerator.SyncStatus().(PartialSync); { + case ok && int32(h) > s.recent.lastHeight: + height = int32(h) + default: + height = s.recent.lastHeight + if n := len(s.recent.hashes); n != 0 { + hash = s.recent.hashes[n-1] + } + } + for _, a := range s.addrMap { + var syncHeight int32 + switch e := a.SyncStatus().(type) { + case Unsynced: + syncHeight = int32(e) + case PartialSync: + syncHeight = int32(e) + case FullSync: + continue + } + if syncHeight < height { + height = syncHeight + hash = nil + // Can't go lower than 0. + if height == 0 { + return + } + } + } + return +} + +// NewIterateRecentBlocks returns an iterator for recently-seen blocks. The iterator starts at the most recently-added +// block, and Prev should be used to access earlier blocks. +func (s *Store) NewIterateRecentBlocks() *BlockIterator { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.recent.iter(s) +} + +// ImportPrivateKey imports a WIF private key into the keystore. The imported address is created using either a +// compressed or uncompressed serialized public key, depending on the CompressPubKey bool of the WIF. +func (s *Store) ImportPrivateKey(wif *util.WIF, bs *BlockStamp) (btcaddr.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { + return nil, ErrWatchingOnly + } + // First, must check that the key being imported will not result in a duplicate address. + pkh := btcaddr.Hash160(wif.SerializePubKey()) + if _, ok := s.addrMap[addressKey(pkh)]; ok { + return nil, ErrDuplicate + } + // The key store must be unlocked to encrypt the imported private key. + if s.isLocked() { + return nil, ErrLocked + } + // Create new address with this private key. + privKey := wif.PrivKey.Serialize() + btcaddr, e := newBtcAddress(s, privKey, nil, bs, wif.CompressPubKey) + if e != nil { + return nil, e + } + btcaddr.chainIndex = importedKeyChainIdx + // Mark as unsynced if import height is below currently-synced height. + if len(s.recent.hashes) != 0 && bs.Height < s.recent.lastHeight { + btcaddr.flags.unsynced = true + } + // Encrypt imported address with the derived AES key. + if e = btcaddr.encrypt(s.secret); E.Chk(e) { + return nil, e + } + addr := btcaddr.Address() + // Add address to key store's bookkeeping structures. Adding to the map will result in the imported address being + // serialized on the next WriteTo call. + s.addrMap[getAddressKey(addr)] = btcaddr + s.importedAddrs = append(s.importedAddrs, btcaddr) + // Create and return address. + return addr, nil +} + +// ImportScript creates a new scriptAddress with a user-provided script and adds it to the key store. +func (s *Store) ImportScript(script []byte, bs *BlockStamp) (btcaddr.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + if s.flags.watchingOnly { + return nil, ErrWatchingOnly + } + if _, ok := s.addrMap[addressKey(btcaddr.Hash160(script))]; ok { + return nil, ErrDuplicate + } + // Create new address with this private key. + scriptaddr, e := newScriptAddress(s, script, bs) + if e != nil { + return nil, e + } + // Mark as unsynced if import height is below currently-synced height. + if len(s.recent.hashes) != 0 && bs.Height < s.recent.lastHeight { + scriptaddr.flags.unsynced = true + } + // Add address to key store's bookkeeping structures. Adding to the map will result in the imported address being + // serialized on the next WriteTo call. + addr := scriptaddr.Address() + s.addrMap[getAddressKey(addr)] = scriptaddr + s.importedAddrs = append(s.importedAddrs, scriptaddr) + // Create and return address. + return addr, nil +} + +// CreateDate returns the Unix time of the key store creation time. This is used to compare the key store creation time +// against block headers and set a better minimum block height of where to being rescans. +func (s *Store) CreateDate() int64 { + s.mtx.RLock() + defer s.mtx.RUnlock() + return s.createDate +} + +// ExportWatchingWallet creates and returns a new key store with the same addresses in w, but as a watching-only key +// store without any private keys. +// +// New addresses created by the watching key store will match the new addresses created the original key store (thanks +// to public key address chaining), but will be missing the associated private keys. +func (s *Store) ExportWatchingWallet() (*Store, error) { + s.mtx.RLock() + defer s.mtx.RUnlock() + // Don't continue if key store is already watching-only. + if s.flags.watchingOnly { + return nil, ErrWatchingOnly + } + // Copy members of w into a new key store, but mark as watching-only and do not include any private keys. + ws := &Store{ + vers: s.vers, + net: s.net, + flags: walletFlags{ + useEncryption: false, + watchingOnly: true, + }, + name: s.name, + desc: s.desc, + createDate: s.createDate, + highestUsed: s.highestUsed, + recent: recentBlocks{ + lastHeight: s.recent.lastHeight, + }, + addrMap: make(map[addressKey]walletAddress), + // todo oga make me a list + chainIdxMap: make(map[int64]btcaddr.Address), + lastChainIdx: s.lastChainIdx, + } + kgwc := s.keyGenerator.watchingCopy(ws) + ws.keyGenerator = *(kgwc.(*btcAddress)) + if len(s.recent.hashes) != 0 { + ws.recent.hashes = make([]*chainhash.Hash, 0, len(s.recent.hashes)) + for _, hash := range s.recent.hashes { + hashCpy := *hash + ws.recent.hashes = append(ws.recent.hashes, &hashCpy) + } + } + for apkh, addr := range s.addrMap { + if !addr.Imported() { + // Must be a btcAddress if !imported. + btcAddr := addr.(*btcAddress) + ws.chainIdxMap[btcAddr.chainIndex] = + addr.Address() + } + apkhCopy := apkh + ws.addrMap[apkhCopy] = addr.watchingCopy(ws) + } + if len(s.importedAddrs) != 0 { + ws.importedAddrs = make( + []walletAddress, 0, + len(s.importedAddrs), + ) + for _, addr := range s.importedAddrs { + ws.importedAddrs = append(ws.importedAddrs, addr.watchingCopy(ws)) + } + } + return ws, nil +} + +// SyncStatus is the interface type for all sync variants. +type SyncStatus interface { + ImplementsSyncStatus() +} +type ( + // Unsynced is a type representing an unsynced address. When this is returned by a key store method, the value is + // the recorded first seen block height. + Unsynced int32 + // PartialSync is a type representing a partially synced address (for example, due to the result of a + // partially-completed rescan). + PartialSync int32 + // FullSync is a type representing an address that is in sync with the recently seen blocks. + FullSync struct{} +) + +// ImplementsSyncStatus is implemented to make Unsynced a SyncStatus. +func (u Unsynced) ImplementsSyncStatus() { +} + +// ImplementsSyncStatus is implemented to make PartialSync a SyncStatus. +func (p PartialSync) ImplementsSyncStatus() { +} + +// ImplementsSyncStatus is implemented to make FullSync a SyncStatus. +func (f FullSync) ImplementsSyncStatus() { +} + +// WalletAddress is an interface that provides acces to information regarding an address managed by a key store. +// Concrete implementations of this type may provide further fields to provide information specific to that type of +// address. +type WalletAddress interface { + // Address returns a util.Address for the backing address. + Address() btcaddr.Address + // AddrHash returns the key or script hash related to the address + AddrHash() string + // FirstBlock returns the first block an address could be in. + FirstBlock() int32 + // Imported returns true if the backing address was imported instead + // of being part of an address chain. + Imported() bool + // Change returns true if the backing address was created for a change output + // of a transaction. + Change() bool + // Compressed returns true if the backing address is compressed. + Compressed() bool + // SyncStatus returns the current synced state of an address. + SyncStatus() SyncStatus +} + +// SortedActiveAddresses returns all key store addresses that have been requested to be generated. These do not include +// unused addresses in the key pool. Use this when ordered addresses are needed. Otherwise, ActiveAddresses is +// preferred. +func (s *Store) SortedActiveAddresses() []WalletAddress { + s.mtx.RLock() + defer s.mtx.RUnlock() + addrs := make( + []WalletAddress, 0, + s.highestUsed+int64(len(s.importedAddrs))+1, + ) + for i := int64(rootKeyChainIdx); i <= s.highestUsed; i++ { + a := s.chainIdxMap[i] + info, ok := s.addrMap[getAddressKey(a)] + if ok { + addrs = append(addrs, info) + } + } + for _, addr := range s.importedAddrs { + addrs = append(addrs, addr) + } + return addrs +} + +// ActiveAddresses returns a map between active payment addresses and their full info. These do not include unused +// addresses in the key pool. If addresses must be sorted, use SortedActiveAddresses. +func (s *Store) ActiveAddresses() map[btcaddr.Address]WalletAddress { + s.mtx.RLock() + defer s.mtx.RUnlock() + addrs := make(map[btcaddr.Address]WalletAddress) + for i := int64(rootKeyChainIdx); i <= s.highestUsed; i++ { + a := s.chainIdxMap[i] + addr := s.addrMap[getAddressKey(a)] + addrs[addr.Address()] = addr + } + for _, addr := range s.importedAddrs { + addrs[addr.Address()] = addr + } + return addrs +} + +// ExtendActiveAddresses gets or creates the next n addresses from the address chain and marks each as active. This is +// used to recover deterministic (not imported) addresses from a key store backup, or to keep the active addresses in +// sync between an encrypted key store with private keys and an exported watching key store without. +// +// A slice is returned with the util.Address of each new address. The blockchain must be rescanned for these addresses. +func (s *Store) ExtendActiveAddresses(n int) ([]btcaddr.Address, error) { + s.mtx.Lock() + defer s.mtx.Unlock() + last := s.addrMap[getAddressKey(s.chainIdxMap[s.highestUsed])] + bs := &BlockStamp{Height: last.FirstBlock()} + addrs := make([]btcaddr.Address, n) + for i := 0; i < n; i++ { + addr, e := s.nextChainedAddress(bs) + if e != nil { + return nil, e + } + addrs[i] = addr + } + return addrs, nil +} + +type walletFlags struct { + useEncryption bool + watchingOnly bool +} + +func (wf *walletFlags) ReadFrom(r io.Reader) (int64, error) { + var b [8]byte + n, e := io.ReadFull(r, b[:]) + if e != nil { + return int64(n), e + } + wf.useEncryption = b[0]&(1<<0) != 0 + wf.watchingOnly = b[0]&(1<<1) != 0 + return int64(n), nil +} +func (wf *walletFlags) WriteTo(w io.Writer) (int64, error) { + var b [8]byte + if wf.useEncryption { + b[0] |= 1 << 0 + } + if wf.watchingOnly { + b[0] |= 1 << 1 + } + n, e := w.Write(b[:]) + return int64(n), e +} + +type addrFlags struct { + hasPrivKey bool + hasPubKey bool + encrypted bool + createPrivKeyNextUnlock bool + compressed bool + change bool + unsynced bool + partialSync bool +} + +func (af *addrFlags) ReadFrom(r io.Reader) (int64, error) { + var b [8]byte + n, e := io.ReadFull(r, b[:]) + if e != nil { + return int64(n), e + } + af.hasPrivKey = b[0]&(1<<0) != 0 + af.hasPubKey = b[0]&(1<<1) != 0 + af.encrypted = b[0]&(1<<2) != 0 + af.createPrivKeyNextUnlock = b[0]&(1<<3) != 0 + af.compressed = b[0]&(1<<4) != 0 + af.change = b[0]&(1<<5) != 0 + af.unsynced = b[0]&(1<<6) != 0 + af.partialSync = b[0]&(1<<7) != 0 + // Currently (at least until watching-only key stores are implemented) btcwallet shall refuse to open any + // unencrypted addresses. This check only makes sense if there is a private key to encrypt, which there may not be + // if the keypool was extended from just the last public key and no private keys were written. + if af.hasPrivKey && !af.encrypted { + return int64(n), errors.New("private key is unencrypted") + } + return int64(n), nil +} +func (af *addrFlags) WriteTo(w io.Writer) (int64, error) { + var b [8]byte + if af.hasPrivKey { + b[0] |= 1 << 0 + } + if af.hasPubKey { + b[0] |= 1 << 1 + } + if af.hasPrivKey && !af.encrypted { + // We only support encrypted privkeys. + return 0, errors.New("address must be encrypted") + } + if af.encrypted { + b[0] |= 1 << 2 + } + if af.createPrivKeyNextUnlock { + b[0] |= 1 << 3 + } + if af.compressed { + b[0] |= 1 << 4 + } + if af.change { + b[0] |= 1 << 5 + } + if af.unsynced { + b[0] |= 1 << 6 + } + if af.partialSync { + b[0] |= 1 << 7 + } + n, e := w.Write(b[:]) + return int64(n), e +} + +// recentBlocks holds at most the last 20 seen block hashes as well as the block height of the most recently seen block. +type recentBlocks struct { + hashes []*chainhash.Hash + lastHeight int32 +} + +func (rb *recentBlocks) readFromVersion(v ksVersion, r io.Reader) (int64, error) { + if !v.LT(Vers20LastBlocks) { + // Use current ksVersion. + return rb.ReadFrom(r) + } + // Old file versions only saved the most recently seen block height and hash, not the last 20. + var read int64 + // Read height. + var heightBytes [4]byte // 4 bytes for a int32 + n, e := io.ReadFull(r, heightBytes[:]) + read += int64(n) + if e != nil { + return read, e + } + rb.lastHeight = int32(binary.LittleEndian.Uint32(heightBytes[:])) + // If height is -1, the last synced block is unknown, so don't try to read a block hash. + if rb.lastHeight == -1 { + rb.hashes = nil + return read, nil + } + // Read block hash. + var syncedBlockHash chainhash.Hash + n, e = io.ReadFull(r, syncedBlockHash[:]) + read += int64(n) + if e != nil { + return read, e + } + rb.hashes = []*chainhash.Hash{ + &syncedBlockHash, + } + return read, nil +} +func (rb *recentBlocks) ReadFrom(r io.Reader) (int64, error) { + var read int64 + // Read number of saved blocks. This should not exceed 20. + var nBlockBytes [4]byte // 4 bytes for a uint32 + n, e := io.ReadFull(r, nBlockBytes[:]) + read += int64(n) + if e != nil { + return read, e + } + nBlocks := binary.LittleEndian.Uint32(nBlockBytes[:]) + if nBlocks > 20 { + return read, errors.New("number of last seen blocks exceeds maximum of 20") + } + // Read most recently seen block height. + var heightBytes [4]byte // 4 bytes for a int32 + n, e = io.ReadFull(r, heightBytes[:]) + read += int64(n) + if e != nil { + return read, e + } + height := int32(binary.LittleEndian.Uint32(heightBytes[:])) + // height should not be -1 (or any other negative number) since at this point we should be reading in at least one + // known block. + if height < 0 { + return read, errors.New("expected a block but specified height is negative") + } + // Set last seen height. + rb.lastHeight = height + // Read nBlocks block hashes. Merkles are expected to be in order of oldest to newest, but there's no way to check + // that here. + rb.hashes = make([]*chainhash.Hash, 0, nBlocks) + for i := uint32(0); i < nBlocks; i++ { + var blockHash chainhash.Hash + n, e := io.ReadFull(r, blockHash[:]) + read += int64(n) + if e != nil { + return read, e + } + rb.hashes = append(rb.hashes, &blockHash) + } + return read, nil +} +func (rb *recentBlocks) WriteTo(w io.Writer) (int64, error) { + var written int64 + // Write number of saved blocks. This should not exceed 20. + nBlocks := uint32(len(rb.hashes)) + if nBlocks > 20 { + return written, errors.New("number of last seen blocks exceeds maximum of 20") + } + if nBlocks != 0 && rb.lastHeight < 0 { + return written, errors.New("number of block hashes is positive, but height is negative") + } + var nBlockBytes [4]byte // 4 bytes for a uint32 + binary.LittleEndian.PutUint32(nBlockBytes[:], nBlocks) + n, e := w.Write(nBlockBytes[:]) + written += int64(n) + if e != nil { + return written, e + } + // Write most recently seen block height. + var heightBytes [4]byte // 4 bytes for a int32 + binary.LittleEndian.PutUint32(heightBytes[:], uint32(rb.lastHeight)) + n, e = w.Write(heightBytes[:]) + written += int64(n) + if e != nil { + return written, e + } + // Write block hashes. + for _, hash := range rb.hashes { + n, e := w.Write(hash[:]) + written += int64(n) + if e != nil { + return written, e + } + } + return written, nil +} + +// BlockIterator allows for the forwards and backwards iteration of recently seen blocks. +type BlockIterator struct { + storeMtx *sync.RWMutex + height int32 + index int + rb *recentBlocks +} + +func (rb *recentBlocks) iter(s *Store) *BlockIterator { + if rb.lastHeight == -1 || len(rb.hashes) == 0 { + return nil + } + return &BlockIterator{ + storeMtx: &s.mtx, + height: rb.lastHeight, + index: len(rb.hashes) - 1, + rb: rb, + } +} + +// Next is +func (it *BlockIterator) Next() bool { + it.storeMtx.RLock() + defer it.storeMtx.RUnlock() + if it.index+1 >= len(it.rb.hashes) { + return false + } + it.index++ + return true +} + +// Prev is +func (it *BlockIterator) Prev() bool { + it.storeMtx.RLock() + defer it.storeMtx.RUnlock() + if it.index-1 < 0 { + return false + } + it.index-- + return true +} + +// BlockStamp is +func (it *BlockIterator) BlockStamp() BlockStamp { + it.storeMtx.RLock() + defer it.storeMtx.RUnlock() + return BlockStamp{ + Height: it.rb.lastHeight - int32(len(it.rb.hashes)-1-it.index), + Hash: it.rb.hashes[it.index], + } +} + +// unusedSpace is a wrapper type to read or write one or more types that btcwallet fits into an unused space left by +// Armory's key store file format. +type unusedSpace struct { + nBytes int // number of unused bytes that armory left. + rfvs []readerFromVersion +} + +func newUnusedSpace(nBytes int, rfvs ...readerFromVersion) *unusedSpace { + return &unusedSpace{ + nBytes: nBytes, + rfvs: rfvs, + } +} +func (u *unusedSpace) readFromVersion(v ksVersion, r io.Reader) (int64, error) { + var read int64 + for _, rfv := range u.rfvs { + n, e := rfv.readFromVersion(v, r) + if e != nil { + return read + n, e + } + read += n + if read > int64(u.nBytes) { + return read, errors.New("read too much from armory's unused space") + } + } + // Read rest of actually unused bytes. + unused := make([]byte, u.nBytes-int(read)) + n, e := io.ReadFull(r, unused) + return read + int64(n), e +} +func (u *unusedSpace) WriteTo(w io.Writer) (int64, error) { + var written int64 + for _, wt := range u.rfvs { + n, e := wt.WriteTo(w) + if e != nil { + return written + n, e + } + written += n + if written > int64(u.nBytes) { + return written, errors.New("wrote too much to armory's unused space") + } + } + // Write rest of actually unused bytes. + unused := make([]byte, u.nBytes-int(written)) + n, e := w.Write(unused) + return written + int64(n), e +} + +// walletAddress is the internal interface used to abstracted around the different address types. +type walletAddress interface { + io.ReaderFrom + io.WriterTo + WalletAddress + watchingCopy(*Store) walletAddress + setSyncStatus(SyncStatus) +} +type btcAddress struct { + store *Store + address btcaddr.Address + flags addrFlags + chaincode [32]byte + chainIndex int64 + chainDepth int64 // unused + initVector [16]byte + privKey [32]byte + pubKey *ec.PublicKey + firstSeen int64 + lastSeen int64 + firstBlock int32 + partialSyncHeight int32 // This is reappropriated from armory's `lastBlock` field. + privKeyCT []byte // non-nil if unlocked. +} + +const ( + // Root address has a chain index of -1. Each subsequent chained address increments the index. + rootKeyChainIdx = -1 + // Imported private keys are not part of the chain, and have a special index of -2. + importedKeyChainIdx = -2 +) +const ( + pubkeyCompressed byte = 0x2 + pubkeyUncompressed byte = 0x4 +) + +type publicKey []byte + +func (k *publicKey) ReadFrom(r io.Reader) (n int64, e error) { + var read int64 + var format byte + read, e = binaryRead(r, binary.LittleEndian, &format) + if e != nil { + return n + read, e + } + n += read + // Remove the oddness from the format + noodd := format + noodd &= ^byte(0x1) + var s []byte + switch noodd { + case pubkeyUncompressed: + // Read the remaining 64 bytes. + s = make([]byte, 64) + case pubkeyCompressed: + // Read the remaining 32 bytes. + s = make([]byte, 32) + default: + return n, errors.New("unrecognized pubkey format") + } + read, e = binaryRead(r, binary.LittleEndian, &s) + if e != nil { + return n + read, e + } + n += read + *k = append([]byte{format}, s...) + return +} +func (k *publicKey) WriteTo(w io.Writer) (n int64, e error) { + return binaryWrite(w, binary.LittleEndian, []byte(*k)) +} + +// PubKeyAddress implements WalletAddress and additionally provides the pubkey for a pubkey-based address. +type PubKeyAddress interface { + WalletAddress + // PubKey returns the public key associated with the address. + PubKey() *ec.PublicKey + // ExportPubKey returns the public key associated with the address serialised as a hex encoded string. + ExportPubKey() string + // PrivKey returns the private key for the address. It can fail if the key store is watching only, the key store is + // locked, or the address doesn't have any keys. + PrivKey() (*ec.PrivateKey, error) + // ExportPrivKey exports the WIF private key. + ExportPrivKey() (*util.WIF, error) +} + +// newBtcAddress initializes and returns a new address. privkey must be 32 bytes. iv must be 16 bytes, or nil (in which +// case it is randomly generated). +func newBtcAddress(wallet *Store, privkey, iv []byte, bs *BlockStamp, compressed bool) (addr *btcAddress, e error) { + if len(privkey) != 32 { + return nil, errors.New("private key is not 32 bytes") + } + addr, e = newBtcAddressWithoutPrivkey( + wallet, + pubkeyFromPrivkey(privkey, compressed), iv, bs, + ) + if e != nil { + return nil, e + } + addr.flags.createPrivKeyNextUnlock = false + addr.flags.hasPrivKey = true + addr.privKeyCT = privkey + return addr, nil +} + +// newBtcAddressWithoutPrivkey initializes and returns a new address with an unknown (at the time) private key that must +// be found later. pubkey must be 33 or 65 bytes, and iv must be 16 bytes or empty (in which case it is randomly +// generated). +func newBtcAddressWithoutPrivkey(s *Store, pubkey, iv []byte, bs *BlockStamp) (addr *btcAddress, e error) { + var compressed bool + switch n := len(pubkey); n { + case ec.PubKeyBytesLenCompressed: + compressed = true + case ec.PubKeyBytesLenUncompressed: + compressed = false + default: + return nil, fmt.Errorf("invalid pubkey length %d", n) + } + if len(iv) == 0 { + iv = make([]byte, 16) + if _, e = rand.Read(iv); E.Chk(e) { + return nil, e + } + } else if len(iv) != 16 { + return nil, errors.New("init vector must be nil or 16 bytes large") + } + pk, e := ec.ParsePubKey(pubkey, ec.S256()) + if e != nil { + return nil, e + } + address, e := btcaddr.NewPubKeyHash(btcaddr.Hash160(pubkey), s.netParams()) + if e != nil { + return nil, e + } + addr = &btcAddress{ + flags: addrFlags{ + hasPrivKey: false, + hasPubKey: true, + encrypted: false, + createPrivKeyNextUnlock: true, + compressed: compressed, + change: false, + unsynced: false, + }, + store: s, + address: address, + firstSeen: time.Now().Unix(), + firstBlock: bs.Height, + pubKey: pk, + } + copy(addr.initVector[:], iv) + return addr, nil +} + +// newRootBtcAddress generates a new address, also setting the chaincode and chain index to represent this address as a +// root address. +func newRootBtcAddress( + s *Store, privKey, iv, chaincode []byte, + bs *BlockStamp, +) (addr *btcAddress, e error) { + if len(chaincode) != 32 { + return nil, errors.New("chaincode is not 32 bytes") + } + // Create new btcAddress with provided inputs. This will always use a compressed pubkey. + addr, e = newBtcAddress(s, privKey, iv, bs, true) + if e != nil { + return nil, e + } + copy(addr.chaincode[:], chaincode) + addr.chainIndex = rootKeyChainIdx + return addr, e +} + +// verifyKeypairs creates a signature using the parsed private key and verifies the signature with the parsed public +// key. If either of these steps fail, the keypair generation failed and any funds sent to this address will be +// unspendable. This step requires an unencrypted or unlocked btcAddress. +func (a *btcAddress) verifyKeypairs() (e error) { + if len(a.privKeyCT) != 32 { + return errors.New("private key unavailable") + } + privKey := &ec.PrivateKey{ + PublicKey: *a.pubKey.ToECDSA(), + D: new(big.Int).SetBytes(a.privKeyCT), + } + data := "String to sign." + sig, e := privKey.Sign([]byte(data)) + if e != nil { + return e + } + ok := sig.Verify([]byte(data), privKey.PubKey()) + if !ok { + return errors.New("pubkey verification failed") + } + return nil +} + +// ReadFrom reads an encrypted address from an io.Reader. +func (a *btcAddress) ReadFrom(r io.Reader) (n int64, e error) { + var read int64 + // Checksums + var chkPubKeyHash uint32 + var chkChaincode uint32 + var chkInitVector uint32 + var chkPrivKey uint32 + var chkPubKey uint32 + var pubKeyHash [ripemd160.Size]byte + var pubKey publicKey + // Read serialized key store into addr fields and checksums. + datas := []interface{}{ + &pubKeyHash, + &chkPubKeyHash, + make([]byte, 4), // ksVersion + &a.flags, + &a.chaincode, + &chkChaincode, + &a.chainIndex, + &a.chainDepth, + &a.initVector, + &chkInitVector, + &a.privKey, + &chkPrivKey, + &pubKey, + &chkPubKey, + &a.firstSeen, + &a.lastSeen, + &a.firstBlock, + &a.partialSyncHeight, + } + for _, data := range datas { + if rf, ok := data.(io.ReaderFrom); ok { + read, e = rf.ReadFrom(r) + } else { + read, e = binaryRead(r, binary.LittleEndian, data) + } + if e != nil { + return n + read, e + } + n += read + } + // Verify checksums, correct errors where possible. + checks := []struct { + data []byte + chk uint32 + }{ + {pubKeyHash[:], chkPubKeyHash}, + {a.chaincode[:], chkChaincode}, + {a.initVector[:], chkInitVector}, + {a.privKey[:], chkPrivKey}, + {pubKey, chkPubKey}, + } + for i := range checks { + if e = verifyAndFix(checks[i].data, checks[i].chk); E.Chk(e) { + return n, e + } + } + if !a.flags.hasPubKey { + return n, errors.New("read in an address without a public key") + } + pk, e := ec.ParsePubKey(pubKey, ec.S256()) + if e != nil { + return n, e + } + a.pubKey = pk + addr, e := btcaddr.NewPubKeyHash(pubKeyHash[:], a.store.netParams()) + if e != nil { + return n, e + } + a.address = addr + return n, nil +} +func (a *btcAddress) WriteTo(w io.Writer) (n int64, e error) { + var written int64 + pubKey := a.pubKeyBytes() + hash := a.address.ScriptAddress() + datas := []interface{}{ + &hash, + walletHash(hash), + make([]byte, 4), // ksVersion + &a.flags, + &a.chaincode, + walletHash(a.chaincode[:]), + &a.chainIndex, + &a.chainDepth, + &a.initVector, + walletHash(a.initVector[:]), + &a.privKey, + walletHash(a.privKey[:]), + &pubKey, + walletHash(pubKey), + &a.firstSeen, + &a.lastSeen, + &a.firstBlock, + &a.partialSyncHeight, + } + for _, data := range datas { + if wt, ok := data.(io.WriterTo); ok { + written, e = wt.WriteTo(w) + } else { + written, e = binaryWrite(w, binary.LittleEndian, data) + } + if e != nil { + return n + written, e + } + n += written + } + return n, nil +} + +// encrypt attempts to encrypt an address's clear text private key, failing if the address is already encrypted or if +// the private key is not 32 bytes. If successful, the encryption flag is set. +func (a *btcAddress) encrypt(key []byte) (e error) { + if a.flags.encrypted { + return ErrAlreadyEncrypted + } + if len(a.privKeyCT) != 32 { + return errors.New("invalid clear text private key") + } + aesBlockEncrypter, e := aes.NewCipher(key) + if e != nil { + return e + } + aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:]) + aesEncrypter.XORKeyStream(a.privKey[:], a.privKeyCT) + a.flags.hasPrivKey = true + a.flags.encrypted = true + return nil +} + +// lock removes the reference this address holds to its clear text private key. This function fails if the address is +// not encrypted. +func (a *btcAddress) lock() (e error) { + if !a.flags.encrypted { + return errors.New("unable to lock unencrypted address") + } + zero(a.privKeyCT) + a.privKeyCT = nil + return nil +} + +// unlock decrypts and stores a pointer to an address's private key, failing if the address is not encrypted, or the +// provided key is incorrect. +// +// The returned clear text private key will always be a copy that may be safely used by the caller without worrying +// about it being zeroed during an address lock. +func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, e error) { + if !a.flags.encrypted { + return nil, errors.New("unable to unlock unencrypted address") + } + // Decrypt private key with AES key. + aesBlockDecryptor, e := aes.NewCipher(key) + if e != nil { + return nil, e + } + aesDecryptor := cipher.NewCFBDecrypter(aesBlockDecryptor, a.initVector[:]) + privkey := make([]byte, 32) + aesDecryptor.XORKeyStream(privkey, a.privKey[:]) + // If secret is already saved, simply compare the bytes. + if len(a.privKeyCT) == 32 { + if !bytes.Equal(a.privKeyCT, privkey) { + return nil, ErrWrongPassphrase + } + privKeyCT := make([]byte, 32) + copy(privKeyCT, a.privKeyCT) + return privKeyCT, nil + } + x, y := ec.S256().ScalarBaseMult(privkey) + if x.Cmp(a.pubKey.X) != 0 || y.Cmp(a.pubKey.Y) != 0 { + return nil, ErrWrongPassphrase + } + privkeyCopy := make([]byte, 32) + copy(privkeyCopy, privkey) + a.privKeyCT = privkey + return privkeyCopy, nil +} + +// changeEncryptionKey re-encrypts the private keys for an address with a new AES encryption key. oldkey must be the old +// AES encryption key and is used to decrypt the private key. +func (a *btcAddress) changeEncryptionKey(oldkey, newkey []byte) (e error) { + // Address must have a private key and be encrypted to continue. + if !a.flags.hasPrivKey { + return errors.New("no private key") + } + if !a.flags.encrypted { + return errors.New("address is not encrypted") + } + privKeyCT, e := a.unlock(oldkey) + if e != nil { + return e + } + aesBlockEncrypter, e := aes.NewCipher(newkey) + if e != nil { + return e + } + newIV := make([]byte, len(a.initVector)) + if _, e = rand.Read(newIV); E.Chk(e) { + return e + } + copy(a.initVector[:], newIV) + aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:]) + aesEncrypter.XORKeyStream(a.privKey[:], privKeyCT) + return nil +} + +// Address returns the pub key address, implementing AddressInfo. +func (a *btcAddress) Address() btcaddr.Address { + return a.address +} + +// AddrHash returns the pub key hash, implementing WalletAddress. +func (a *btcAddress) AddrHash() string { + return string(a.address.ScriptAddress()) +} + +// FirstBlock returns the first block the address is seen in, implementing AddressInfo. +func (a *btcAddress) FirstBlock() int32 { + return a.firstBlock +} + +// Imported returns the pub if the address was imported, or a chained address, implementing AddressInfo. +func (a *btcAddress) Imported() bool { + return a.chainIndex == importedKeyChainIdx +} + +// Change returns true if the address was created as a change address, implementing AddressInfo. +func (a *btcAddress) Change() bool { + return a.flags.change +} + +// Compressed returns true if the address backing key is compressed, implementing AddressInfo. +func (a *btcAddress) Compressed() bool { + return a.flags.compressed +} + +// SyncStatus returns a SyncStatus type for how the address is currently synced. For an Unsynced type, the value is the +// recorded first seen block height of the address. +func (a *btcAddress) SyncStatus() SyncStatus { + switch { + case a.flags.unsynced && !a.flags.partialSync: + return Unsynced(a.firstBlock) + case a.flags.unsynced && a.flags.partialSync: + return PartialSync(a.partialSyncHeight) + default: + return FullSync{} + } +} + +// PubKey returns the hex encoded pubkey for the address. Implementing PubKeyAddress. +func (a *btcAddress) PubKey() *ec.PublicKey { + return a.pubKey +} +func (a *btcAddress) pubKeyBytes() []byte { + if a.Compressed() { + return a.pubKey.SerializeCompressed() + } + return a.pubKey.SerializeUncompressed() +} + +// ExportPubKey returns the public key associated with the address serialised as a hex encoded string. Implements +// PubKeyAddress +func (a *btcAddress) ExportPubKey() string { + return hex.EncodeToString(a.pubKeyBytes()) +} + +// PrivKey implements PubKeyAddress by returning the private key, or an error if the key store is locked, watching only +// or the private key is missing. +func (a *btcAddress) PrivKey() (*ec.PrivateKey, error) { + if a.store.flags.watchingOnly { + return nil, ErrWatchingOnly + } + if !a.flags.hasPrivKey { + return nil, errors.New("no private key for address") + } + // Key store must be unlocked to decrypt the private key. + if a.store.isLocked() { + return nil, ErrLocked + } + // Unlock address with key store secret. unlock returns a copy of the clear text private key, and may be used safely + // even during an address lock. + privKeyCT, e := a.unlock(a.store.secret) + if e != nil { + return nil, e + } + return &ec.PrivateKey{ + PublicKey: *a.pubKey.ToECDSA(), + D: new(big.Int).SetBytes(privKeyCT), + }, nil +} + +// ExportPrivKey exports the private key as a WIF for encoding as a string in the Wallet Import Format. +func (a *btcAddress) ExportPrivKey() (*util.WIF, error) { + pk, e := a.PrivKey() + if e != nil { + return nil, e + } + // NewWIF only errors if the network is nil. In this case, panic, as our program's assumptions are so broken that + // this needs to be caught immediately, and a stack trace here is more useful than elsewhere. + wif, e := util.NewWIF( + pk, a.store.netParams(), + a.Compressed(), + ) + if e != nil { + panic(e) + } + return wif, nil +} + +// watchingCopy creates a copy of an address without a private key. This is used to fill a watching a key store with +// addresses from a normal key store. +func (a *btcAddress) watchingCopy(s *Store) walletAddress { + return &btcAddress{ + store: s, + address: a.address, + flags: addrFlags{ + hasPrivKey: false, + hasPubKey: true, + encrypted: false, + createPrivKeyNextUnlock: false, + compressed: a.flags.compressed, + change: a.flags.change, + unsynced: a.flags.unsynced, + }, + chaincode: a.chaincode, + chainIndex: a.chainIndex, + chainDepth: a.chainDepth, + pubKey: a.pubKey, + firstSeen: a.firstSeen, + lastSeen: a.lastSeen, + firstBlock: a.firstBlock, + partialSyncHeight: a.partialSyncHeight, + } +} + +// setSyncStatus sets the address flags and possibly the partial sync height depending on the type of s. +func (a *btcAddress) setSyncStatus(s SyncStatus) { + switch e := s.(type) { + case Unsynced: + a.flags.unsynced = true + a.flags.partialSync = false + a.partialSyncHeight = 0 + case PartialSync: + a.flags.unsynced = true + a.flags.partialSync = true + a.partialSyncHeight = int32(e) + case FullSync: + a.flags.unsynced = false + a.flags.partialSync = false + a.partialSyncHeight = 0 + } +} + +// note that there is no encrypted bit here since if we had a script encrypted and then used it on the blockchain this +// provides a simple known plaintext in the key store file. +// +// It was determined that the script in a p2sh transaction is not a secret and any sane situation would also require a +// signature (which does have a secret). +type scriptFlags struct { + hasScript bool + change bool + unsynced bool + partialSync bool +} + +// ReadFrom implements the io.ReaderFrom interface by reading from r into sf. +func (sf *scriptFlags) ReadFrom(r io.Reader) (int64, error) { + var b [8]byte + n, e := io.ReadFull(r, b[:]) + if e != nil { + return int64(n), e + } + // We match bits from addrFlags for similar fields. hence hasScript uses the same bit as hasPubKey and the change + // bit is the same for both. + sf.hasScript = b[0]&(1<<1) != 0 + sf.change = b[0]&(1<<5) != 0 + sf.unsynced = b[0]&(1<<6) != 0 + sf.partialSync = b[0]&(1<<7) != 0 + return int64(n), nil +} + +// WriteTo implements the io.WriteTo interface by writing sf into w. +func (sf *scriptFlags) WriteTo(w io.Writer) (int64, error) { + var b [8]byte + if sf.hasScript { + b[0] |= 1 << 1 + } + if sf.change { + b[0] |= 1 << 5 + } + if sf.unsynced { + b[0] |= 1 << 6 + } + if sf.partialSync { + b[0] |= 1 << 7 + } + n, e := w.Write(b[:]) + return int64(n), e +} + +// p2SHScript represents the variable length script entry in a key store. +type p2SHScript []byte + +// ReadFrom implements the ReaderFrom interface by reading the P2SH script from r in the format <4 bytes little endian +// length>