diff --git a/.gitignore b/.gitignore index 542dd32..954e521 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ fatd* -.idea -coverage.out fat-cli* -todo -.vscode -*.sqlite3 \ No newline at end of file +*.sqlite3 diff --git a/CLI.md b/CLI.md deleted file mode 100644 index 1d3cbed..0000000 --- a/CLI.md +++ /dev/null @@ -1,359 +0,0 @@ -# FAT CLI Documentation - -The fat-cli allows users to explore and interact with FAT chains. - -fat-cli can be used to explore FAT chains to get balances, issuance, and -transaction data. It can also be used to send transactions on existing FAT -chains, and issue new FAT-0 or FAT-1 tokens. - -**Chain ID Settings** - -Most sub-commands need to be scoped to a specific FAT chain, identified by a -`--chainid`. Alternatively, this can be specified by using both the `--tokenid` -and `--identity`, which together determine the chain ID. - -**API Settings** - -fat-cli makes use of the fatd, factomd, and factom-walletd JSON-RPC 2.0 APIs -for various operations. Trust in these API endpoints is imperative to secure -operation. - -The `--fatd` API is used to explore issuance, transactions, and balances for -existing FAT chains. - -The `--factomd` API is used to submit entries directly to the Factom -blockchain, as well as for checking EC balances, chain existence, and identity -keys. - -The `--walletd` API is used to access private keys for FA and EC addresses. To -avoid use of factom-walletd, use private Fs or Es keys directly on the CLI -instead. - -If `--debug` is set, all fatd and factomd API calls will be printed to stdout. -API calls to factom-walletd are omitted to avoid leaking private key data. - -**Offline Mode** - -For increased security to protect private keys, it is possible to run fat-cli -such that it makes no network calls when generating Factom entries for FAT -transactions or token issuance. - -Use `--curl` to skip submitting the entry directly to Factom, and instead print -out the curl commands for committing and revealing the entry. These curl -commands contain the encoded signed data and may be safely copied to, and run -from, a computer with access to factomd. - -Use `--force` to skip all sanity checks that involve API calls out factomd or -fatd. As a result, this may result in generating a Factom Entry that is invalid -for Factom or FAT, but may still use up Entry Credits to submit. - -Use private keys for `--ecadr` and --input directly to avoid any network calls -to factom-walletd. - -**Entry Credits** -Making FAT transactions or issuing new FAT tokens requires creating entries on -the Factom blockchain. Creating Factom entries costs Entry Credits. Entry -Credits have a relatively fixed price of about $0.001 USD. Entry Credits can be -obtained by burning Factoids which can be done using the official factom-cli. -FAT transactions normally cost 1 EC. The full FAT Token Issuance process -normally costs 12 EC. - -### CLI Completion -After installing fat-cli in some permanent location in your PATH. Use ---installcompletion to install CLI completion for Bash, Zsh, or Fish. This -simply adds a single line to your `~/.bash_profile` (or shell equivalent), -which can be removed with --uninstallcompletion. You must re-open your shell -before completion changes take effect. - -No other programs or files need to be installed because fat-cli is also its own -completion program. If fat-cli is envoked by the completion system, it returns -completions for the currently typed arguments. - -If the `--fatd` endpoint is available, Token Chain IDs can be completed based -on the chains that fatd is tracking. - -If the `--walletd` endpoint is available, then all FA and EC addresses can be -completed based on the addresses saved by factom-walletd. - -Since both of these completion flags require successful API calls, any required -API related flags must already be supplied before completion for Token Chain -IDs, FA or EC addresses can succeed. Otherwise, if the default settings are -incorrect, generating completion suggestions will fail silently. Note that ---timeout is ignored as a very short timeout is always used to avoid noticeable -blocking when generating completion suggestions. - - -# Flags - -## General - -### `--debug` - -Print fatd and factomd API calls - -### `--verbose` - -Print verbose details about sanity check and other operations - -### `--help` - -Get help with using the CLI. Can follow any command or subcommand to get -detailed help. - - - -## Network & Auth - -### `--fatd` - -Fatd URL - -scheme://host:port for fatd (default `localhost:8078`) - -### `--fatdpass` - -Basic HTTP Auth Password for fatd - -### `--fatduser` - -Basic HTTP Auth User for fatd - -### `--factomd` - -Factomd URL - -scheme://host:port for factomd (default `localhost:8088`) - -### `--factomdpass` - -Basic HTTP Auth Password for factomd - -### `--factomduser` - -Basic HTTP Auth User for factomd - -### `--walletd` - -Factom Walletd URL - -scheme://host:port for factom-walletd (default `localhost:8089`) - -### `--walletduser` - -Basic HTTP Auth User for factom-walletd - -### `--walletdpass` - -Basic HTTP Auth Password for factom-walletd - -### `--timeout` - -Timeout for all API requests (i.e. `10s`, `1m`) (default `3s`) - - - -## Tokens - -### `--chainid` - -The 32 Byte Factom Chain ID of the token to get data for. The token chain ID -can be calculated from `--identity` & `--tokenid`. Either `--chainid` OR -`--identity` & `--tokenid` should be supplied - -### `--identity` - -Token Issuer Identity Chain ID of a FAT token. - -### `--tokenid` - -The Token ID string of a FAT chain. - - - -# Commands - -## `get` - -Retrieve data about FAT tokens or a specific FAT token - -``` - fat-cli get [subcommand] [flags] -``` - -### Subcommands - -#### `chains` - -Get information about tokens and token chains. Print a list including the Chain -ID, Issuer identity chain ID, and token ID of each token currently tracked by -the fat daemon. - -``` -fat-cli get chains -``` - -If the optional `` argument is supplied the info for that specific -chain will be returned including statistics. - - -#### `balance` - -Get the balance of a Factoid address for a token - -``` -fat-cli get balance --chainid -``` - -#### `transactions` - -Get transaction history and specific transactions belonging to a specific FAT -token. - - -``` -fat-cli get transactions --chainid [--starttx ] - [--page ] [--limit ] [--order <"asc" | "desc">] - [--address [--address ]... [--to] [--from]] - [--nftokenid ] -``` - -- `--address` - Add to the set of addresses to lookup txs for -- `--from` - Request only txs FROM the given `--address` set -- `--to` - Request only txs TO the given --address set -- `--limit` - Limit of returned txs (default `10`) -- `--nftokenid` - Request only txs involving this NF Token ID -- `--order` - Order of returned txs (`asc`|`desc`, default `asc`) -- `--page` - Page of returned txs (default `1`) -- `--starttx` - Entryhash of tx to start indexing from - - - -## `issue` - -Issue a new FAT-0 or FAT-1 token chain. - -Issuing a new FAT token chain is a two step process but only requires a single command. - -First, the Token Chain must be created with the correct Name IDs on the Factom -Blockchain. So both --tokenid and --identity are required and use of --chainid -is not allowed for this step. If the Chain Creation Entry has already been -submitted then this step is skipped over. - -Second, the Token Initialization Entry must be added to the Token Chain. The -Token Initialization Entry must be signed by the SK1 key corresponding to the -ID1 key declared in the --identity chain. Both --type and --supply are -required. The --supply must be positive or -1 for an unlimited supply of -tokens. - -Note that publishing a Token Initialization Entry is an immutable operation. -The protocol does not permit altering the Token Initialization Entry in any -way. - -Sanity Checks - Prior to composing the Chain Creation or Token Initialization Entry, a - number of calls to fatd and factomd are made to ensure that the token - can be issued. These checks are skipped if --force is used. - - - Skip Chain Creation Entry if already submitted. - - The token has not already been issued. - - The --identity chain exists. - - The --sk1 key corresponds to the --identity's id1 key. - - The --ecadr has enough ECs to pay for all entries. - -Identity Chain - FAT token chains may only be issued by an entity controlling the - sk1/id1 key established by the Identity Chain pointed to by the FAT - token chain. An Identity Chain and the associated keys can be created - using the factom-identity-cli. - - https://github.com/PaulBernier/factom-identity-cli - -Entry Credits - Creating entries on the Factom blockchain costs Entry Credits. The full - Token Issuance process normally costs 12 ECs. You must specify a funded - Entry Credit address with --ecadr, which may be either a private Es - address, or a pubilc EC address that can be fetched from - factom-walletd. - -**Usage** - -``` - fat-cli issue --ecadr --sk1 - --identity --tokenid - --type <"FAT-0" | "FAT-1"> --supply [--metadata ] [flags] -``` - -``` -Flags: - --curl Do not submit Factom entry; print curl commands - -e, --ecadr EC or Es address to pay for entries - --force Skip sanity checks for balances, chain status, and sk1 key - -h, --help help for issue - -m, --metadata JSON JSON metadata to include in tx - --sk1 sk1 Secret Identity Key 1 to sign entry - --supply int Max Token supply, use -1 for unlimited - --symbol string Optional abbreviated token symbol - --type <"FAT-0" | "FAT-1"> Token standard to use -``` - -**Example Commands** - -Initialize a FAT-0 token called "test" with a maximum supply of 100,000 units: - -``` -fat-cli issue --ecadr EC3cQ1QnsE5rKWR1B5mzVHdTkAReK5kJwaQn5meXzU9wANyk7Aej --sk1 sk1... --identity 888888a37cbf303c0bfc8d0cc7e77885c42000b757bd4d9e659de994477a0904 --tokenid test --type "FAT-0" --supply 100000 -``` - -Initialize a FAT-1 token called "test-nft" with an unlimited supply: -``` -fat-cli issue --ecadr EC3cQ1QnsE5rKWR1B5mzVHdTkAReK5kJwaQn5meXzU9wANyk7Aej --sk1 sk1... --identity 888888a37cbf303c0bfc8d0cc7e77885c42000b757bd4d9e659de994477a0904 --tokenid test-nft --type "FAT-1" --supply -1 -``` - -## `transact` - -Send or distribute FAT-0 or FAT-1 tokens. - -Submitting a FAT transaction involves submitting a signed transaction entry to -the given FAT Token Chain - -**Usage** - -``` -fat-cli transact --input --output [--metadata ] [--sk1 ] [--ecadr ] [--curl] [--force] -``` - -- `--input` - An input to the transaction. May be specified multiple times. For - example a FAT-0 tx input could look like -`FA3SjebEevRe964p4tQ6eieEvzi7puv9JWF3S3Wgw2v3WGKueL3R:150`, a FAT-1 tx input -could look like -`FA3SjebEevRe964p4tQ6eieEvzi7puv9JWF3S3Wgw2v3WGKueL3R:[1,2,5-100]`. It is -allowed to use private Factoid keys instead of public ones if `--walletd` is -not specified or available. -- `--output` - An output to the transaction. May be specified multiple times. - Follows the same form as `--input` except no private Factoid keys are -permitted. -- `--metadata` - JSON compliant metadata to attach to the transaction -- `--sk1` - The SK1 Private identity key belonging to the issuer of the token. - Required for coinbase transactions. -- `--ecadr` - EC or Es address to pay for the chain creation and token issuance - entries, if `--factomd` is specified. -- `--curl` - Do not submit the Factom entry; print curl commands instead! -- `--force` - Skip sanity checks for balances, chain status, and sk1 key - -**Example Commands** - -FAT-0 Coinbase Transaction - -Use `--sk1` and `--output` for coinbase transactions; no `--input` is specified as the coinbase input address is always `FA1zT4aFpEvcnPqPCigB3fvGu4Q4mTXY22iiuV69DqE1pNhdF2MC`, the public address that corresponds to a private key of all zeroes. This creates 100 new tokens and sends them to the the `FA2gCm...` address. - -``` -fat-cli transact fat0 --output FA2gCmih3PaSYRVMt1jLkdG4Xpo2koebUpQ6FpRRnqw5FfTSN2vW:100 --sk1 sk1... --ecadr EC3cQ1QnsE5rKWR1B5mzVHdTkAReK5kJwaQn5meXzU9wANyk7Aej -``` - -FAT-1 Transaction - -This moves the token with an id of 10 from `FA2gCm...` to `FA3j68...`. - -``` -fat-cli transact fat1 --input FA2gCmih3PaSYRVMt1jLkdG4Xpo2koebUpQ6FpRRnqw5FfTSN2vW:[10] --output FA3j68XNwKwvHXV2TKndxPpyCK3KrWTDyyfxzi8LwuM5XRuEmhy6:[10] --ecadr EC3cQ1QnsE5rKWR1B5mzVHdTkAReK5kJwaQn5meXzU9wANyk7Aej -``` \ No newline at end of file diff --git a/CODE_POLICY.md b/CODE_POLICY.md new file mode 100644 index 0000000..22bac9f --- /dev/null +++ b/CODE_POLICY.md @@ -0,0 +1,232 @@ +# CODE POLICY + +## Core Values + +The following policies are oriented to promote the following: +1. Correctness +2. Consistency +3. Simplicity +4. Performance + +Note that performance is dead last. We address performance issues only when +they arise, and never before they are actually a problem for someone. +Optimizing for performance generally means increasing complexity, which is a +trade off that must be weighed as part of design. + +It is much better to have a simple design that is easy for anyone to debug, +than a performant design that is difficult to debug. It is better to bear the +keystroke cost of clear code upfront, than bear the cost of increased time +spent debugging later. + +> DONT MAKE THINGS EASY TO DO, MAKE THEM EASY TO UNDERSTAND. +> +> -- [Bill Kenedy, Ardan Labs](https://twitter.com/goinggodotnet) + +## Recommended Reading + +- [SOLID Go Design by Dave Cheney](https://dave.cheney.net/2016/08/20/solid-go-design) +- [Data and Semantics by Bill Kenedy](https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html) +- [For Range Semantics by Bill Kenedy](https://www.ardanlabs.com/blog/2017/06/for-range-semantics.html) +- [Interface Values are Valueless by Bill Kenedy](https://www.ardanlabs.com/blog/2018/03/interface-values-are-valueless.html) +- [On Packaging by Bill Kenedy](https://www.ardanlabs.com/blog/2017/02/design-philosophy-on-packaging.html) +- [On Logging by Bill Kenedy](https://www.ardanlabs.com/blog/2017/05/design-philosophy-on-logging.html) +- [Ardan Labs Go Training repo](https://github.com/ardanlabs/gotraining) +- [Go and SQLite by David Crawshaw](https://crawshaw.io/blog/go-and-sqlite) + +## Git + +### Committing +Do the following before committing: +- Ensure the code builds using `make`. +- Always run `go mod tidy` before committing your code and include changes to + `go.mod` and `go.sum` to ensure reproducible builds. + +### Commit messages +Commit messages allow for developers to quickly and concisely review changes. +Bad commit messages force developers to look at the code and wonder why a +change was made. + +Commit messages should follow these conventions: +1. [Separate subject from body with a blank line](https://chris.beams.io/posts/git-commit/#separate) +2. [Limit the subject line to 50 characters (as best as possible)](https://chris.beams.io/posts/git-commit/#limit-50) +3. [Capitalize the subject line](https://chris.beams.io/posts/git-commit/#capitalize) +4. [Do not end the subject line with a period](https://chris.beams.io/posts/git-commit/#end) +5. [Use the imperative mood in the subject line](https://chris.beams.io/posts/git-commit/#imperative) +6. [Wrap the body at 72 characters](https://chris.beams.io/posts/git-commit/#wrap-72) +7. [Use the body to explain what and why vs. how](https://chris.beams.io/posts/git-commit/#why-not-how) + +Most of those are self explanatory but it is recommended that everyone at least +review the links for 5 and 7. + +### Branches +We generally follow use a [Git +Flow](https://nvie.com/posts/a-successful-git-branching-model/) branching +model. The two major long running branches are: +- `develop` - where the action happens, you probably want to start from here +- `master` - latest official release, you'll only need this when tracking down + an open bug on the release + +These two branches shall never be rebased or `--force` pushed. + +Other transient branches shall use the following naming conventions +- `feature/ABC` - long running features under development, generally "owned" by + one developer and only pushed to share for code reviews, regularly rebased on +`develop` and later deleted +- `release/vX.X.X` - where the next release is prepared and reviewed, based on + `develop`, deleted after merging into `master` and back into `develop` +- `hotfix/DEF` - where a bugfix for the current release is prepared and + reviewed, based on `master`, deleted after merging back into `master` and +into `develop` + +#### Rebase onto develop/master, don't merge +When working on your own fork, please always rebase onto whatever branch you +intend to make your PR against, and never merge from the base branch (`master` +or `develop`) to your feature. + +## Golang + +### Formatting + +#### Gofmt + +Use the official gofmt tool. It is highly recommended to run gofmt on every +save, as this will always catch any syntax errors and ensure consistent +formatting. Virtually every IDE and code editor has a plugin that runs gofmt on +save. + +Do not commit code that has not been run through the formatter. + +#### Line length + +Please limit line length to 79 characters max to avoid the need for horizontal +scrolling. This also applies to comments. Indented comments must still limit +the total line length. Most everything in golang can be broken up across +multiple lines to properly limit line length. + +The line length max for many modern editors are set to 80 characters max now. +But the default in vim is 79 for historical reasons, and so this will be +perpetuated in this project as well. + +The one exception to the max line length are string literals. In order to allow +string literals to be easily searched for in the codebase, these should not be +broken up across lines. However always insert a newline before the string +literal declaration if this allows the line length rule to be respected. + +#### Comments + +Comments must respect line length rules (see above). Comments should be used to +document exported APIs, and explain non-obvious or complex code. + +Comments must be complete english sentences. They should be concise and +precise. In general, they should explain the intent or high level behavior of +code, and only explain non-obvious implementation details or design decisions. + +Comments must be kept up to date with the code they describe. When you change +code, you must review the comments for discrepancies. + +When comments describe behavior you must not change the behavior, as this +represents an API level change. Such changes must be discussed and well +understood so that any code depending on the behavior can be updated. + +### Imports and modules + +Importing external code should be done with care. Imports should be evaluated +on test coverage, recent development activity, documentation, and code quality. +All external code that is called should be read and understood. + +Always run `go mod tidy` before committing your code. + +### Packages + +Packages should *provide* something useful and specific, not just contain +things. Packages named `common`, `utils`, and the like are prohibited. + +### Variables + +When intentionally declaring a variable with its zero value, use the `var` +declaration syntax. +```golang +var x int +``` + +Only use the short declaration syntax `:=` when declaring AND initializing a +variable to a non-zero value. +```golang +y := Type{Hello: "World"} +x, err := computeX() +``` + +Never do this: `x := Type{}` + +### Errors +Errors must always be checked, even if it is extremely unlikely, or guaranteed +not to occur. Most errors should cause `fatd` to cleanly exit. Only a few +exceptions to this rule: +- network server errors like 500 may be retried +- transaction validation errors, named `txErr` by convention + +Never report normal errors by panicking. + +#### Panic +Panics represent a program integrity error. A program integrity error is when +the program does something that is or should be impossible, or never happen. + +Examples of integrity issues: +- An out of bounds array or slice access or write +- A nil ptr dereference +- A function that must only ever be used in a certain way, with valid inputs. + e.g. regexp.MustCompile + +The idea of an integrity error, is that when the program is written correctly, +this should never occur. So if this occurs, the program is misusing something +critical. + +If you panic on an error, you should explain in comments why the error +represents an integrity issue, if it is not exceedingly obvious. + +### Types + +#### Interfaces +Interfaces define behavior, not data. Do not use interfaces to represent data. +Interfaces should describe what something *does*, not what it *is*. + +As a general rule, you probably don't need an interface. Create the concrete +type first and *discover* the appropriate interfaces later when you refactor to +de-duplicate code that needs to *do* the same thing to more than one type. + + +#### Pointer/Value semantics + +#### Factory functions +Factory functions construct and initialize a type and by convention start with +the word `New`. + +Only create factory functions for types where the initialization/set-up is not +possible or obvious from outside the package. + +Do not simply create factory functions out of convenience. It is preferred that +types that can be initialized by the user, are left to the user to initialize. + +Factory functions must follow the data semantics of the type. See Pointer/Value +semantics above. + + +### Goroutines + +You probably don't need to use a goroutine to solve the problem. Always write a +serialized solution first, evaluate performance, and only then can a concurrent +solution be considered. + + +### Logging + +Only main, engine, and srv get to log. No other package may log. Other packages +must return errors up to the caller. + + +### Documentation + + +### Testing + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..98a951e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# CONTRIBUTING + +Thank you for your interest in contributing to the Factom Asset Tokens Daemon +project. There is [plenty of +work](https://github.com/Factom-Asset-Tokens/fatd/projects/2) to do and we +welcome your contribution. + +### Ways to contribute +- Open an issue: + - Report a bug + - Request a feature + - Suggesting an improvement +- Debug an open issue: + - Reproduce and confirm it + - Identify the issue in code + - Suggest a fix +- Open a pull request: + - Implement a bug fix + - Implement an approved feature, + - Update documentation or code not conforming to [policy](./CODE_POLICY.md) + +## Getting started + +Aside from getting your build environment set up, which is described in the +[README](./README.md), please take the time to read through this and the [Code +Policy](./CODE_POLICY.md). These documents are going to evolve over time, so +please take special note of any changes to these files when you pull. + +Check out the [project +board](https://github.com/Factom-Asset-Tokens/fatd/projects/2) to get an idea +about what is currently being worked on. + +## Reporting Issues + +**!!!DO NOT REPORT SECURITY ISSUES IN THE PUBLIC ISSUE TRACKER!!!** + +Security related issues should be emailed directly to +[adam@canonical-ledgers.com](mailto:adam@canonical-ledgers.com). + +**!!!DO NOT REPORT SECURITY ISSUES IN THE PUBLIC ISSUE TRACKER!!!** + +When reporting issues, please try to use the templates provided by the GitHub +Issue tracker, which can also be found in `.github/ISSUE_TEMPLATE/` on the +`master` branch. + +Please do not use the issue tracker for support. Use the [Factom Asset Tokens +Discord](https://discord.gg/wGqT8VB) server instead. We'll be happy to help. + + +## Opening Pull Requests + +**!!!DO NOT DISCLOSE SECURITY ISSUES OR SUBMIT PATCHES HERE!!!** + +Please do not submit PRs that address unpublished security issues. If the issue +is security related, copy this template and email the issue or patch to +[adam@canonical-ledgers.com](mailto:adam@canonical-ledgers.com). + +**!!!DO NOT DISCLOSE SECURITY ISSUES OR SUBMIT PATCHES HERE!!!** + +It is highly recommended to wait for confirmation from @AdamSLevy before +proceeding with a major code change. However, if the change is small and is +clearly in line with an open issue or corrects something, by all means please +go ahead and open a PR. diff --git a/Dockerfile b/Dockerfile index 085582b..979ddf1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,7 @@ FROM alpine:3.10 COPY --from=builder /go/src/github.com/Factom-Asset-Tokens/fatd/fatd . +ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.2.1/wait /wait +RUN chmod +x /wait + ENTRYPOINT [ "./fatd" ] diff --git a/Makefile b/Makefile index 8e4bbdd..cab20cf 100644 --- a/Makefile +++ b/Makefile @@ -35,51 +35,45 @@ export GOFLAGS GOFLAGS = -gcflags=all=-trimpath=${PWD} -asmflags=all=-trimpath=${PWD} GO_LDFLAGS = -extldflags=$(LDFLAGS) -X github.com/Factom-Asset-Tokens/fatd -FATD_LDFLAGS = "$(GO_LDFLAGS)/flag.Revision=$(REVISION)" -CLI_LDFLAGS = "$(GO_LDFLAGS)/cli/cmd.Revision=$(REVISION)" +FATD_LDFLAGS = "$(GO_LDFLAGS)/internal/flag.Revision=$(REVISION)" +CLI_LDFLAGS = "$(GO_LDFLAGS)/cli.Revision=$(REVISION)" DEPSRC = go.mod go.sum SRC = $(DEPSRC) $(filter-out %_test.go,$(wildcard *.go */*.go */*/*.go)) -GENSRC=factom/idkey_gen.go factom/idkey_gen_test.go - FATDSRC=$(filter-out cli/%,$(SRC)) $(GENSRC) fatd: $(FATDSRC) - go build -ldflags=$(FATD_LDFLAGS) ./ + go build -trimpath -ldflags=$(FATD_LDFLAGS) ./ CLISRC=$(filter-out main.go engine/% state/% flag/%,$(SRC)) $(GENSRC) fat-cli: $(CLISRC) - go build -ldflags=$(CLI_LDFLAGS) -o fat-cli ./cli + go build -trimpath -ldflags=$(CLI_LDFLAGS) -o fat-cli ./cli fatd.race: $(FATDSRC) - go build -race -ldflags=$(FATD_LDFLAGS) -o fatd.race ./ + go build -trimpath -race -ldflags=$(FATD_LDFLAGS) -o fatd.race ./ fat-cli.race: $(CLISRC) - go build -race -ldflags=$(CLI_LDFLAGS) -o fat-cli.race ./cli + go build -trimpath -race -ldflags=$(CLI_LDFLAGS) -o fat-cli.race ./cli fatd.mac: $(FATDSRC) - env GOOS=darwin GOARCH=amd64 go build -ldflags=$(FATD_LDFLAGS) -o fatd.mac ./ + env GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags=$(FATD_LDFLAGS) -o fatd.mac ./ fatd.exe: $(FATDSRC) - env GOOS=windows GOARCH=amd64 go build -ldflags=$(FATD_LDFLAGS) -o fatd.exe ./ + env GOOS=windows GOARCH=amd64 go build -trimpath -ldflags=$(FATD_LDFLAGS) -o fatd.exe ./ fatd-linux: $(FATDSRC) - env GOOS=linux GOARCH=amd64 go build -ldflags=$(FATD_LDFLAGS) ./ + env GOOS=linux GOARCH=amd64 go build -trimpath -ldflags=$(FATD_LDFLAGS) ./ fat-cli.mac: $(CLISRC) - env GOOS=darwin GOARCH=amd64 go build -ldflags=$(CLI_LDFLAGS) -o fat-cli.mac ./ + env GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags=$(CLI_LDFLAGS) -o fat-cli.mac ./ fat-cli.exe: $(CLISRC) - env GOOS=windows GOARCH=amd64 go build -ldflags=$(CLI_LDFLAGS) -o fat-cli.exe ./ + env GOOS=windows GOARCH=amd64 go build -trimpath -ldflags=$(CLI_LDFLAGS) -o fat-cli.exe ./ fat-cli-linux: $(CLISRC) - env GOOS=linux GOARCH=amd64 go build -ldflags=$(CLI_LDFLAGS) -o fat-cli ./cli - -$(GENSRC): factom/gen.go factom/genmain.go $(wildcard factom/*.tmpl) - go generate ./factom - + env GOOS=linux GOARCH=amd64 go build -trimpath -ldflags=$(CLI_LDFLAGS) -o fat-cli ./cli .PHONY: clean clean-gen purge-db unpurge-db diff --git a/README.md b/README.md index c481104..ad6acdf 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,26 @@ ![](https://png.icons8.com/ios-glyphs/200/5ECCDD/octahedron.png)![](https://png.icons8.com/color/64/3498db/golang.png) +This repo contains the Golang reference implementation of the Factom Asset +Tokens protocol. This repo provides two executables for interacting with FAT +chains, as well as several Golang packages for use by external programs. + # fatd - Factom Asset Token Daemon - Alpha -A daemon written in Golang that maintains the current state of Factom Asset -Tokens (FAT) token chains. The daemon provides a JSON-RPC 2.0 API for accessing -data about token chains. +A daemon written in Golang that discovers new Factom Asset Tokens chains and +maintains their current state. The daemon provides a JSON-RPC 2.0 API for +accessing data about token chains. ### fat-cli A CLI for creating new FAT chains, as well as exploring and making transactions -on existing FAT chains. See the output from `fat-cli --help` and see -[CLI.md](CLI.md) for more information. +on existing FAT chains. See the output from `fat-cli --help` for more +information. ## Development Status -The FAT protocol and this implementation is still in Alpha. That means that we -are still testing and making changes to the protocol and the implementation. -The on-chain data protocol is relatively stable but this implementation and the -database schema is not. - -So long as the major version is v0 everything is subject to potential change. - -At times new v0 releases may require you to rebuild your fatd.db database from -scratch, but this will be minimized after the next major release due to an -improved migration framework and database validation on startup. +This implementation is now at a v1.0 release, which means that the public APIs, +both Golang and the JSON-RPC endpoint are version locked. This also means that +we believe the code to be stable and secure. That being said, the FAT system +and really the entire blockchain industry is experimental. Please help us improve the code and the protocol by trying to break things and reporting bugs! Thank you! @@ -32,6 +30,7 @@ reporting bugs! Thank you! Pre-compiled binaries for Linux, Windows, and Mac x86\_64 systems can be found on the [releases page.](https://github.com/Factom-Asset-Tokens/fatd/releases/) +However, building from source is very easy on most platforms. ## Install with Docker 🐳 @@ -58,22 +57,15 @@ $ docker run -d --name=fatd --network=host -v "fatd_db:/fatd.db" fatd [fatd opti #### Build Dependencies This project uses SQLite3 which uses [CGo](https://blog.golang.org/c-go-cgo) to -dynamically link to the SQLite3 C shared libraries to the `fatd` Golang binary. -CGo requires that GCC be available on your system. +compile and statically link the SQLite3 C libraries to the `fatd` Golang +binary. CGo requires that GCC be available on your system. The following dependencies are required to build `fatd` and `fat-cli`. -- [Golang](https://golang.org/) 1.11.4 or later. The latest official release of +- [Golang](https://golang.org/) 1.13 or later. The latest official release of Golang is always recommended. -- [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports) is used by - the code generation used in the `./factom` package. The code generation step -is not required as the latest generated code is already checked into the -repository. Sometimes `make` will try to run this step which will result in an -error if `goimprots` is not installed. - [GNU GCC](https://gcc.gnu.org/) is used by [CGo](https://blog.golang.org/c-go-cgo) to link to the SQLite3 shared libraries. -- [SQLite3](https://sqlite.org/index.html) is the database `fatd` uses to save - state. - [Git](https://git-scm.com/) is used to clone the project and is used by `go build` to pull some dependencies. - [GNU Bash](https://www.gnu.org/software/bash/) is used by a small script @@ -131,69 +123,43 @@ when `fatd` or `fat-cli` is updated. -## Running +## Getting started + +The Daemon needs a connection to `factomd`'s API. This defaults to +`http://localhost:8088` and can be specified with `-s`. + Start the daemon from the command line: ``` -$ ./fatd -INFO Fatd Version: r155.c812dd1 pkg=main +INFO Fatd Version: v0.6.0.r110.g73bdb76 pkg=main +INFO Loading chain databases from /home/aslevy/.fatd/mainnet/... pkg=engine INFO State engine started. pkg=main +INFO Listening on :8078... pkg=srv INFO JSON RPC API server started. pkg=main INFO Factom Asset Token Daemon started. pkg=main -INFO Syncing from block 183396 to 183520... pkg=engine -INFO Synced. pkg=engine -``` - -### Exiting -To tell `fatd` to safely exit send a `SIGINT`. From most shells this can be -done by simply pressing `CTRL`+`c`. -``` -INFO Synced. pkg=engine -^CINFO SIGINT: Shutting down now. pkg=main -INFO Factom Asset Token Daemon stopped. pkg=main -INFO JSON RPC API server stopped. pkg=main -INFO State engine stopped. pkg=main -$ +INFO Searching for new FAT chains from block 163181 to 215642... pkg=engine +INFO Tracking new FAT chain: b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb pkg=engine +INFO Syncing... chain=b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb +INFO Synced. chain=b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb ``` +At this point `fatd` has synced the first ever created FAT chain on mainnet, +which is for testing purposes. It is continuing to scan for all valid FAT +chains so it can track them. +If you know the chain id of a FAT chain you are interested in, you can fast +sync it by using the `-whitelist` flag. -## Startup Flags & Options +The daemon can be stopped and restarted and syncing will resume from the latest +point. By default the database directory is `~/.fatd/`. It can be specified +with `-dbdir`. -Control how fatd runs using additional options at startup. For example: +Once the JSON RPC API is started, `fat-cli` can be used to query about synced +chains, transactions and balances. -```bash -./fatd -debug -dbpath /home/ubuntu/mycustomfolder -``` +For a complete an up-to-date list of flags & options please see `fatd -h` and +`fat-cli -h`. - - -| Name | Description | Validation | Default | -| ----------------- | ------------------------------------------------------------ | ------------------------ | ------------------------- | -| `startscanheight` | The Factom block height to begin scanning for new FAT chains and transactions | Positive Integer | 0 | -| `debug` | Enable debug mode for extra information during runtime. No value needed. | - | - | -| `dbpath` | Specify the path to use as fatd's sqlite database. | Valid system path | Current working directory | -| `ecpub` | The public Entry Credit address used to pay for submitting transactions | Valid EC address | - | -| `apiaddress` | What port string the FAT daemon RPC will be bound to | String | `:8078` | -| | | | | -| `s` | The URL of the Factom API host | Valid URL | `localhost:8088` | -| `factomdtimeout` | The timeout in seconds to time out requests to factomd | integer | 0 | -| `factomduser` | The username of the user for factomd API authentication | string | - | -| `factomdpassword` | The password of the user for factomd API authentication | string | - | -| `factomdcert` | Path to the factomd connection TLS certificate file | Valid system path string | - | -| `factomdtls` | Whether to use TLS on connection to factomd | boolean | false | -| | | | | -| `w` | The URL of the Factom Wallet Daemon API host | Valid URL | `localhost:8089` | -| `wallettimeout` | The timeout in seconds to time out requests to factomd | integer | 0 | -| `walletuser` | The username of the user for walletd API authentication | string | - | -| `walletpassword` | The username of the user for walletd API authentication | string | - | -| `walletcert` | Path to the walletd connection TLS certificate file | Valid system path string | - | -| `wallettls` | Whether to use TLS on connection to walletd | boolean | false | - -For a complete up to date list of flags & options please see `flag/flag.go` - - - -## [FAT CLI Documentation](CLI.md) +### Create a chain, make transactions Interact with the FAT daemon RPC from the command line @@ -207,15 +173,7 @@ Default `http://localhost:8078/v1` - ## Contributing -All PRs should be rebased on the latest `develop` branch commit. - -## Issues - -Please attempt to reproduce the issue using the `-debug` flag. For `fatd`, -please provide the initial output which prints all current settings. -Intermediate `DEBUG Scanning block 187682 for FAT entries.` lines may be -omitted, but please provide the first and last of these lines. +See [CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/srv/client.go b/api/client.go similarity index 87% rename from srv/client.go rename to api/client.go index c34a36c..6814cb5 100644 --- a/srv/client.go +++ b/api/client.go @@ -20,13 +20,14 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package srv +package api import ( + "context" "fmt" "time" - jrpc "github.com/AdamSLevy/jsonrpc2/v11" + jrpc "github.com/AdamSLevy/jsonrpc2/v12" ) // Client makes RPC requests to fatd's APIs. Client embeds a jsonrpc2.Client, @@ -52,10 +53,11 @@ func NewClient() *Client { } // Request makes a request to fatd's v1 API. -func (c *Client) Request(method string, params, result interface{}) error { - url := c.FatdServer + "/v1" +func (c *Client) Request(ctx context.Context, + method string, params, result interface{}) error { + if c.DebugRequest { - fmt.Println("fatd:", url) + fmt.Println("fatd:", c.FatdServer) } - return c.Client.Request(url, method, params, result) + return c.Client.Request(ctx, c.FatdServer, method, params, result) } diff --git a/srv/errors.go b/api/errors.go similarity index 70% rename from srv/errors.go rename to api/errors.go index 32a183b..0ce4be8 100644 --- a/srv/errors.go +++ b/api/errors.go @@ -20,18 +20,20 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package srv +package api -import jrpc "github.com/AdamSLevy/jsonrpc2/v11" +import jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" var ( - ErrorTokenNotFound = jrpc.NewError(-32800, "Token Not Found", + ErrorTokenNotFound = jsonrpc2.NewError(-32800, "Token Not Found", "token may be invalid, or not yet issued or tracked") - ErrorTransactionNotFound = jrpc.NewError(-32803, "Transaction Not Found", + ErrorTransactionNotFound = jsonrpc2.NewError(-32803, "Transaction Not Found", "no matching tx-id was found") - ErrorInvalidTransaction = jrpc.NewError(-32804, "Invalid Transaction", nil) - ErrorTokenSyncing = jrpc.NewError(-32805, "Token Syncing", + ErrorInvalidTransaction = jsonrpc2.NewError(-32804, "Invalid Transaction", nil) + ErrorTokenSyncing = jsonrpc2.NewError(-32805, "Token Syncing", "token is in the process of syncing") - ErrorNoEC = jrpc.NewError(-32806, "No Entry Credits", + ErrorNoEC = jsonrpc2.NewError(-32806, "No Entry Credits", "not configured with entry credits") + ErrorPendingDisabled = jsonrpc2.NewError(-32807, "Pending Transactions Disabled", + "fatd is not tracking pending transactions") ) diff --git a/srv/params.go b/api/params.go similarity index 69% rename from srv/params.go rename to api/params.go index ea54e7a..fc69ae1 100644 --- a/srv/params.go +++ b/api/params.go @@ -20,21 +20,22 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package srv +package api import ( "strings" "time" - jrpc "github.com/AdamSLevy/jsonrpc2/v11" - "github.com/Factom-Asset-Tokens/fatd/factom" + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" + "github.com/Factom-Asset-Tokens/factom" "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" + "github.com/Factom-Asset-Tokens/fatd/fat1" ) type Params interface { IsValid() error ValidChainID() *factom.Bytes32 + GetIncludePending() bool } // ParamsToken scopes a request down to a single FAT token using either the @@ -43,47 +44,58 @@ type ParamsToken struct { ChainID *factom.Bytes32 `json:"chainid,omitempty"` TokenID string `json:"tokenid,omitempty"` IssuerChainID *factom.Bytes32 `json:"issuerid,omitempty"` + + IncludePending bool `json:"includepending,omitempty"` } func (p ParamsToken) IsValid() error { if p.ChainID != nil { if len(p.TokenID) > 0 || p.IssuerChainID != nil { - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `cannot use "chainid" with "tokenid" or "issuerid"`) } return nil } if len(p.TokenID) > 0 || p.IssuerChainID != nil { if len(p.TokenID) == 0 { - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `"tokenid" is required with "issuerid"`) } if p.IssuerChainID == nil { - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `"issuerid" is required with "tokenid"`) } return nil } - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `required: either "chainid" or both "tokenid" and "issuerid"`) } +func (p ParamsToken) GetIncludePending() bool { return p.IncludePending } + func (p ParamsToken) ValidChainID() *factom.Bytes32 { if p.ChainID != nil { return p.ChainID } - chainID := fat.ChainID(p.TokenID, *p.IssuerChainID) + chainID := fat.ComputeChainID(p.TokenID, p.IssuerChainID) p.ChainID = &chainID return p.ChainID } type ParamsPagination struct { - Page uint64 `json:"page,omitempty"` - Limit uint64 `json:"limit,omitempty"` + Page *uint `json:"page,omitempty"` + Limit uint `json:"limit,omitempty"` Order string `json:"order,omitempty"` } func (p *ParamsPagination) IsValid() error { + if p.Page == nil { + p.Page = new(uint) + *p.Page = 1 + } else if *p.Page == 0 { + return jsonrpc2.ErrorInvalidParams( + `"order" value must be either "asc" or "desc"`) + } if p.Limit == 0 { p.Limit = 25 } @@ -93,7 +105,7 @@ func (p *ParamsPagination) IsValid() error { case "", "asc", "desc": // ok default: - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `"order" value must be either "asc" or "desc"`) } @@ -112,7 +124,7 @@ func (p ParamsGetTransaction) IsValid() error { return err } if p.Hash == nil { - return jrpc.InvalidParams(`required: "entryhash"`) + return jsonrpc2.ErrorInvalidParams(`required: "entryhash"`) } return nil } @@ -139,13 +151,13 @@ func (p *ParamsGetTransactions) IsValid() error { switch p.ToFrom { case "to", "from": if len(p.Addresses) == 0 { - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `"addresses" may not be empty when "tofrom" is set`) } case "": // empty is ok default: - return jrpc.InvalidParams( + return jsonrpc2.ErrorInvalidParams( `"tofrom" value must be either "to" or "from"`) } return nil @@ -161,7 +173,7 @@ func (p ParamsGetNFToken) IsValid() error { return err } if p.NFTokenID == nil { - return jrpc.InvalidParams(`required: "nftokenid"`) + return jsonrpc2.ErrorInvalidParams(`required: "nftokenid"`) } return nil } @@ -176,18 +188,21 @@ func (p ParamsGetBalance) IsValid() error { return err } if p.Address == nil { - return jrpc.InvalidParams(`required: "address"`) + return jsonrpc2.ErrorInvalidParams(`required: "address"`) } return nil } type ParamsGetBalances struct { - Address *factom.FAAddress `json:"address,omitempty"` + Address *factom.FAAddress `json:"address,omitempty"` + IncludePending bool `json:"includepending,omitempty"` } +func (p ParamsGetBalances) GetIncludePending() bool { return p.IncludePending } + func (p ParamsGetBalances) IsValid() error { if p.Address == nil { - return jrpc.InvalidParams(`required: "address"`) + return jsonrpc2.ErrorInvalidParams(`required: "address"`) } return nil } @@ -209,7 +224,7 @@ func (p *ParamsGetNFBalance) IsValid() error { return err } if p.Address == nil { - return jrpc.InvalidParams(`required: "address"`) + return jsonrpc2.ErrorInvalidParams(`required: "address"`) } return nil } @@ -231,25 +246,52 @@ func (p *ParamsGetAllNFTokens) IsValid() error { type ParamsSendTransaction struct { ParamsToken - ExtIDs []factom.Bytes `json:"extids"` - Content factom.Bytes `json:"content"` + ExtIDs []factom.Bytes `json:"extids,omitempty"` + Content factom.Bytes `json:"content,omitempty"` + Raw factom.Bytes `json:"raw,omitempty"` + DryRun bool `json:"dryrun,omitempty"` + entry factom.Entry } -func (p ParamsSendTransaction) IsValid() error { +func (p *ParamsSendTransaction) IsValid() error { + if p.Raw != nil { + if p.ExtIDs != nil || p.Content != nil || + p.ParamsToken.IsValid() == nil { + return jsonrpc2.ErrorInvalidParams( + `"raw cannot be used with "content" or "extids"`) + } + if err := p.entry.UnmarshalBinary(p.Raw); err != nil { + return jsonrpc2.ErrorInvalidParams(err) + } + p.entry.Timestamp = time.Now() + p.ChainID = p.entry.ChainID + return nil + } if err := p.ParamsToken.IsValid(); err != nil { return err } if len(p.Content) == 0 || len(p.ExtIDs) == 0 { - return jrpc.InvalidParams(`required: "content" and "extids"`) + return jsonrpc2.ErrorInvalidParams( + `required: "raw" or "content" and "extids"`) } - return nil -} - -func (p ParamsSendTransaction) Entry() factom.Entry { - return factom.Entry{ + p.entry = factom.Entry{ ExtIDs: p.ExtIDs, Content: p.Content, Timestamp: time.Now(), - ChainID: p.ChainID, + ChainID: p.ValidChainID(), + } + + data, err := p.entry.MarshalBinary() + if err != nil { + return jsonrpc2.ErrorInvalidParams(err) } + hash := factom.ComputeEntryHash(data) + p.entry.Hash = &hash + p.Raw = data + + return nil +} + +func (p ParamsSendTransaction) Entry() factom.Entry { + return p.entry } diff --git a/api/results.go b/api/results.go new file mode 100644 index 0000000..d2fa884 --- /dev/null +++ b/api/results.go @@ -0,0 +1,104 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package api + +import ( + "encoding/json" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/fat1" +) + +const APIVersion = "1" + +type ResultGetIssuance struct { + ParamsToken + Hash *factom.Bytes32 `json:"entryhash"` + Timestamp int64 `json:"timestamp"` + Issuance fat.Issuance `json:"issuance"` +} + +type ResultGetTransaction struct { + Hash *factom.Bytes32 `json:"entryhash"` + Timestamp int64 `json:"timestamp"` + Tx interface{} `json:"data"` + Pending bool `json:"pending,omitempty"` +} + +type ResultGetBalances map[factom.Bytes32]uint64 + +func (r ResultGetBalances) MarshalJSON() ([]byte, error) { + strMap := make(map[string]uint64, len(r)) + for chainID, balance := range r { + strMap[chainID.String()] = balance + } + return json.Marshal(strMap) +} + +func (r *ResultGetBalances) UnmarshalJSON(data []byte) error { + var strMap map[string]uint64 + if err := json.Unmarshal(data, &strMap); err != nil { + return err + } + *r = make(map[factom.Bytes32]uint64, len(strMap)) + var chainID factom.Bytes32 + for str, balance := range strMap { + if err := chainID.Set(str); err != nil { + return err + } + (*r)[chainID] = balance + } + return nil +} + +type ResultGetStats struct { + ParamsToken + Issuance *fat.Issuance + IssuanceHash *factom.Bytes32 + CirculatingSupply uint64 `json:"circulating"` + Burned uint64 `json:"burned"` + Transactions int64 `json:"transactions"` + IssuanceTimestamp int64 `json:"issuancets"` + LastTransactionTimestamp int64 `json:"lasttxts,omitempty"` + NonZeroBalances int64 `json:"nonzerobalances, omitempty"` +} + +type ResultGetNFToken struct { + NFTokenID fat1.NFTokenID `json:"id"` + Owner *factom.FAAddress `json:"owner,omitempty"` + Burned bool `json:"burned,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` + CreationTx *factom.Bytes32 `json:"creationtx"` +} + +type ResultGetDaemonProperties struct { + FatdVersion string `json:"fatdversion"` + APIVersion string `json:"apiversion"` + NetworkID factom.NetworkID `json:"factomnetworkid"` +} + +type ResultGetSyncStatus struct { + Sync uint32 `json:"syncheight"` + Current uint32 `json:"factomheight"` +} diff --git a/cli/cmd/complete.go b/cli/complete.go similarity index 99% rename from cli/cmd/complete.go rename to cli/complete.go index 7d9bca2..00d0097 100644 --- a/cli/cmd/complete.go +++ b/cli/complete.go @@ -20,7 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( goflag "flag" diff --git a/cli/cmd/get.go b/cli/get.go similarity index 99% rename from cli/cmd/get.go rename to cli/get.go index a04c04d..e565ed0 100644 --- a/cli/cmd/get.go +++ b/cli/get.go @@ -20,7 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( "github.com/posener/complete" diff --git a/cli/cmd/getbalance.go b/cli/getbalance.go similarity index 69% rename from cli/cmd/getbalance.go rename to cli/getbalance.go index a7dde34..d0a2c16 100644 --- a/cli/cmd/getbalance.go +++ b/cli/getbalance.go @@ -20,16 +20,17 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "fmt" "math" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/fat0" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/Factom-Asset-Tokens/fatd/srv" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" + "github.com/Factom-Asset-Tokens/fatd/fat0" + "github.com/Factom-Asset-Tokens/fatd/fat1" "github.com/posener/complete" "github.com/spf13/cobra" ) @@ -97,57 +98,81 @@ func getBalanceArgs(cmd *cobra.Command, args []string) error { func getBalance(cmd *cobra.Command, _ []string) { if paramsToken.ChainID == nil { - var params srv.ParamsGetBalances + var params api.ParamsGetBalances + params.IncludePending = paramsToken.IncludePending vrbLog.Println("Fetching balances for all chains...") for _, adr := range addresses { params.Address = &adr - var balances srv.ResultGetBalances - if err := FATClient.Request("get-balances", params, - &balances); err != nil { + var balances api.ResultGetBalances + if err := FATClient.Request(context.Background(), + "get-balances", params, &balances); err != nil { errLog.Fatal(err) } - fmt.Printf("%v:", adr) + fmt.Printf("%v ", adr) if len(balances) == 0 { fmt.Println(" none") continue } fmt.Println() for chainID, balance := range balances { - fmt.Printf("\t%v: %v\n", chainID, balance) + vrbLog.Printf("Fetching token chain details... %v", chainID) + params := api.ParamsToken{ChainID: &chainID} + var stats api.ResultGetStats + if err := FATClient.Request(context.Background(), + "get-stats", params, &stats); err != nil { + errLog.Fatal(err) + } + var bal interface{} + if stats.Issuance.Precision > 1 { + bal = float64(balance) / math.Pow10( + int(stats.Issuance.Precision)) + } else { + bal = balance + } + fmt.Printf("\t%v %v\n", chainID, bal) } } return } + vrbLog.Printf("Fetching token chain details... %v", paramsToken.ChainID) - params := srv.ParamsToken{ChainID: paramsToken.ChainID} - var stats srv.ResultGetStats - if err := FATClient.Request("get-stats", params, &stats); err != nil { + params := api.ParamsToken{ChainID: paramsToken.ChainID} + var stats api.ResultGetStats + if err := FATClient.Request(context.Background(), + "get-stats", params, &stats); err != nil { errLog.Fatal(err) } switch stats.Issuance.Type { case fat0.Type: - params := srv.ParamsGetBalance{} + var params api.ParamsGetBalance params.ChainID = paramsToken.ChainID + params.IncludePending = paramsToken.IncludePending vrbLog.Println("Fetching balances...") for _, adr := range addresses { params.Address = &adr var balance uint64 - if err := FATClient.Request("get-balance", params, - &balance); err != nil { + if err := FATClient.Request(context.Background(), + "get-balance", params, &balance); err != nil { errLog.Fatal(err) } - fmt.Println(adr, balance) + if stats.Issuance.Precision > 1 { + fmt.Println(adr, float64(balance)/math.Pow10( + int(stats.Issuance.Precision))) + } else { + fmt.Println(adr, balance) + } } case fat1.Type: - var params srv.ParamsGetNFBalance + var params api.ParamsGetNFBalance params.Limit = math.MaxUint64 params.ChainID = paramsToken.ChainID + params.IncludePending = paramsToken.IncludePending vrbLog.Println("Fetching NF balances...") for _, adr := range addresses { params.Address = &adr var balance fat1.NFTokens - if err := FATClient.Request("get-nf-balance", params, - &balance); err != nil { + if err := FATClient.Request(context.Background(), + "get-nf-balance", params, &balance); err != nil { errLog.Fatal(err) } fmt.Println(adr, balance) diff --git a/cli/cmd/getchains.go b/cli/getchains.go similarity index 77% rename from cli/cmd/getchains.go rename to cli/getchains.go index 1af17e2..1632e6e 100644 --- a/cli/cmd/getchains.go +++ b/cli/getchains.go @@ -20,13 +20,15 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "fmt" + "time" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/srv" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" "github.com/posener/complete" "github.com/spf13/cobra" ) @@ -98,9 +100,9 @@ func getChainsArgs(_ *cobra.Command, args []string) error { func getChains(_ *cobra.Command, _ []string) { if len(chainIDs) == 0 { vrbLog.Println("Fetching list of issued token chains...") - var chains []srv.ParamsToken - if err := FATClient.Request("get-daemon-tokens", nil, - &chains); err != nil { + var chains []api.ParamsToken + if err := FATClient.Request(context.Background(), + "get-daemon-tokens", nil, &chains); err != nil { errLog.Fatal(err) } for _, chain := range chains { @@ -115,35 +117,40 @@ Token ID: %q for _, chainID := range chainIDs { vrbLog.Printf("Fetching token chain details... %v", chainID) - params := srv.ParamsToken{ChainID: &chainID} - var stats srv.ResultGetStats - if err := FATClient.Request("get-stats", params, &stats); err != nil { + params := api.ParamsToken{ChainID: &chainID, + IncludePending: paramsToken.IncludePending} + var stats api.ResultGetStats + if err := FATClient.Request(context.Background(), + "get-stats", params, &stats); err != nil { errLog.Fatal(err) } printStats(&chainID, stats) } } -func printStats(chainID *factom.Bytes32, stats srv.ResultGetStats) { - fmt.Printf(`Chain ID: %v -Issuer Identity Chain ID: %v -Token ID: %v -Type: %v -Symbol: %q -Supply: %v -Ciculating Supply: %v -Burned: %v -Number of Transactions: %v +func printStats(chainID *factom.Bytes32, stats api.ResultGetStats) { + fmt.Printf(` +Chain ID: %v +Issuer Identity: %v +Issuance Entry: %v +Token ID: %v +Type: %v +Symbol: %q +Precision: %v +Supply: %v +Circulating Supply: %v +Burned: %v +Number of Transactions: %v Issuance Timestamp: %v `, - chainID, stats.IssuerChainID, stats.TokenID, - stats.Issuance.Type, stats.Issuance.Symbol, + chainID, stats.IssuerChainID, stats.IssuanceHash, stats.TokenID, + stats.Issuance.Type, stats.Issuance.Symbol, stats.Issuance.Precision, stats.Issuance.Supply, stats.CirculatingSupply, stats.Burned, stats.Transactions, - stats.IssuanceTimestamp) + time.Unix(stats.IssuanceTimestamp, 0)) if stats.LastTransactionTimestamp > 0 { - fmt.Printf("Last Tx Timestamp: %v\n", - stats.LastTransactionTimestamp) + fmt.Printf("Last Tx Timestamp: %v\n", + time.Unix(stats.LastTransactionTimestamp, 0)) } fmt.Println() diff --git a/cli/cmd/gettransactions.go b/cli/gettransactions.go similarity index 80% rename from cli/cmd/gettransactions.go rename to cli/gettransactions.go index e72ef07..0271795 100644 --- a/cli/cmd/gettransactions.go +++ b/cli/gettransactions.go @@ -20,26 +20,30 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "encoding/json" "fmt" "strings" + "time" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/Factom-Asset-Tokens/fatd/srv" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" + "github.com/Factom-Asset-Tokens/fatd/fat1" "github.com/posener/complete" "github.com/spf13/cobra" ) var ( - paramsGetTxs = srv.ParamsGetTransactions{ + paramsGetTxs = api.ParamsGetTransactions{ StartHash: new(factom.Bytes32), NFTokenID: new(fat1.NFTokenID), - ParamsToken: srv.ParamsToken{ChainID: paramsToken.ChainID}, + ParamsToken: api.ParamsToken{ChainID: paramsToken.ChainID}, + ParamsPagination: api.ParamsPagination{Page: new(uint), + Order: "desc"}, } to, from bool transactionIDs []factom.Bytes32 @@ -50,7 +54,7 @@ var getTxsCmd = func() *cobra.Command { cmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: ` -transactions --chainid TXID... +transactions --chainid TXHASH... fat-cli get transactions --chainid [--starttx ] [--page ] [--limit ] [--order <"asc" | "desc">] @@ -80,10 +84,10 @@ more, and in the case of a FAT-1 chain, by a single --nftokenid. Use --page and rootCmplCmd.Sub["help"].Sub["get"].Sub["transactions"] = complete.Command{} flags := cmd.Flags() - flags.Uint64VarP(¶msGetTxs.Page, "page", "p", 1, "Page of returned txs") - flags.Uint64VarP(¶msGetTxs.Limit, "limit", "l", 10, "Limit of returned txs") + flags.UintVarP(paramsGetTxs.Page, "page", "p", 1, "Page of returned txs") + flags.UintVarP(¶msGetTxs.Limit, "limit", "l", 10, "Limit of returned txs") flags.VarPF((*txOrder)(¶msGetTxs.Order), "order", "", "Order of returned txs"). - DefValue = "asc" + DefValue = "desc" flags.BoolVar(&to, "to", false, "Request only txs TO the given --address set") flags.BoolVar(&from, "from", false, "Request only txs FROM the given --address set") flags.VarPF(paramsGetTxs.StartHash, "starttx", "", @@ -127,10 +131,11 @@ func validateGetTxsFlags(cmd *cobra.Command, args []string) error { if err := validateChainIDFlags(cmd, args); err != nil { return err } + paramsGetTxs.IncludePending = paramsToken.IncludePending flags := cmd.LocalFlags() if len(transactionIDs) > 0 { for _, flgName := range []string{"page", "order", "page", "limit", - "starttxhash", "to", "from", "nftokenid", "address"} { + "starttx", "to", "from", "nftokenid", "address"} { if flags.Changed(flgName) { return fmt.Errorf("--%v is incompatible with TXID arguments", flgName) @@ -153,13 +158,22 @@ func validateGetTxsFlags(cmd *cobra.Command, args []string) error { } } - if !flags.Changed("starttxhash") { + if !flags.Changed("starttx") { paramsGetTxs.StartHash = nil } if !flags.Changed("nftokenid") { paramsGetTxs.NFTokenID = nil } + if flags.Changed("page") { + if *paramsGetTxs.Page == 0 { + return fmt.Errorf("--page cannot be 0, starts at 1") + } + if *paramsGetTxs.Page == 1 { + // No need to explicitly send "page": 1 + paramsGetTxs.Page = nil + } + } return nil } @@ -168,12 +182,12 @@ func getTxs(_ *cobra.Command, _ []string) { vrbLog.Printf("Fetching txs for chain... %v", paramsToken.ChainID) if len(transactionIDs) == 0 { - result := make([]srv.ResultGetTransaction, paramsGetTxs.Limit) + result := make([]api.ResultGetTransaction, paramsGetTxs.Limit) for i := range result { result[i].Tx = &json.RawMessage{} } - if err := FATClient.Request("get-transactions", - paramsGetTxs, &result); err != nil { + if err := FATClient.Request(context.Background(), + "get-transactions", paramsGetTxs, &result); err != nil { errLog.Fatal(err) } for _, result := range result { @@ -181,15 +195,15 @@ func getTxs(_ *cobra.Command, _ []string) { } return } - params := srv.ParamsGetTransaction{ParamsToken: paramsGetTxs.ParamsToken} - result := srv.ResultGetTransaction{} - tx := json.RawMessage{} + params := api.ParamsGetTransaction{ParamsToken: paramsGetTxs.ParamsToken} + var result api.ResultGetTransaction + var tx json.RawMessage result.Tx = &tx for _, txID := range transactionIDs { vrbLog.Printf("Fetching tx details... %v", txID) params.Hash = &txID - if err := FATClient.Request("get-transaction", - params, &result); err != nil { + if err := FATClient.Request(context.Background(), + "get-transaction", params, &result); err != nil { errLog.Fatal(err) } printTx(result) @@ -197,10 +211,14 @@ func getTxs(_ *cobra.Command, _ []string) { return } -func printTx(result srv.ResultGetTransaction) { +func printTx(result api.ResultGetTransaction) { + if result.Pending { + fmt.Println("PENDING TX") + } fmt.Println("TXID:", result.Hash) - fmt.Println("Timestamp:", result.Timestamp) + fmt.Println("Timestamp:", time.Unix(result.Timestamp, 0)) fmt.Println("TX:", (string)(*result.Tx.(*json.RawMessage))) + fmt.Println() } @@ -228,7 +246,7 @@ func (o *txOrder) Set(str string) error { switch str { case "asc", "ascending", "earliest": *o = "asc" - case "desc", "descending", "latest": + case "des", "desc", "descending", "latest": *o = "desc" default: return fmt.Errorf(`must be "asc" or "desc"`) diff --git a/cli/cmd/issue.go b/cli/issue.go similarity index 80% rename from cli/cmd/issue.go rename to cli/issue.go index 904b77a..2851b22 100644 --- a/cli/cmd/issue.go +++ b/cli/issue.go @@ -20,18 +20,19 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "encoding/json" "fmt" "strings" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/srv" - jrpc "github.com/AdamSLevy/jsonrpc2/v11" + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" "github.com/posener/complete" "github.com/spf13/cobra" flag "github.com/spf13/pflag" @@ -122,7 +123,9 @@ Entry Credits "Token standard to use").DefValue = "" flags.VarPF(&sk1, "sk1", "", "Secret Identity Key 1 to sign entry").DefValue = "" flags.Int64Var(&Issuance.Supply, "supply", 0, - "Max Token supply, use -1 for unlimited") + "Max Token supply in base units, use -1 for unlimited") + flags.UintVar(&Issuance.Precision, "precision", 0, + "Number of whole unit decimal places, \ni.e. 3 means 1.0 is 1000 base units") flags.StringVar(&Issuance.Symbol, "symbol", "", "Optional abbreviated token symbol") flags.VarPF((*RawMessage)(&Issuance.Metadata), "metadata", "m", "JSON metadata to include in tx") @@ -153,10 +156,11 @@ var issueCmplCmd = complete.Command{ } var ( - missingChainHeadErr = jrpc.Error{Code: -32009, Message: "Missing Chain Head"} - newChainInProcessListErr = jrpc.Error{Message: "new chain in process list"} + missingChainHeadErr = jsonrpc2.Error{ + Code: -32009, Message: "Missing Chain Head"} + newChainInProcessListErr = jsonrpc2.Error{Message: "new chain in process list"} - first factom.Entry + first = factom.Entry{Content: factom.Bytes{}} chainExists bool Issuance fat.Issuance @@ -194,38 +198,44 @@ func validateIssueFlags(cmd *cobra.Command, args []string) error { } vrbLog.Println("Preparing and signing Token Initialization Entry...") - Issuance.ChainID = paramsToken.ChainID - if err := Issuance.MarshalEntry(); err != nil { + Issuance.Entry.ChainID = paramsToken.ChainID + e, err := Issuance.Sign(sk1) + if err != nil { errLog.Fatal(err) } - Issuance.Sign(sk1) - initCost, err := Issuance.Cost() + initCost, err := e.Cost() if err != nil { errLog.Fatal(err) } + Issuance.Entry = e if !force { vrbLog.Println("Checking chain existence...") eb := factom.EBlock{ChainID: paramsToken.ChainID} - if err := eb.GetChainHead(FactomClient); err != nil { - rpcErr, _ := err.(jrpc.Error) - if rpcErr != missingChainHeadErr && - rpcErr != newChainInProcessListErr { - // If err was anything other than the missingChainHeadErr... + inProcessList, err := eb.GetChainHead(context.Background(), FactomClient) + if err != nil { + if err, ok := err.(jsonrpc2.Error); ok { + if err != (jsonrpc2.Error{ + Code: -32009, Message: "Missing Chain Head"}) { + errLog.Fatal(err) + } + } else { errLog.Fatal(err) } - } else { + } + if inProcessList || eb.KeyMR != nil { chainCost = 0 chainExists = true vrbLog.Printf("Chain already exists.") } vrbLog.Println("Checking token chain status...") - params := srv.ParamsToken{ChainID: paramsToken.ChainID} - var stats srv.ResultGetStats - if err := FATClient.Request("get-stats", params, &stats); err != nil { - rpcErr, _ := err.(jrpc.Error) - if rpcErr != *srv.ErrorTokenNotFound { + params := api.ParamsToken{ChainID: paramsToken.ChainID} + var stats api.ResultGetStats + if err := FATClient.Request(context.Background(), + "get-stats", params, &stats); err != nil { + rpcErr, _ := err.(jsonrpc2.Error) + if rpcErr != api.ErrorTokenNotFound { errLog.Fatal(err) } } else { @@ -257,9 +267,9 @@ func validateECAdrFlag(cmd *cobra.Command, _ []string) error { var err error if ecEsAdr.Es == zero { vrbLog.Println("Fetching secret address...", ecEsAdr.EC) - ecEsAdr.Es, err = ecEsAdr.EC.GetEsAddress(FactomClient) + ecEsAdr.Es, err = ecEsAdr.EC.GetEsAddress(context.Background(), FactomClient) if err != nil { - if err, ok := err.(jrpc.Error); ok { + if err, ok := err.(jsonrpc2.Error); ok { errLog.Fatal(err.Data, ecEsAdr.EC) } errLog.Fatal(err) @@ -268,9 +278,9 @@ func validateECAdrFlag(cmd *cobra.Command, _ []string) error { return nil } -func verifyECBalance(ec *factom.ECAddress, cost int8) { +func verifyECBalance(ec *factom.ECAddress, cost uint8) { vrbLog.Println("Checking EC balance... ") - ecBalance, err := ec.GetBalance(FactomClient) + ecBalance, err := ec.GetBalance(context.Background(), FactomClient) if err != nil { errLog.Fatal(err) } @@ -284,11 +294,12 @@ func verifySK1Key(sk1 *factom.SK1Key, idChainID *factom.Bytes32) { vrbLog.Printf("Fetching Identity Chain...") var identity factom.Identity identity.ChainID = idChainID - if err := identity.Get(FactomClient); err != nil { - rpcErr, _ := err.(jrpc.Error) + if err := identity.Get(context.Background(), FactomClient); err != nil { + rpcErr, _ := err.(jsonrpc2.Error) if rpcErr == newChainInProcessListErr { - errLog.Fatalf("New identity chain %v is in process list. "+ - "Wait ~10 mins.\n", idChainID) + errLog.Fatalf( + "New identity chain %v is in process list. Wait ~10 mins.\n", + idChainID) } if rpcErr == missingChainHeadErr { errLog.Fatalf("Identity Chain does not exist: %v", idChainID) @@ -296,9 +307,9 @@ func verifySK1Key(sk1 *factom.SK1Key, idChainID *factom.Bytes32) { errLog.Fatal(err) } vrbLog.Println("Verifying SK1 Key... ") - if identity.ID1 != sk1.ID1Key() { - errLog.Fatal("--sk1 is not the secret key corresponding to " + - "the ID1Key declared in the Identity Chain.") + if *identity.ID1Key != sk1.ID1Key() { + errLog.Fatal( + "--sk1 is not the secret key corresponding to the ID1Key declared in the Identity Chain.") } } @@ -317,9 +328,9 @@ func issueChain(_ *cobra.Command, _ []string) { } vrbLog.Println("Submitting the Chain Creation Entry to the Factom blockchain...") - txID, err := first.ComposeCreate(FactomClient, ecEsAdr.Es) + txID, err := first.ComposeCreate(context.Background(), FactomClient, ecEsAdr.Es) if err != nil { - errLog.Fatal(err) + errLog.Fatal(fmt.Errorf("factom.Entry.ComposeCreate(): %w", err)) } fmt.Println("Chain Creation Entry Submitted") fmt.Println("Chain ID: ", first.ChainID) @@ -330,7 +341,7 @@ func issueChain(_ *cobra.Command, _ []string) { } func issueToken(_ *cobra.Command, _ []string) { if curl { - if err := printCurl(Issuance.Entry.Entry, ecEsAdr.Es); err != nil { + if err := printCurl(Issuance.Entry, ecEsAdr.Es); err != nil { errLog.Fatal(err) } return @@ -338,13 +349,15 @@ func issueToken(_ *cobra.Command, _ []string) { vrbLog.Println( "Submitting the Token Initialization Entry to the Factom blockchain...") - txID, err := Issuance.ComposeCreate(FactomClient, ecEsAdr.Es) + txID, err := Issuance.Entry. + ComposeCreate(context.Background(), FactomClient, ecEsAdr.Es) if err != nil { errLog.Fatal(err) } fmt.Println("Token Initialization Entry Submitted") - fmt.Println("Entry Hash: ", Issuance.Hash) + fmt.Println("Entry Hash: ", Issuance.Entry.Hash) fmt.Println("Factom Tx ID:", txID) + fmt.Println() return } @@ -367,14 +380,12 @@ func printCurl(entry factom.Entry, es factom.EsAddress) error { } vrbLog.Println("Curl commands:") - commitHex, _ := factom.Bytes(commit).MarshalJSON() - fmt.Printf(`curl -X POST --data-binary '{"jsonrpc": "2.0", "id": 0, "method": "%v", "params":{"message":%v}}' -H 'content-type:text/plain;' %v/v2`, - commitMethod, string(commitHex), FactomClient.FactomdServer) + fmt.Printf(`curl -X POST --data-binary '{"jsonrpc":"2.0","id":0,"method":%q,"params":{"message":%q}}' -H 'content-type:text/plain;' %v`, + commitMethod, factom.Bytes(commit), FactomClient.FactomdServer) fmt.Println() - revealHex, _ := factom.Bytes(reveal).MarshalJSON() - fmt.Printf(`curl -X POST --data-binary '{"jsonrpc": "2.0", "id": 0, "method": "%v", "params":{"entry":%v}}' -H 'content-type:text/plain;' %v/v2`, - revealMethod, string(revealHex), FactomClient.FactomdServer) + fmt.Printf(`curl -X POST --data-binary '{"jsonrpc":"2.0","id": 0,"method":%q,"params":{"entry":%q}}' -H 'content-type:text/plain;' %v`, + revealMethod, factom.Bytes(reveal), FactomClient.FactomdServer) fmt.Println() return nil } diff --git a/cli/main.go b/cli/main.go index 95f99e7..7e71c5e 100644 --- a/cli/main.go +++ b/cli/main.go @@ -22,11 +22,9 @@ package main -import "github.com/Factom-Asset-Tokens/fatd/cli/cmd" - func main() { - if cmd.Complete() { + if Complete() { return } - cmd.Execute() + Execute() } diff --git a/cli/cmd/predict.go b/cli/predict.go similarity index 62% rename from cli/cmd/predict.go rename to cli/predict.go index d540518..a163471 100644 --- a/cli/cmd/predict.go +++ b/cli/predict.go @@ -20,22 +20,22 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "log" "os" "strings" "time" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/srv" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" "github.com/posener/complete" ) var logErr = func(_ ...interface{}) {} -// parseAPIFlags parses func parseAPIFlags() error { args := strings.Fields(os.Getenv("COMP_LINE"))[1:] if err := apiFlags.Parse(args); err != nil { @@ -56,38 +56,6 @@ func parseAPIFlags() error { return nil } -var PredictFAAddresses complete.PredictFunc = func(args complete.Args) []string { - if len(args.Last) > 52 { - return nil - } - if err := parseAPIFlags(); err != nil { - return nil - } - adrs, err := FactomClient.GetFAAddresses() - if err != nil { - logErr(err) - return nil - } - completed := make(map[factom.FAAddress]struct{}, len(args.Completed)-1) - for _, arg := range args.Completed[1:] { - var adr factom.FAAddress - if adr.Set(arg) != nil { - continue - } - completed[adr] = struct{}{} - } - adrStrs := make([]string, len(adrs)-len(completed)) - var i int - for _, adr := range adrs { - if _, ok := completed[adr]; ok { - continue - } - adrStrs[i] = adr.String() - i++ - } - return adrStrs -} - func PredictAppend(predict complete.PredictFunc, suffix string) complete.PredictFunc { return func(args complete.Args) []string { predictions := predict(args) @@ -98,38 +66,45 @@ func PredictAppend(predict complete.PredictFunc, suffix string) complete.Predict } } -var PredictECAddresses complete.PredictFunc = func(args complete.Args) []string { - if len(args.Last) > 52 { - return nil - } - if err := parseAPIFlags(); err != nil { - return nil - } - adrs, err := FactomClient.GetECAddresses() - if err != nil { - logErr(err) - return nil - } - completed := make(map[factom.ECAddress]struct{}, len(args.Completed)-1) - for _, arg := range args.Completed[1:] { - var adr factom.ECAddress - if adr.Set(arg) != nil { - continue +func PredictAddressesFunc(fa bool) complete.PredictFunc { + return func(args complete.Args) []string { + // Check if the argument we are completing already exceeds the + // length of an address. + if len(args.Last) > 52 { + return nil } - completed[adr] = struct{}{} - } - adrStrs := make([]string, len(adrs)-len(completed)) - var i int - for _, adr := range adrs { - if _, ok := completed[adr]; ok { - continue + + if err := parseAPIFlags(); err != nil { + return nil } - adrStrs[i] = adr.String() - i++ + + fss, ess, err := FactomClient.GetPrivateAddresses(context.Background()) + if err != nil { + logErr(err) + return nil + } + + // Return only the public addresses that we need. + var adrStrs []string + if fa { + adrStrs = make([]string, len(fss)) + for i, fs := range fss { + adrStrs[i] = fs.FAAddress().String() + } + } else { + adrStrs = make([]string, len(ess)) + for i, es := range ess { + adrStrs[i] = es.ECAddress().String() + } + } + return adrStrs } - return adrStrs + } +var PredictFAAddresses = PredictAddressesFunc(true) +var PredictECAddresses = PredictAddressesFunc(false) + var PredictChainIDs complete.PredictFunc = func(args complete.Args) []string { if len(args.Last) > 64 { return nil @@ -137,8 +112,9 @@ var PredictChainIDs complete.PredictFunc = func(args complete.Args) []string { if err := parseAPIFlags(); err != nil { return nil } - var chains []srv.ParamsToken - if err := FATClient.Request("get-daemon-tokens", nil, &chains); err != nil { + var chains []api.ParamsToken + if err := FATClient.Request(context.Background(), + "get-daemon-tokens", nil, &chains); err != nil { logErr(err) return nil } diff --git a/cli/cmd/root.go b/cli/root.go similarity index 94% rename from cli/cmd/root.go rename to cli/root.go index 799f29f..ba93834 100644 --- a/cli/cmd/root.go +++ b/cli/root.go @@ -20,9 +20,10 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "fmt" "io/ioutil" "log" @@ -30,10 +31,10 @@ import ( "strings" "time" - jrpc "github.com/AdamSLevy/jsonrpc2/v11" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/AdamSLevy/jsonrpc2/v12" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/srv" homedir "github.com/mitchellh/go-homedir" "github.com/posener/complete" "github.com/spf13/cobra" @@ -65,7 +66,7 @@ func initClients() { // Use --debugwalletd explicitly to debug wallet API calls. } - for _, client := range []*jrpc.Client{ + for _, client := range []*jsonrpc2.Client{ &FATClient.Client, &FactomClient.Factomd, &FactomClient.Walletd, @@ -107,7 +108,7 @@ var ( Verbose bool cfgFile string - FATClient = srv.NewClient() + FATClient = api.NewClient() FactomClient = factom.NewClient() Debug bool @@ -115,7 +116,7 @@ var ( Version bool - paramsToken = srv.ParamsToken{ + paramsToken = api.ParamsToken{ ChainID: new(factom.Bytes32), IssuerChainID: new(factom.Bytes32)} NameIDs []factom.Bytes @@ -126,11 +127,11 @@ var apiFlags = func() *flag.FlagSet { flags.ParseErrorsWhitelist.UnknownFlags = true flags.StringVarP(&FATClient.FatdServer, "fatd", "d", - "localhost:8078", "scheme://host:port for fatd") + "http://localhost:8078", "scheme://host:port for fatd") flags.StringVarP(&FactomClient.FactomdServer, "factomd", "s", - "localhost:8088", "scheme://host:port for factomd") + factom.FactomdDefault, "scheme://host:port for factomd") flags.StringVarP(&FactomClient.WalletdServer, "walletd", "w", - "localhost:8089", "scheme://host:port for factom-walletd") + factom.WalletdDefault, "scheme://host:port for factom-walletd") flags.StringVar(&FATClient.User, "fatduser", "", "Basic HTTP Auth User for fatd") @@ -273,6 +274,8 @@ CLI Completion "Token ID of a FAT chain") flags.VarPF(paramsToken.IssuerChainID, "identity", "I", "Issuer Identity Chain ID of a FAT chain").DefValue = "" + flags.BoolVarP(¶msToken.IncludePending, "includepending", "P", false, + "Include pending transactions") generateCmplFlags(cmd, rootCmplCmd.Flags) return cmd @@ -288,6 +291,8 @@ var apiCmplFlags = complete.Flags{ var tokenCmplFlags = complete.Flags{ "--chainid": PredictChainIDs, "-C": PredictChainIDs, + "--pending": complete.PredictNothing, + "-P": complete.PredictNothing, } func validateRunCompletionFlags(cmd *cobra.Command, _ []string) error { @@ -395,8 +400,9 @@ You must re-open your shell before completion changes take effect.`[1:]) func printVersions() { fmt.Printf("fat-cli: %v\n", Revision) vrbLog.Println("Fetching fatd properties...") - var properties srv.ResultGetDaemonProperties - if err := FATClient.Request("get-daemon-properties", nil, &properties); err != nil { + var properties api.ResultGetDaemonProperties + if err := FATClient.Request(context.Background(), + "get-daemon-properties", nil, &properties); err != nil { errLog.Fatal(err) } fmt.Printf("fatd: %v\n", properties.FatdVersion) @@ -430,8 +436,8 @@ func validateChainIDFlags(cmd *cobra.Command, _ []string) error { return nil } func initChainID() { - NameIDs = fat.NameIDs(paramsToken.TokenID, *paramsToken.IssuerChainID) - *paramsToken.ChainID = factom.ChainID(NameIDs) + NameIDs = fat.NameIDs(paramsToken.TokenID, paramsToken.IssuerChainID) + *paramsToken.ChainID = factom.ComputeChainID(NameIDs) vrbLog.Println("Token Chain ID:", paramsToken.ChainID) } diff --git a/cli/cmd/transact.go b/cli/transact.go similarity index 83% rename from cli/cmd/transact.go rename to cli/transact.go index 23e11ba..2c1bca4 100644 --- a/cli/cmd/transact.go +++ b/cli/transact.go @@ -20,20 +20,20 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( + "context" "encoding/json" "fmt" "math" - "github.com/Factom-Asset-Tokens/fatd/factom" + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat0" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/Factom-Asset-Tokens/fatd/srv" - - jrpc "github.com/AdamSLevy/jsonrpc2/v11" + "github.com/Factom-Asset-Tokens/fatd/fat0" + "github.com/Factom-Asset-Tokens/fatd/fat1" "github.com/posener/complete" "github.com/spf13/cobra" ) @@ -200,9 +200,9 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { if !ok { var err error vrbLog.Println("Fetching secret address...", fa) - fs, err = fa.GetFsAddress(FactomClient) + fs, err = fa.GetFsAddress(context.Background(), FactomClient) if err != nil { - if err, ok := err.(jrpc.Error); ok { + if err, ok := err.(jsonrpc2.Error); ok { errLog.Fatal(err.Data, fa) } errLog.Fatal(err) @@ -218,41 +218,44 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { fat0Tx.Inputs[fat.Coinbase()] = fat0Tx.Outputs.Sum() case fat1.Type: fat1Tx.Inputs = make(fat1.AddressNFTokensMap, 1) - fat1Tx.Inputs[fat.Coinbase()] = fat1Tx.Outputs.AllNFTokens() + tkns, err := fat1Tx.Outputs.AllNFTokens() + if err != nil { + errLog.Fatal(err) + } + fat1Tx.Inputs[fat.Coinbase()] = tkns } } vrbLog.Printf("Preparing %v Transaction Entry...", cmdType) var tx interface { - Sign(...factom.RCDPrivateKey) - MarshalEntry() error - Cost() (int8, error) + Sign(...factom.RCDPrivateKey) (factom.Entry, error) } switch cmdType { case fat0.Type: - fat0Tx.ChainID = paramsToken.ChainID + fat0Tx.Entry.ChainID = paramsToken.ChainID fat0Tx.Metadata = metadata tx = &fat0Tx case fat1.Type: - fat1Tx.ChainID = paramsToken.ChainID + fat1Tx.Entry.ChainID = paramsToken.ChainID fat1Tx.Metadata = metadata tx = &fat1Tx } - if err := tx.MarshalEntry(); err != nil { + entry, err := tx.Sign(signingSet...) + if err != nil { errLog.Fatal(err) } - vrbLog.Println("Transaction Entry Content: ", tx) - tx.Sign(signingSet...) - cost, err := tx.Cost() + cost, err := entry.Cost() if err != nil { errLog.Fatal(err) } + vrbLog.Println("Transaction Entry Content: ", string(entry.Content)) if !force { vrbLog.Println("Checking token chain status...") - params := srv.ParamsToken{ChainID: paramsToken.ChainID} - var stats srv.ResultGetStats - if err := FATClient.Request("get-stats", params, &stats); err != nil { + params := api.ParamsToken{ChainID: paramsToken.ChainID} + var stats api.ResultGetStats + if err := FATClient.Request(context.Background(), + "get-stats", params, &stats); err != nil { errLog.Fatal(err) } // Verify we are using the right command for this token type. @@ -262,12 +265,13 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { } if inputSet { - paramsGetBalance := srv.ParamsGetBalance{ParamsToken: params} + paramsGetBalance := api.ParamsGetBalance{ParamsToken: params} for _, adr := range inputAdrs { vrbLog.Println("Checking FAT Token balance...", adr) paramsGetBalance.Address = &adr var balance uint64 - if err := FATClient.Request("get-balance", + if err := FATClient.Request(context.Background(), + "get-balance", paramsGetBalance, &balance); err != nil { errLog.Fatal(err) } @@ -286,22 +290,20 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { } } if inputSet && cmdType == fat1.Type { - params := srv.ParamsGetNFBalance{ParamsToken: params} + params := api.ParamsGetNFBalance{ParamsToken: params} params.Limit = math.MaxUint64 for _, adr := range inputAdrs { vrbLog.Println("Checking FAT NF Token ownership...", adr) params.Address = &adr var balance fat1.NFTokens - if err := FATClient.Request("get-nf-balance", - params, &balance); err != nil { + if err := FATClient.Request(context.Background(), + "get-nf-balance", params, &balance); err != nil { errLog.Fatal(err) } - if err := balance.ContainsAll(fat1Tx.Inputs[adr]); err != nil { - tknID := fat1.NFTokenID( - err.(fat1.ErrorMissingNFTokenID)) - errLog.Fatalf( - "--input %v:%v does not own NFTokenID %v", - adr, addressValueStrMap[adr], tknID) + if err := balance.ContainsAll( + fat1Tx.Inputs[adr]); err != nil { + errLog.Fatalf("--input %v:%v balance: %v", + adr, addressValueStrMap[adr], err) } } } @@ -316,7 +318,7 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { case fat0.Type: issuing = fat0Tx.Inputs.Sum() case fat1.Type: - issuing = uint64(len(fat1Tx.Inputs.AllNFTokens())) + issuing = fat1Tx.Inputs.Sum() } issued := stats.CirculatingSupply + stats.Burned if stats.Issuance.Supply != -1 && @@ -325,16 +327,21 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { "invalid coinbase transaction: exceeds max supply") } if cmdType == fat1.Type { - params := srv.ParamsGetNFToken{ParamsToken: params} - for tknID := range fat1Tx.Inputs.AllNFTokens() { + params := api.ParamsGetNFToken{ParamsToken: params} + tkns, err := fat1Tx.Inputs.AllNFTokens() + if err != nil { + errLog.Fatal(err) + } + for tknID := range tkns { params.NFTokenID = &tknID - err := FATClient.Request("get-nf-token", params, nil) + err := FATClient.Request(context.Background(), + "get-nf-token", params, nil) if err == nil { errLog.Fatalf("invalid coinbase transaction: NFTokenID (%v) already exists", tknID) } - rpcErr, _ := err.(jrpc.Error) - if rpcErr.Code != srv.ErrorTokenNotFound.Code { + rpcErr, _ := err.(jsonrpc2.Error) + if rpcErr.Code != api.ErrorTokenNotFound.Code { errLog.Fatal(err) } } @@ -346,14 +353,6 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { vrbLog.Println() } - var entry factom.Entry - switch cmdType { - case fat0.Type: - entry = fat0Tx.Entry.Entry - case fat1.Type: - entry = fat1Tx.Entry.Entry - } - entry.ChainID = paramsToken.ChainID if curl { if err := printCurl(entry, ecEsAdr.Es); err != nil { errLog.Fatal(err) @@ -363,12 +362,15 @@ func validateTransactFlags(cmd *cobra.Command, args []string) error { vrbLog.Printf("Submitting the %v Transaction Entry to the Factom blockchain...", cmdType) - txID, err := entry.ComposeCreate(FactomClient, ecEsAdr.Es) + txID, err := entry.ComposeCreate(context.Background(), FactomClient, ecEsAdr.Es) if err != nil { errLog.Fatal(err) } - fmt.Printf("%v Transaction Entry Created: %v\n", cmdType, entry.Hash) - fmt.Printf("Chain ID: %v\n", entry.ChainID) + fmt.Println() + fmt.Printf("%v Transaction Entry Created\n", cmdType) + fmt.Printf("Chain ID: %v\n", entry.ChainID) + fmt.Printf("Entry Hash: %v\n", entry.Hash) fmt.Printf("Factom Tx ID: %v\n", txID) + fmt.Println() return nil } diff --git a/cli/cmd/transactfat0.go b/cli/transactfat0.go similarity index 96% rename from cli/cmd/transactfat0.go rename to cli/transactfat0.go index 49a7fcb..234ba21 100644 --- a/cli/cmd/transactfat0.go +++ b/cli/transactfat0.go @@ -20,17 +20,16 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( "fmt" "strconv" "strings" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat0" - + "github.com/Factom-Asset-Tokens/fatd/fat0" "github.com/posener/complete" "github.com/spf13/cobra" ) @@ -129,7 +128,7 @@ func (m AddressAmountMap) set(data string) error { if err := fa.Set(adrStr); err != nil { // Not FA, try FsAddress... if err := fs.Set(adrStr); err != nil { - return fmt.Errorf("invalid address: %v", err) + return fmt.Errorf("invalid address: %w", err) } fa = fs.FAAddress() if fa != fat.Coinbase() { @@ -145,7 +144,7 @@ func (m AddressAmountMap) set(data string) error { // Parse amount amount, err := parsePositiveInt(amountStr) if err != nil { - return fmt.Errorf("invalid amount: %v", err) + return fmt.Errorf("invalid amount: %w", err) } m[fa] = amount addressValueStrMap[fa] = amountStr diff --git a/cli/cmd/transactfat1.go b/cli/transactfat1.go similarity index 73% rename from cli/cmd/transactfat1.go rename to cli/transactfat1.go index 7ab1e74..59391a2 100644 --- a/cli/cmd/transactfat1.go +++ b/cli/transactfat1.go @@ -20,16 +20,15 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package cmd +package main import ( "fmt" "strings" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - + "github.com/Factom-Asset-Tokens/fatd/fat1" "github.com/posener/complete" "github.com/spf13/cobra" ) @@ -129,7 +128,7 @@ func (m AddressNFTokensMap) set(data string) error { if err := fa.Set(adrStr); err != nil { // Not FA, try FsAddress... if err := fs.Set(adrStr); err != nil { - return fmt.Errorf("invalid address: %v", err) + return fmt.Errorf("invalid address: %w", err) } fa = fs.FAAddress() if fa != fat.Coinbase() { @@ -148,7 +147,7 @@ func (m AddressNFTokensMap) set(data string) error { return err } - m[fa] = fat1.NFTokens(tkns) + m[fa] = tkns.NFTokens addressValueStrMap[fa] = tknIDsStr return nil } @@ -159,69 +158,11 @@ func (AddressNFTokensMap) Type() string { return ":[,-]" } -type NFTokens fat1.NFTokens +type NFTokens struct{ fat1.NFTokens } func (tkns *NFTokens) Set(adrAmtStr string) error { - if *tkns == nil { - *tkns = make(NFTokens) - } - return tkns.set(adrAmtStr) -} -func (tkns NFTokens) set(data string) error { - if len(data) < 2 || data[0] != '[' || data[len(data)-1] != ']' { - return fmt.Errorf("invalid NFTokenIDs format") - } - data = data[1 : len(data)-1] // Trim '[' and ']' - - // Split NFTokenIDs or NFTokenIDRanges on ',' - tknIDStrs := strings.Split(data, ",") - for _, tknIDStr := range tknIDStrs { - var tknIDs fat1.NFTokensSetter - tknRangeStrs := strings.Split(tknIDStr, "-") - switch len(tknRangeStrs) { - case 1: - // Parse single NFToken - tknID, err := parseNFTokenID(tknIDStr) - if err != nil { - return err - } - tknIDs = tknID - case 2: - minMax := make([]fat1.NFTokenID, 2) - for i, tknIDStr := range tknRangeStrs { - if len(tknIDStr) == 0 { - return fmt.Errorf("invalid NFTokenIDRange format: %v", - tknIDStr) - } - tknID, err := parseNFTokenID(tknIDStr) - if err != nil { - return err - } - minMax[i] = tknID - } - if minMax[0] > minMax[1] { - return fmt.Errorf("invalid NFTokenIDRange: %v > %v", - minMax[0], minMax[1]) - } - tknIDs = fat1.NewNFTokenIDRange(minMax...) - default: - return fmt.Errorf("invalid NFTokenIDRange format: %v", tknIDStr) - } - // Set all NFTokenIDs to the NFTokens map. - if err := fat1.NFTokens(tkns).Set(tknIDs); err != nil { - return fmt.Errorf("invalid NFTokens: %v", err) - } - } - return nil -} -func (tkns NFTokens) String() string { - return fmt.Sprintf("%v", fat1.NFTokens(tkns)) -} - -func parseNFTokenID(tknIDStr string) (fat1.NFTokenID, error) { - tknID, err := parsePositiveInt(tknIDStr) - if err != nil { - return 0, fmt.Errorf("invalid NFTokenID: %v", err) + if tkns.NFTokens == nil { + tkns.NFTokens = make(fat1.NFTokens) } - return fat1.NFTokenID(tknID), nil + return tkns.UnmarshalText([]byte(adrAmtStr)) } diff --git a/docs/ISSUING.md b/docs/ISSUING.md index 76b28d8..f9d3fa7 100644 --- a/docs/ISSUING.md +++ b/docs/ISSUING.md @@ -24,7 +24,7 @@ Level 4: Root Chain : Management Chain : -While creating the Identity you would have created an EC Address, save the public and private key. You see the private key once you export the address. +While creating the Identity you would have created an EC Address, save the public and private key. You see the private key once you export the address. EC Address : EC Address PK : @@ -36,85 +36,145 @@ Now that you have all this done, you're ready to initialize your token. ### Initialize a token Run the following command: -```bash -fat-cli -identity -tokenid issue -ecpub -name -sk1 -supply -symbol -type <"FAT-0" | "FAT-1> ``` - -The token symbol can be up to 4 letters. -Once this transaction completes - instantaneously on a private chain, about 10 minutes on public testnet, you need to run the same command again. - -The first time you run it is to create the chain for the new token, you should see an output like this: -``` -Created Token Chain -Token Chain ID: d769a40522998f10ed82e8cd96f875d3163742efa07d973d799a507246210cb7 -First Entry Hash: 5b11d18d1d17ebdeffc2f96a50dc1e9e24c171df6f9e7aaca46186ff40fdad48 -Factom TxID: ac42be15931ed95b288e13696a0224c4d172fed737b84d6e6398475cea5c74f1 -You must wait until the Token Chain is created before issuing the token. -This can take up to 10 minutes. -``` - -The second time is to create the issuance entry, this will actually issue the tokens. You should see an output like this: -``` -Created Issuance Entry -Token Chain ID: d769a40522998f10ed82e8cd96f875d3163742efa07d973d799a507246210cb7 -Issuance Entry Hash: 44e6994c12315244d2920902273bbbfb000eed7a64aa55da30902c280131998f -Factom TxID: f93fe0e6056f9ca0b5eabceb9af3880a93b6861ddb666a0b3cec21a9d54fff8e +$ fat-cli issue -v --sk1 \ + --ecadr EC3emTZegtoGuPz3MRA4uC8SU6Up52abqQUEKqW44TppGGAc4Vrq \ + --tokenid "SPARTA" \ + --identity 8888888de45074fb3505cfdc942f80f4c9ef1ddd5c4633cd21a940288ffc89f3 \ + --type FAT-0 \ + --symbol "SPT" \ + --precision 10 \ + --supply 3000000000000 \ + --metadata '{"fight":true}' +Fetching secret address... EC3emTZegtoGuPz3MRA4uC8SU6Up52abqQUEKqW44TppGGAc4Vrq +Token Chain ID: 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c +Preparing Chain Creation Entry... +Preparing and signing Token Initialization Entry... +Checking chain existence... +Checking token chain status... +Fetching Identity Chain... +Verifying SK1 Key... +Checking EC balance... +New chain creation cost: 11 EC +Token Initialization Entry cost: 1 EC + +Submitting the Chain Creation Entry to the Factom blockchain... +Chain Creation Entry Submitted +Chain ID: 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c +Entry Hash: ca6a22133e5ad6c96afd26567b52cc8437f7507f11833b5607cf532487fa6376 +Factom Tx ID: eb9db70e8e854ff6ad7abc51d42a589c5c922345caf96508c0e2f80dd44f6e02 + +Submitting the Token Initialization Entry to the Factom blockchain... +Token Initialization Entry Submitted +Entry Hash: 271ade6467d44937406fe934e7d68cc44a18b61778cdd4a478b4353db4caaa5c +Factom Tx ID: 6df52ff302c6a03734ac94c0cacacabcbc9461ade1ff016e9d9e5866a11d0abb ``` -You can find these in your factomd control panel (port 8090) +The token symbol can be up to 4 letters. Once this transaction completes - +instantaneously on a private chain, about 10 minutes on public testnet, you +need to run the same command again. -### Distribute tokens +The command will create the chain if it does not exist, and submit the issuance +entry. See `fat-cli help issue` for more details. -Now that the token have been initialized, you actually need to distribute the tokens to Factom addresses. A `coinbase` transaction will be used to do so. You don't need to distribute the entire token supply in one coinbase transaction, you can submit multiple transactions over time to distribute your token supply. +After about 10 minutes, the chain will be tracked by `fatd`. Information about +the chain can then be queried. -The coinbase commmand looks like this: -```bash -fat-cli -chainid -sk1 -coinbase -output ... -ecpub +``` +$ fat-cli get chains 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c + +Chain ID: 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c +Issuer Identity: 8888888de45074fb3505cfdc942f80f4c9ef1ddd5c4633cd21a940288ffc89f3 +Issuance Entry: 7f8ab15b40aaece738d6afa1053ba070a0daa28052be28e777a2ee6258bec569 +Token ID: SPARTA +Type: FAT-0 +Symbol: "SPT" +Precision: 10 +Supply: 3000000000000 +Circulating Supply: 0 +Burned: 0 +Number of Transactions: 0 +Issuance Timestamp: 2019-10-24 17:00:00 -0800 AKDT ``` -There can be many outputs. You may want to supply tokens to multiple accounts in the coinbase command, just remember the sum of the output amounts should equal `AMOUNT TO DISTRIBUTE`. In the case of FAT-1 tokens the inputs and outputs must contain the same set of NF token IDs. +### Distribute tokens -An entry credit address needs to be provided so that fatd can pay for the transaction. +Now that the token has been initialized, you need to initially distribute some +tokens to Factom addresses using a `coinbase` transaction. You don't need to +distribute the entire token supply in one coinbase transaction, you can submit +multiple coinbase transactions over time to distribute your token supply. -Using the FA Addresses you've created, run this: +Coinbase transactions are signed with the same SK1 key that the Issuance entry +uses. The following command distributes the entire supply of the newly created +token to two addresses. ``` -fat-cli -chainid d769a40522998f10ed82e8cd96f875d3163742efa07d973d799a507246210cb7 transact -sk1 -coinbase 1000 -output FA2jK2HcLnRdS94dEcU27rF3meoJfpUcZPSinpb7AwQvPRY6RL1Q:500 -output FA3TMQHrCrmLa4F9t442U3Ab3R9sM1gThYMDoygPEVtxrbHtFRtg:500 +$ fat-cli transact fat0 \ + --chainid 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c \ + --sk1 \ + --output FA2kEkNgQ5RMNx5Y14HRQa4X8czeZqg74AJykR8f3jx4Cbk26gcM:1500000000000 \ + --output FA2mnS2QfXNQjdq6jJKxUxDnwPXzLpxivYDrYMtLgmRDbrxZztY5:1500000000000 \ + --ecadr EC3emTZegtoGuPz3MRA4uC8SU6Up52abqQUEKqW44TppGGAc4Vrq + +FAT-0 Transaction Entry Created +Entry Hash: dd2afeeb99232893caa52264cc80f094f78ebfed47766fd77ea99e8987e54a0f +Chain ID: 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c +Factom Tx ID: 693060a967a1ceee0353e35ebf66dd30f0ec4fd76a65eac228a5f2a10aeb448b ``` -You may see an error like this: `jsonrpc2.Error{Code:-32806, Message:"No Entry Credits", Data:"not configured with entry credits"}` - -If you do, simply add the `-ecpub` flag to the command and it should work as expected. - -Great! Now that we have 2 FA addresses funded with your token, you're free to transact between other addresses. +See `fat-cli help transact` for more details. ### Send tokens -```bash -fat-cli -chainid transact -input -output -ecpub +``` +$ fat-cli transact fat0 \ + --chainid 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c \ + --input FA2kEkNgQ5RMNx5Y14HRQa4X8czeZqg74AJykR8f3jx4Cbk26gcM:5 + --output FA2mnS2QfXNQjdq6jJKxUxDnwPXzLpxivYDrYMtLgmRDbrxZztY5:5 \ + --ecadr EC3emTZegtoGuPz3MRA4uC8SU6Up52abqQUEKqW44TppGGAc4Vrq + +FAT-0 Transaction Entry Created +Chain ID: 56bba15293b1f24849a7c3205a30db5981022776bea711d217437a642e9e080c +Entry Hash: 5e170b84c076b71363a13075b40765617378db4960a7efc851658c97291010e4 +Factom Tx ID: 404f7a19c515f42d057cc0f8981651750069e4c612cb5a3963ef5a5a18c94844 ``` -An entry credit address needs to be provided so that fatd can pay for the transaction. +A normal transaction can have multiple inputs and multiple outputs. The private +key for any public address used as an `--input` will be fetched from +factom-walletd. -The `input` and `output` flags are used to indicate how the transaction is funded and who the funds should be dispersed to. +The sum of the inputs must equal the sum of the outputs. ## Potential Errors -* No entry credits/ not configured with entry credits. -This error is caused with fat-cli doesn't know how a transaction should be funded. There are two ways to solve this.
-1. Pass `-ecpub` with a funded EC address to all fat-cli commands that submit a transaction.
-OR
-2. Run/restart fat-cli with the flag `-ecpub `
-This will ensure that fat-cli knows how to pay for the transactions. +#### No entry credits/ not configured with entry credits. -* Unable to retrieve account balance -If you want to find out how many tokens your wallet has, you would run the following command:
-`fat-cli -chainid balance ` and you might run into the following error (most likely if you are on a private chain) -
-`jsonrpc2.Error{Code:-32800, Message:"Token Not Found", Data:"token may be invalid, or not yet issued or tracked"}` +This error is caused with fat-cli doesn't know how a transaction should be +funded. To solve this, pass `-ecpub` with a funded EC address to all fat-cli +commands that submit a transaction. -This happens when fatd starts scanning at a block height *much* higher than where your private chain is most likely at. To solve this, you want to restart fatd with the flag `-startscanheight 0`. fatd will scan through all the blocks until your private chain's max block height and look for all valid FAT transactions. Now you will be able to run the command with no error and see your token wallet balance. A good indicator of this being the case is your `fatd.db` folder will be empty. It *should* have a folder with the same name as you . - -*fatd will create fatd.db in the folder from which you run fatd. It is advised to run fatd from the same location each time you do it until a better implementation of this is released.* - -*Do not use `-startscanheight 0` the next time you run fatd. You could, but you'd just be wasting time. The next time it runs it'll run from the last blockheight so there's no need to rescan.* +#### Unable to retrieve account balance +If you want to find out how many tokens your wallet has, you would run the +following command: +``` +fat-cli get balance --chainid +``` +and you might run into the following error (most likely if you are on a private +chain) - `jsonrpc2.Error{Code:-32800, Message:"Token Not Found", Data:"token +may be invalid, or not yet issued or tracked"}` + +This happens when fatd starts scanning at a block height *much* higher than +where your private chain is most likely at. To solve this, you want to restart +fatd with the flag `-startscanheight 0`. fatd will scan through all the blocks +until your private chain's max block height and look for all valid FAT +transactions. Now you will be able to run the command with no error and see +your token wallet balance. A good indicator of this being the case is your +`fatd.db` folder will be empty. It *should* have a folder with the same name as +your . + +*By default, fatd will create its databases in your home folder: `~/.fatd/`* + +*Do not use `-startscanheight 0` the next time you run fatd. You could, but +you'd just be wasting time. The next time it runs it'll run from the last +blockheight so there's no need to rescan.* diff --git a/engine/engine.go b/engine/engine.go deleted file mode 100644 index bd431f8..0000000 --- a/engine/engine.go +++ /dev/null @@ -1,246 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Package engine manages syncing with the Factom blockchain and updating -// state. Start launches a number of goroutines: one to query for DBlocks -// sequentially, and a number of workers to concurrently process EBlocks within -// a DBlock and update state. If any runtime errors occur, engine finishes -// processing the current set of EBlocks and then exits. See Start for more -// details. -package engine - -import ( - "fmt" - "sync" - "time" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/flag" - _log "github.com/Factom-Asset-Tokens/fatd/log" - "github.com/Factom-Asset-Tokens/fatd/state" -) - -var ( - log _log.Log - c = flag.FactomClient -) - -const ( - scanInterval = 15 * time.Second -) - -// Start launches the main engine goroutine, which loads state and starts the -// worker goroutines. If stop is closed or if an error occurs, the engine will -// finish processing the current DBlock, cleanup and close state, all -// goroutines will exit, and done will be closed. If the done channel is closed -// before the stop channel is closed, an error occurred. -func Start(stop <-chan struct{}) (done <-chan struct{}) { - _done := make(chan struct{}) - go engine(stop, _done) - return _done -} - -func engine(stop <-chan struct{}, done chan struct{}) { - // Ensure done is always closed exactly once. - var once sync.Once - exit := func() { once.Do(func() { close(done) }) } - defer exit() - - log = _log.New("engine") - - if err := state.Load(); err != nil { - log.Error(err) - return - } - // Set up sync and factom heights... - setSyncHeight(state.SavedHeight) - if err := updateFactomHeight(); err != nil { - log.Error(err) - return - } - - // Guard against syncing against a network with an earlier blockheight. - if syncHeight > factomHeight { - log.Errorf("Saved height (%v) > Factom height (%v)", - syncHeight, factomHeight) - return - } - if flag.StartScanHeight > -1 { // If -startscanheight was set... - if flag.StartScanHeight > int32(factomHeight) { - log.Errorf("-startscanheight %v > Factom height (%v)", - flag.StartScanHeight, factomHeight) - return - } - // Warn if we are skipping blocks. - if flag.StartScanHeight > int32(syncHeight)+1 { - log.Warnf("-startscanheight %v skips over %v blocks from the last saved last saved block height which will very likely result in a corrupted database.", - flag.StartScanHeight, - flag.StartScanHeight-int32(syncHeight)-1) - } - // We start syncing at syncHeight+1, so subtract one. This - // overflows for 0 but it's OK as long as we don't rely on the - // value until the first scan loop. - setSyncHeight(uint32(flag.StartScanHeight - 1)) - } else if syncHeight == 0 { // else if the syncHeight has not been set... - const mainnetStart = 163180 - const testnetStart = 60000 - // This is a hacky, unreliable way to determine what network we - // are on. This needs to be replaced with using the actually - // Network ID. - if factomHeight > mainnetStart { - setSyncHeight(mainnetStart) // Set for mainnet - } else if factomHeight > testnetStart { - setSyncHeight(testnetStart) // Set for testnet - } else { - var zero uint32 // Avoid constant overflow compile error. - setSyncHeight(zero - 1) // Start scan at 0. - } - } - - wg := &sync.WaitGroup{} - eblocks := make(chan factom.EBlock) - // Ensure all workers exit and state is closed when we exit. - defer close(eblocks) - defer state.Close() - - // Launch workers - const numWorkers = 8 - for i := 0; i < numWorkers; i++ { - go func() { - for eb := range eblocks { // Read until close(eblocks) - if err := state.Process(eb); err != nil { - log.Errorf("ChainID(%v): %v", eb.ChainID, err) - exit() // Tell engine() to exit. - } - wg.Done() - } - }() - } - - log.Infof("Syncing from block %v to %v...", syncHeight+1, factomHeight) - var synced bool - var retries int64 - scanTicker := time.NewTicker(scanInterval) - for { - if !synced && syncHeight == factomHeight { - synced = true - log.Debugf("Synced to block %v...", syncHeight) - log.Infof("Synced.") - } - - // Process all new DBlocks sequentially... - for h := syncHeight + 1; h <= factomHeight; h++ { - // Get DBlock. - var dblock factom.DBlock - dblock.Header.Height = h - if err := dblock.Get(c); err != nil { - log.Errorf("%#v.Get(c): %v", dblock, err) - return - } - - // Queue all EBlocks for processing and wait. - wg.Add(len(dblock.EBlocks)) - for _, eb := range dblock.EBlocks { - eblocks <- eb - } - wg.Wait() // Wait for all EBlocks to be processed. - - // Check for process errors... - select { - case <-done: - // We cannot consider this DBlock completed. - return - default: - } - - // DBlock completed. - setSyncHeight(h) - if err := state.SaveHeight(h); err != nil { - log.Errorf("state.SaveHeight(%v): %v", h, err) - return - } - - // Check that we haven't been told to stop. - select { - case <-stop: - return - default: - } - - if flag.LogDebug && h%100 == 0 { - log.Debugf("Synced to block %v...", h) - } - } - - if synced { - // Wait until the next scan tick or we're told to stop. - select { - case <-scanTicker.C: - case <-stop: - return - } - } - - if err := updateFactomHeight(); err != nil { - log.Error(err) - if flag.FactomScanRetries > -1 && - retries >= flag.FactomScanRetries { - return - } - retries++ - log.Infof("Retrying in %v... (%v)", scanInterval, retries) - } else { - retries = 0 - } - } -} - -var ( - syncHeight, factomHeight uint32 - heightMtx = &sync.RWMutex{} -) - -// GetSyncStatus is a threadsafe way to get the sync height and current Factom -// Blockchain height. -func GetSyncStatus() (sync, current uint32) { - heightMtx.RLock() - defer heightMtx.RUnlock() - return syncHeight, factomHeight -} - -func setSyncHeight(sync uint32) { - heightMtx.Lock() - defer heightMtx.Unlock() - syncHeight = sync -} -func updateFactomHeight() error { - // Get the current Factom Blockchain height. - var heights factom.Heights - err := heights.Get(c) - if err != nil { - return fmt.Errorf("factom.Heights.Get(c): %v", err) - } - heightMtx.Lock() - defer heightMtx.Unlock() - factomHeight = heights.Entry - return nil -} diff --git a/factom/address.go b/factom/address.go deleted file mode 100644 index 881ae94..0000000 --- a/factom/address.go +++ /dev/null @@ -1,777 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "crypto/rand" - "crypto/sha256" - "database/sql" - "database/sql/driver" - "fmt" - - "golang.org/x/crypto/ed25519" -) - -// Notes: This file contains all types, interfaces, and methods related to -// Factom Addresses as specified by -// https://github.com/FactomProject/FactomDocs/blob/master/factomDataStructureDetails.md -// -// There are four Factom address types, forming two pairs: public and private -// Factoid addresses, and public and private Entry Credit addresses. All -// addresses are a 32 byte payload encoded using base58check with various -// prefixes. - -// Address is the interface implemented by the four address types: FAAddress, -// FsAddress, ECAddress, and EsAddress. -type Address interface { - // PrefixBytes returns the prefix bytes for the Address. - PrefixBytes() []byte - // PrefixString returns the encoded prefix string for the Address. - PrefixString() string - - // String encodes the address to a base58check string with the - // appropriate prefix. - String() string - // Payload returns the address as a byte array. - Payload() [sha256.Size]byte - - // PublicAddress returns the corresponding public address in an Address - // interface. Public addresses return themselves. Private addresses - // compute the public address. - PublicAddress() Address - // GetPrivateAddress returns the corresponding private address in a - // PrivateAddress interface. Public addresses query factom-walletd for - // the private address. Private addresses return themselves. - GetPrivateAddress(*Client) (PrivateAddress, error) - - // GetBalance returns the current balance for the address. - GetBalance(*Client) (uint64, error) - - // Remove queries factom-walletd to remove the public and private - // addresses from its database. - // WARNING: DESTRUCTIVE ACTION! LOSS OF KEYS AND FUNDS MAY RESULT! - Remove(*Client) error -} - -// PrivateAddress is the interface implemented by the two private address -// types: FsAddress, and EsAddress. -type PrivateAddress interface { - Address - - // PrivateKey returns the ed25519.PrivateKey which can be used for - // signing data. - PrivateKey() ed25519.PrivateKey - // PublicKey returns the ed25519.PublicKey which can be used for - // verifying signatures. - PublicKey() ed25519.PublicKey -} - -// FAAddress is a Public Factoid Address. -type FAAddress [sha256.Size]byte - -// FsAddress is the secret key to a FAAddress. -type FsAddress [sha256.Size]byte - -// ECAddress is a Public Entry Credit Address. -type ECAddress [sha256.Size]byte - -// EsAddress is the secret key to a ECAddress. -type EsAddress [sha256.Size]byte - -// Ensure that the Address and PrivateAddress interfaces are implemented. -var _ Address = FAAddress{} -var _ PrivateAddress = FsAddress{} -var _ Address = ECAddress{} -var _ PrivateAddress = EsAddress{} - -// Payload returns adr as a byte array. -func (adr FAAddress) Payload() [sha256.Size]byte { - return adr -} - -// Payload returns adr as a byte array. -func (adr FsAddress) Payload() [sha256.Size]byte { - return adr -} - -// Payload returns adr as a byte array. -func (adr ECAddress) Payload() [sha256.Size]byte { - return adr -} - -// Payload returns adr as a byte array. -func (adr EsAddress) Payload() [sha256.Size]byte { - return adr -} - -// payload returns adr as payload. This is syntactic sugar useful in other -// methods that leverage payload. -func (adr FAAddress) payload() payload { - return payload(adr) -} -func (adr FsAddress) payload() payload { - return payload(adr) -} -func (adr ECAddress) payload() payload { - return payload(adr) -} -func (adr EsAddress) payload() payload { - return payload(adr) -} - -// payloadPtr returns adr as *payload. This is syntactic sugar useful in other -// methods that leverage *payload. -func (adr *FAAddress) payloadPtr() *payload { - return (*payload)(adr) -} -func (adr *FsAddress) payloadPtr() *payload { - return (*payload)(adr) -} -func (adr *ECAddress) payloadPtr() *payload { - return (*payload)(adr) -} -func (adr *EsAddress) payloadPtr() *payload { - return (*payload)(adr) -} - -var ( - faPrefixBytes = [...]byte{0x5f, 0xb1} - fsPrefixBytes = [...]byte{0x64, 0x78} - ecPrefixBytes = [...]byte{0x59, 0x2a} - esPrefixBytes = [...]byte{0x5d, 0xb6} -) - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x5f, 0xb1}. -func (FAAddress) PrefixBytes() []byte { - prefix := faPrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x64, 0x78}. -func (FsAddress) PrefixBytes() []byte { - prefix := fsPrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x59, 0x2a}. -func (ECAddress) PrefixBytes() []byte { - prefix := ecPrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x5d, 0xb6}. -func (EsAddress) PrefixBytes() []byte { - prefix := esPrefixBytes - return prefix[:] -} - -const ( - faPrefixStr = "FA" - fsPrefixStr = "Fs" - ecPrefixStr = "EC" - esPrefixStr = "Es" -) - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "FA". -func (FAAddress) PrefixString() string { - return faPrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "Fs". -func (FsAddress) PrefixString() string { - return fsPrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "EC". -func (ECAddress) PrefixString() string { - return ecPrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "Es". -func (EsAddress) PrefixString() string { - return esPrefixStr -} - -// String encodes adr into its human readable form: a base58check string with -// adr.PrefixBytes(). -func (adr FAAddress) String() string { - return adr.payload().StringPrefix(adr.PrefixBytes()) -} - -// String encodes adr into its human readable form: a base58check string with -// adr.PrefixBytes(). -func (adr FsAddress) String() string { - return adr.payload().StringPrefix(adr.PrefixBytes()) -} - -// String encodes adr into its human readable form: a base58check string with -// adr.PrefixBytes(). -func (adr ECAddress) String() string { - return adr.payload().StringPrefix(adr.PrefixBytes()) -} - -// String encodes adr into its human readable form: a base58check string with -// adr.PrefixBytes(). -func (adr EsAddress) String() string { - return adr.payload().StringPrefix(adr.PrefixBytes()) -} - -// MarshalJSON encodes adr as a JSON string using adr.String(). -func (adr FAAddress) MarshalJSON() ([]byte, error) { - return adr.payload().MarshalJSONPrefix(adr.PrefixBytes()) -} - -// MarshalJSON encodes adr as a JSON string using adr.String(). -func (adr FsAddress) MarshalJSON() ([]byte, error) { - return adr.payload().MarshalJSONPrefix(adr.PrefixBytes()) -} - -// MarshalJSON encodes adr as a JSON string using adr.String(). -func (adr ECAddress) MarshalJSON() ([]byte, error) { - return adr.payload().MarshalJSONPrefix(adr.PrefixBytes()) -} - -// MarshalJSON encodes adr as a JSON string using adr.String(). -func (adr EsAddress) MarshalJSON() ([]byte, error) { - return adr.payload().MarshalJSONPrefix(adr.PrefixBytes()) -} - -const adrStrLen = 52 - -// NewAddress parses adrStr and returns the correct address type as an Address -// interface. This is useful when the address type isn't known prior to parsing -// adrStr. If the address type is known ahead of time, it is generally better -// to just use the appropriate concrete type. -func NewAddress(adrStr string) (Address, error) { - if len(adrStr) != adrStrLen { - return nil, fmt.Errorf("invalid length") - } - switch adrStr[:2] { - case FAAddress{}.PrefixString(): - return NewFAAddress(adrStr) - case FsAddress{}.PrefixString(): - return NewFsAddress(adrStr) - case ECAddress{}.PrefixString(): - return NewECAddress(adrStr) - case EsAddress{}.PrefixString(): - return NewEsAddress(adrStr) - default: - return nil, fmt.Errorf("unrecognized prefix") - } -} - -// NewPublicAddress parses adrStr and returns the correct address type as an -// Address interface. If adrStr is not a public address then an "invalid -// prefix" error is returned. This is useful when the address type isn't known -// prior to parsing adrStr, but must be a public address. If the address type -// is known ahead of time, it is generally better to just use the appropriate -// concrete type. -func NewPublicAddress(adrStr string) (Address, error) { - if len(adrStr) != adrStrLen { - return nil, fmt.Errorf("invalid length") - } - switch adrStr[:2] { - case FAAddress{}.PrefixString(): - return NewFAAddress(adrStr) - case ECAddress{}.PrefixString(): - return NewECAddress(adrStr) - case FsAddress{}.PrefixString(): - fallthrough - case EsAddress{}.PrefixString(): - return nil, fmt.Errorf("invalid prefix") - default: - return nil, fmt.Errorf("unrecognized prefix") - } -} - -// NewPrivateAddress parses adrStr and returns the correct address type as a -// PrivateAddress interface. If adrStr is not a private address then an -// "invalid prefix" error is returned. This is useful when the address type -// isn't known prior to parsing adrStr, but must be a private address. If the -// address type is known ahead of time, it is generally better to just use the -// appropriate concrete type. -func NewPrivateAddress(adrStr string) (PrivateAddress, error) { - if len(adrStr) != adrStrLen { - return nil, fmt.Errorf("invalid length") - } - switch adrStr[:2] { - case FsAddress{}.PrefixString(): - return NewFsAddress(adrStr) - case EsAddress{}.PrefixString(): - return NewEsAddress(adrStr) - case FAAddress{}.PrefixString(): - fallthrough - case ECAddress{}.PrefixString(): - return nil, fmt.Errorf("invalid prefix") - default: - return nil, fmt.Errorf("unrecognized prefix") - } -} - -// GenerateFsAddress generates a secure random private Factoid address using -// crypto/rand.Random as the source of randomness. -func GenerateFsAddress() (FsAddress, error) { - return generatePrivKey() -} - -// GenerateEsAddress generates a secure random private Entry Credit address -// using crypto/rand.Random as the source of randomness. -func GenerateEsAddress() (EsAddress, error) { - return generatePrivKey() -} -func generatePrivKey() (key [sha256.Size]byte, err error) { - var priv ed25519.PrivateKey - if _, priv, err = ed25519.GenerateKey(rand.Reader); err != nil { - return - } - copy(key[:], priv) - return key, nil -} - -// NewFAAddress attempts to parse adrStr into a new FAAddress. -func NewFAAddress(adrStr string) (adr FAAddress, err error) { - err = adr.Set(adrStr) - return -} - -// NewFsAddress attempts to parse adrStr into a new FsAddress. -func NewFsAddress(adrStr string) (adr FsAddress, err error) { - err = adr.Set(adrStr) - return -} - -// NewECAddress attempts to parse adrStr into a new ECAddress. -func NewECAddress(adrStr string) (adr ECAddress, err error) { - err = adr.Set(adrStr) - return -} - -// NewEsAddress attempts to parse adrStr into a new EsAddress. -func NewEsAddress(adrStr string) (adr EsAddress, err error) { - err = adr.Set(adrStr) - return -} - -// Set attempts to parse adrStr into adr. -func (adr *FAAddress) Set(adrStr string) error { - return adr.payloadPtr().SetPrefix(adrStr, adr.PrefixString()) -} - -// Set attempts to parse adrStr into adr. -func (adr *FsAddress) Set(adrStr string) error { - return adr.payloadPtr().SetPrefix(adrStr, adr.PrefixString()) -} - -// Set attempts to parse adrStr into adr. -func (adr *ECAddress) Set(adrStr string) error { - return adr.payloadPtr().SetPrefix(adrStr, adr.PrefixString()) -} - -// Set attempts to parse adrStr into adr. -func (adr *EsAddress) Set(adrStr string) error { - return adr.payloadPtr().SetPrefix(adrStr, adr.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable public Factoid -// address into adr. -func (adr *FAAddress) UnmarshalJSON(data []byte) error { - return adr.payloadPtr().UnmarshalJSONPrefix(data, adr.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable secret Factoid -// address into adr. -func (adr *FsAddress) UnmarshalJSON(data []byte) error { - return adr.payloadPtr().UnmarshalJSONPrefix(data, adr.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable public Entry -// Credit address into adr. -func (adr *ECAddress) UnmarshalJSON(data []byte) error { - return adr.payloadPtr().UnmarshalJSONPrefix(data, adr.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable secret Entry -// Credit address into adr. -func (adr *EsAddress) UnmarshalJSON(data []byte) error { - return adr.payloadPtr().UnmarshalJSONPrefix(data, adr.PrefixString()) -} - -// GetPrivateAddress queries factom-walletd for the secret address -// corresponding to adr and returns it as a PrivateAddress. -func (adr FAAddress) GetPrivateAddress(c *Client) (PrivateAddress, error) { - return adr.GetFsAddress(c) -} - -// GetPrivateAddress returns adr as a PrivateAddress. -func (adr FsAddress) GetPrivateAddress(_ *Client) (PrivateAddress, error) { - return adr, nil -} - -// GetPrivateAddress queries factom-walletd for the secret address -// corresponding to adr and returns it as a PrivateAddress. -func (adr ECAddress) GetPrivateAddress(c *Client) (PrivateAddress, error) { - return adr.GetEsAddress(c) -} - -// GetPrivateAddress returns adr as a PrivateAddress. -func (adr EsAddress) GetPrivateAddress(_ *Client) (PrivateAddress, error) { - return adr, nil -} - -// GetFsAddress queries factom-walletd for the FsAddress corresponding to adr. -func (adr FAAddress) GetFsAddress(c *Client) (FsAddress, error) { - var privAdr FsAddress - err := c.GetAddress(adr, &privAdr) - return privAdr, err -} - -// GetEsAddress queries factom-walletd for the EsAddress corresponding to adr. -func (adr ECAddress) GetEsAddress(c *Client) (EsAddress, error) { - var privAdr EsAddress - err := c.GetAddress(adr, &privAdr) - return privAdr, err -} - -type walletAddress struct{ Address Address } - -// GetAddress queries factom-walletd for the privAdr corresponding to pubAdr. -// If the returned error is nil, then privAdr is now populated. Note that -// privAdr must be a pointer to a concrete type implementing PrivateAddress. -func (c *Client) GetAddress(pubAdr Address, privAdr PrivateAddress) error { - params := walletAddress{Address: pubAdr} - result := struct{ Secret PrivateAddress }{Secret: privAdr} - if err := c.WalletdRequest("address", params, &result); err != nil { - return err - } - return nil -} - -type walletAddressPublic struct{ Public string } -type walletAddressSecret struct{ Secret string } -type walletAddressesPublic struct{ Addresses []walletAddressPublic } -type walletAddressesSecret struct{ Addresses []walletAddressSecret } - -// GetAddresses queries factom-walletd for all public addresses. -func (c *Client) GetAddresses() ([]Address, error) { - var result walletAddressesPublic - if err := c.WalletdRequest("all-addresses", nil, &result); err != nil { - return nil, err - } - addresses := make([]Address, 0, len(result.Addresses)) - for _, adrStr := range result.Addresses { - adr, err := NewAddress(adrStr.Public) - if err != nil { - return nil, err - } - addresses = append(addresses, adr) - } - return addresses, nil -} - -// GetPrivateAddresses queries factom-walletd for all private addresses. -func (c *Client) GetPrivateAddresses() ([]PrivateAddress, error) { - var result walletAddressesSecret - if err := c.WalletdRequest("all-addresses", nil, &result); err != nil { - return nil, err - } - addresses := make([]PrivateAddress, 0, len(result.Addresses)) - for _, adrStr := range result.Addresses { - adr, err := NewPrivateAddress(adrStr.Secret) - if err != nil { - return nil, err - } - addresses = append(addresses, adr) - } - return addresses, nil -} - -// GetFAAddresses queries factom-walletd for all public Factoid addresses. -func (c *Client) GetFAAddresses() ([]FAAddress, error) { - var result walletAddressesPublic - if err := c.WalletdRequest("all-addresses", nil, &result); err != nil { - return nil, err - } - addresses := make([]FAAddress, 0, len(result.Addresses)) - for _, adrStr := range result.Addresses { - adr, err := NewFAAddress(adrStr.Public) - if err != nil { - continue - } - addresses = append(addresses, adr) - } - return addresses, nil -} - -// GetFsAddresses queries factom-walletd for all secret Factoid addresses. -func (c *Client) GetFsAddresses() ([]FsAddress, error) { - var result walletAddressesSecret - if err := c.WalletdRequest("all-addresses", nil, &result); err != nil { - return nil, err - } - addresses := make([]FsAddress, 0, len(result.Addresses)) - for _, adrStr := range result.Addresses { - adr, err := NewFsAddress(adrStr.Secret) - if err != nil { - continue - } - addresses = append(addresses, adr) - } - return addresses, nil -} - -// GetECAddresses queries factom-walletd for all public Entry Credit addresses. -func (c *Client) GetECAddresses() ([]ECAddress, error) { - var result walletAddressesPublic - if err := c.WalletdRequest("all-addresses", nil, &result); err != nil { - return nil, err - } - addresses := make([]ECAddress, 0, len(result.Addresses)) - for _, adrStr := range result.Addresses { - adr, err := NewECAddress(adrStr.Public) - if err != nil { - continue - } - addresses = append(addresses, adr) - } - return addresses, nil -} - -// GetEsAddresses queries factom-walletd for all secret Entry Credit addresses. -func (c *Client) GetEsAddresses() ([]EsAddress, error) { - var result walletAddressesSecret - if err := c.WalletdRequest("all-addresses", nil, &result); err != nil { - return nil, err - } - addresses := make([]EsAddress, 0, len(result.Addresses)) - for _, adrStr := range result.Addresses { - adr, err := NewEsAddress(adrStr.Secret) - if err != nil { - continue - } - addresses = append(addresses, adr) - } - return addresses, nil -} - -// Save adr with factom-walletd. -func (adr FsAddress) Save(c *Client) error { - return c.SavePrivateAddresses(adr) -} - -// Save adr with factom-walletd. -func (adr EsAddress) Save(c *Client) error { - return c.SavePrivateAddresses(adr) -} - -// SavePrivateAddresses saves many adrs with factom-walletd. -func (c *Client) SavePrivateAddresses(adrs ...PrivateAddress) error { - var params walletAddressesSecret - params.Addresses = make([]walletAddressSecret, len(adrs)) - for i, adr := range adrs { - params.Addresses[i].Secret = adr.String() - } - if err := c.WalletdRequest("import-addresses", params, nil); err != nil { - return err - } - return nil -} - -// GetBalance queries factomd for the Factoid Balance for adr. -func (adr FAAddress) GetBalance(c *Client) (uint64, error) { - return c.getBalance("factoid-balance", adr) -} - -// GetBalance queries factomd for the Factoid Balance for adr. -func (adr FsAddress) GetBalance(c *Client) (uint64, error) { - return adr.PublicAddress().GetBalance(c) -} - -// GetBalance queries factomd for the Entry Credit Balance for adr. -func (adr ECAddress) GetBalance(c *Client) (uint64, error) { - return c.getBalance("entry-credit-balance", adr) -} - -// GetBalance queries factomd for the Entry Credit Balance for adr. -func (adr EsAddress) GetBalance(c *Client) (uint64, error) { - return adr.PublicAddress().GetBalance(c) -} - -type getBalanceParams struct { - Adr Address `json:"address"` -} -type balanceResult struct{ Balance uint64 } - -func (c *Client) getBalance(method string, adr Address) (uint64, error) { - var result balanceResult - params := getBalanceParams{Adr: adr} - if err := c.FactomdRequest(method, params, &result); err != nil { - return 0, err - } - return result.Balance, nil -} - -// Remove adr from factom-walletd. WARNING: THIS IS DESTRUCTIVE. -func (adr FAAddress) Remove(c *Client) error { - return c.RemoveAddress(adr) -} - -// Remove adr from factom-walletd. WARNING: THIS IS DESTRUCTIVE. -func (adr FsAddress) Remove(c *Client) error { - return adr.PublicAddress().Remove(c) -} - -// Remove adr from factom-walletd. WARNING: THIS IS DESTRUCTIVE. -func (adr ECAddress) Remove(c *Client) error { - return c.RemoveAddress(adr) -} - -// Remove adr from factom-walletd. WARNING: THIS IS DESTRUCTIVE. -func (adr EsAddress) Remove(c *Client) error { - return adr.PublicAddress().Remove(c) -} - -// RemoveAddress removes adr from factom-walletd. WARNING: THIS IS DESTRUCTIVE. -func (c *Client) RemoveAddress(adr Address) error { - params := walletAddress{Address: adr.PublicAddress()} - if err := c.WalletdRequest("remove-address", params, nil); err != nil { - return err - } - return nil -} - -// PublicAddress returns adr as an Address. -func (adr FAAddress) PublicAddress() Address { - return adr -} - -// PublicAddress returns the FAAddress corresponding to adr as an Address. -func (adr FsAddress) PublicAddress() Address { - return adr.FAAddress() -} - -// PublicAddress returns adr as an Address. -func (adr ECAddress) PublicAddress() Address { - return adr -} - -// PublicAddress returns the ECAddress corresponding to adr as an Address. -func (adr EsAddress) PublicAddress() Address { - return adr.ECAddress() -} - -// FAAddress returns the FAAddress corresponding to adr. -func (adr FsAddress) FAAddress() FAAddress { - return adr.RCDHash() -} - -// ECAddress returns the ECAddress corresponding to adr. -func (adr EsAddress) ECAddress() (ec ECAddress) { - copy(ec[:], adr.PublicKey()) - return -} - -// RCDHash returns the RCD hash encoded in adr. -func (adr FAAddress) RCDHash() [sha256.Size]byte { - return adr -} - -// RCDHash computes the RCD hash corresponding to adr. -func (adr FsAddress) RCDHash() [sha256.Size]byte { - return sha256d(adr.RCD()) -} - -// sha256( sha256( data ) ) -func sha256d(data []byte) [sha256.Size]byte { - hash := sha256.Sum256(data) - return sha256.Sum256(hash[:]) -} - -const ( - // RCDType is the magic number identifying the currenctly accepted RCD. - RCDType byte = 0x01 - // RCDSize is the size of the RCD. - RCDSize = ed25519.PublicKeySize + 1 - // SignatureSize is the size of the ed25519 signatures. - SignatureSize = ed25519.SignatureSize -) - -// RCD computes the RCD for adr. -func (adr FsAddress) RCD() []byte { - return append([]byte{RCDType}, adr.PublicKey()[:]...) -} - -// PublicKey returns the ed25519.PublicKey for adr. -func (adr ECAddress) PublicKey() ed25519.PublicKey { - return adr[:] -} - -// PublicKey computes the ed25519.PublicKey for adr. -func (adr EsAddress) PublicKey() ed25519.PublicKey { - return adr.PrivateKey().Public().(ed25519.PublicKey) -} - -// PublicKey computes the ed25519.PublicKey for adr. -func (adr FsAddress) PublicKey() ed25519.PublicKey { - return adr.PrivateKey().Public().(ed25519.PublicKey) -} - -// PrivateKey returns the ed25519.PrivateKey for adr. -func (adr FsAddress) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(adr[:]) -} - -// PrivateKey returns the ed25519.PrivateKey for adr. -func (adr EsAddress) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(adr[:]) -} - -// Scan implements sql.Scanner for adr using Bytes32.Scan. The FAAddress type -// is not encoded and is assumed. -func (adr *FAAddress) Scan(v interface{}) error { - return (*Bytes32)(adr).Scan(v) -} - -// Value implements driver.Valuer for adr using Bytes32.Value. The FAAddress -// type is not encoded. -func (adr FAAddress) Value() (driver.Value, error) { - return (Bytes32)(adr).Value() -} - -var _ sql.Scanner = &FAAddress{} -var _ driver.Valuer = &FAAddress{} diff --git a/factom/address_test.go b/factom/address_test.go deleted file mode 100644 index 78804e5..0000000 --- a/factom/address_test.go +++ /dev/null @@ -1,382 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - // Valid Test Addresses generated by factom-walletd - // OBVIOUSLY NEVER USE THESE FOR ANY FUNDS! - FAAddressStr = "FA2PdKfzGP5XwoSbeW1k9QunCHwC8DY6d8xgEdfm57qfR31nTueb" - FsAddressStr = "Fs1ipNRjEXcWj8RUn1GRLMJYVoPFBL1yw9rn6sCxWGcxciC4HdPd" - ECAddressStr = "EC2Pawhv7uAiKFQeLgaqfRhzk5o9uPVY8Ehjh8DnLXENosvYTT26" - EsAddressStr = "Es2tFRhAqHnydaygVAR6zbpWTQXUDaXy1JHWJugQXnYavS8ssQQE" -) - -type addressUnmarshalJSONTest struct { - Name string - Adr Address - ExpAdr Address - Data string - Err string -} - -var addressUnmarshalJSONTests = []addressUnmarshalJSONTest{{ - Name: "valid FA", - Data: fmt.Sprintf("%q", FAAddressStr), - Adr: new(FAAddress), - ExpAdr: func() *FAAddress { - adr, _ := NewFsAddress(FsAddressStr) - pub := adr.FAAddress() - return &pub - }(), -}, { - Name: "valid Fs", - Data: fmt.Sprintf("%q", FsAddressStr), - Adr: new(FsAddress), - ExpAdr: func() *FsAddress { - adr, _ := NewFsAddress(FsAddressStr) - return &adr - }(), -}, { - Name: "valid EC", - Data: fmt.Sprintf("%q", ECAddressStr), - Adr: new(ECAddress), - ExpAdr: func() *ECAddress { - adr, _ := NewEsAddress(EsAddressStr) - pub := adr.ECAddress() - return &pub - }(), -}, { - Name: "valid Es", - Data: fmt.Sprintf("%q", EsAddressStr), - Adr: new(EsAddress), - ExpAdr: func() *EsAddress { - adr, _ := NewEsAddress(EsAddressStr) - return &adr - }(), -}, { - Name: "invalid type", - Data: `{}`, - Err: "json: cannot unmarshal object into Go value of type string", -}, { - Name: "invalid type", - Data: `5.5`, - Err: "json: cannot unmarshal number into Go value of type string", -}, { - Name: "invalid type", - Data: `["hello"]`, - Err: "json: cannot unmarshal array into Go value of type string", -}, { - Name: "invalid length", - Data: fmt.Sprintf("%q", FAAddressStr[0:len(FAAddressStr)-1]), - Err: "invalid length", -}, { - Name: "invalid length", - Data: fmt.Sprintf("%q", FAAddressStr+"Q"), - Err: "invalid length", -}, { - Name: "invalid prefix", - Data: fmt.Sprintf("%q", func() string { - adr, _ := NewFAAddress(FAAddressStr) - return adr.payload().StringPrefix([]byte{0x50, 0x50}) - }()), - Err: "invalid prefix", -}, { - Name: "invalid prefix", - Data: fmt.Sprintf("%q", FsAddressStr), - Err: "invalid prefix", -}, { - Name: "invalid symbol/FA", - Data: fmt.Sprintf("%q", FAAddressStr[0:len(FAAddressStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - Adr: new(FAAddress), - ExpAdr: new(FAAddress), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", FAAddressStr[0:len(FAAddressStr)-1]+"e"), - Err: "checksum error", - Adr: new(FAAddress), - ExpAdr: new(FAAddress), -}} - -func testAddressUnmarshalJSON(t *testing.T, test addressUnmarshalJSONTest) { - err := json.Unmarshal([]byte(test.Data), test.Adr) - assert := assert.New(t) - if len(test.Err) > 0 { - assert.EqualError(err, test.Err) - return - } - assert.NoError(err) - assert.Equal(test.ExpAdr, test.Adr) -} - -func TestAddress(t *testing.T) { - for _, test := range addressUnmarshalJSONTests { - if test.Adr != nil { - t.Run("UnmarshalJSON/"+test.Name, func(t *testing.T) { - testAddressUnmarshalJSON(t, test) - }) - continue - } - test.ExpAdr, test.Adr = &FAAddress{}, &FAAddress{} - t.Run("UnmarshalJSON/FA", func(t *testing.T) { - testAddressUnmarshalJSON(t, test) - }) - test.ExpAdr, test.Adr = &ECAddress{}, &ECAddress{} - t.Run("UnmarshalJSON/EC", func(t *testing.T) { - testAddressUnmarshalJSON(t, test) - }) - } - - fa, _ := NewFAAddress(FAAddressStr) - fs, _ := NewFsAddress(FsAddressStr) - ec, _ := NewECAddress(ECAddressStr) - es, _ := NewEsAddress(EsAddressStr) - strToAdr := map[string]Address{FAAddressStr: fa, FsAddressStr: fs, - ECAddressStr: ec, EsAddressStr: es} - for adrStr, adr := range strToAdr { - t.Run("MarshalJSON/"+adr.PrefixString(), func(t *testing.T) { - data, err := json.Marshal(adr) - assert := assert.New(t) - assert.NoError(err) - assert.Equal(fmt.Sprintf("%q", adrStr), string(data)) - }) - t.Run("Payload/"+adr.PrefixString(), func(t *testing.T) { - assert.EqualValues(t, adr, adr.Payload()) - }) - } - - t.Run("FsAddress", func(t *testing.T) { - pub, _ := NewFAAddress(FAAddressStr) - priv, _ := NewFsAddress(FsAddressStr) - assert := assert.New(t) - assert.Equal(pub, priv.FAAddress()) - assert.Equal(pub.PublicAddress(), priv.PublicAddress()) - assert.Equal(pub.RCDHash(), priv.RCDHash(), "RCDHash") - }) - t.Run("EsAddress", func(t *testing.T) { - pub, _ := NewECAddress(ECAddressStr) - priv, _ := NewEsAddress(EsAddressStr) - assert := assert.New(t) - assert.Equal(pub, priv.ECAddress()) - assert.Equal(pub.PublicAddress(), priv.PublicAddress()) - }) - - t.Run("New", func(t *testing.T) { - for _, adrStr := range []string{FAAddressStr, FsAddressStr, - ECAddressStr, EsAddressStr} { - t.Run(adrStr, func(t *testing.T) { - assert := assert.New(t) - adr, err := NewAddress(adrStr) - assert.NoError(err) - assert.Equal(adrStr, fmt.Sprintf("%v", adr)) - }) - t.Run("Public/"+adrStr, func(t *testing.T) { - assert := assert.New(t) - adr, err := NewPublicAddress(adrStr) - if adrStr[1] == 's' { - assert.EqualError(err, "invalid prefix") - return - } - assert.NoError(err) - assert.Equal(adrStr, fmt.Sprintf("%v", adr)) - }) - t.Run("Private/"+adrStr, func(t *testing.T) { - assert := assert.New(t) - adr, err := NewPrivateAddress(adrStr) - if adrStr[1] != 's' { - assert.EqualError(err, "invalid prefix") - return - } - assert.NoError(err) - assert.Equal(adrStr, fmt.Sprintf("%v", adr)) - }) - } - - t.Run("invalid length", func(t *testing.T) { - assert := assert.New(t) - - _, err := NewAddress("too short") - assert.EqualError(err, "invalid length") - - _, err = NewPrivateAddress("too short") - assert.EqualError(err, "invalid length") - - _, err = NewPublicAddress("too short") - assert.EqualError(err, "invalid length") - }) - - t.Run("unrecognized prefix", func(t *testing.T) { - adr, _ := NewFAAddress(FAAddressStr) - adrStr := adr.payload().StringPrefix([]byte{0x50, 0x50}) - assert := assert.New(t) - - _, err := NewAddress(adrStr) - assert.EqualError(err, "unrecognized prefix") - - _, err = NewPrivateAddress(adrStr) - assert.EqualError(err, "unrecognized prefix") - - _, err = NewPublicAddress(adrStr) - assert.EqualError(err, "unrecognized prefix") - }) - }) - - t.Run("Generate/Fs", func(t *testing.T) { - var err error - fs, err = GenerateFsAddress() - assert.NoError(t, err) - }) - t.Run("Generate/Es", func(t *testing.T) { - var err error - es, err = GenerateEsAddress() - assert.NoError(t, err) - }) - - c := NewClient() - t.Run("Save/Fs", func(t *testing.T) { - err := fs.Save(c) - assert.NoError(t, err) - }) - t.Run("Save/Es", func(t *testing.T) { - err := es.Save(c) - assert.NoError(t, err) - }) - - t.Run("GetPrivateAddress/Fs", func(t *testing.T) { - assert := assert.New(t) - _, err := fs.GetPrivateAddress(nil) - assert.NoError(err) - - fa = fs.FAAddress() - newFs, err := fa.GetPrivateAddress(c) - assert.NoError(err) - assert.Equal(fs, newFs) - }) - t.Run("GetPrivateAddress/Es", func(t *testing.T) { - assert := assert.New(t) - _, err := es.GetPrivateAddress(nil) - assert.NoError(err) - - ec = es.ECAddress() - newEs, err := ec.GetPrivateAddress(c) - assert.NoError(err) - assert.Equal(es, newEs) - assert.Equal(ec.PublicKey(), es.PublicKey()) - }) - - t.Run("GetAddresses", func(t *testing.T) { - adrs, err := c.GetAddresses() - assert := assert.New(t) - assert.NoError(err) - assert.NotEmpty(adrs) - }) - t.Run("GetPrivateAddresses", func(t *testing.T) { - adrs, err := c.GetPrivateAddresses() - assert := assert.New(t) - assert.NoError(err) - assert.NotEmpty(adrs) - }) - t.Run("GetFAAddresses", func(t *testing.T) { - adrs, err := c.GetFAAddresses() - assert := assert.New(t) - assert.NoError(err) - assert.NotEmpty(adrs) - }) - t.Run("GetFsAddresses", func(t *testing.T) { - adrs, err := c.GetFsAddresses() - assert := assert.New(t) - assert.NoError(err) - assert.NotEmpty(adrs) - }) - t.Run("GetECAddresses", func(t *testing.T) { - adrs, err := c.GetECAddresses() - assert := assert.New(t) - assert.NoError(err) - assert.NotEmpty(adrs) - }) - t.Run("GetEsAddresses", func(t *testing.T) { - adrs, err := c.GetEsAddresses() - assert := assert.New(t) - assert.NoError(err) - assert.NotEmpty(adrs) - }) - - for _, adr := range strToAdr { - t.Run("GetBalance/"+adr.PrefixString(), func(t *testing.T) { - balance, err := adr.GetBalance(c) - assert := assert.New(t) - assert.NoError(err) - assert.Equal(uint64(0), balance) - }) - } - fundedEC, _ := NewECAddress("EC1zANmWuEMYoH6VizJg6uFaEdi8Excn1VbLN99KRuxh3GSvB7YQ") - t.Run("GetBalance/"+fundedEC.String(), func(t *testing.T) { - balance, err := fundedEC.GetBalance(c) - assert := assert.New(t) - assert.NoError(err) - assert.NotEqual(uint64(0), balance) - }) - - t.Run("Remove/Fs", func(t *testing.T) { - err := fs.Remove(c) - assert.NoError(t, err) - }) - t.Run("Remove/Es", func(t *testing.T) { - err := es.Remove(c) - assert.NoError(t, err) - }) - - t.Run("Scan", func(t *testing.T) { - var adr FAAddress - err := adr.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = adr.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = adr.Scan(in) - assert.NoError(err) - assert.EqualValues(in, adr[:]) - }) - - t.Run("Value", func(t *testing.T) { - var adr FAAddress - adr[0] = 0xff - val, err := adr.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(adr[:], val) - }) - -} diff --git a/factom/bytes.go b/factom/bytes.go deleted file mode 100644 index a0ee077..0000000 --- a/factom/bytes.go +++ /dev/null @@ -1,156 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "database/sql" - "database/sql/driver" - "encoding/hex" - "encoding/json" - "fmt" -) - -// Bytes32 implements json.Marshaler and json.Unmarshaler to encode and decode -// strings with exactly 32 bytes of hex encoded data, such as Chain IDs and -// KeyMRs. -type Bytes32 [32]byte - -// Bytes implements json.Marshaler and json.Unmarshaler to encode and decode -// strings with hex encoded data, such as an Entry's External IDs or content. -type Bytes []byte - -// NewBytes32 allocates a new Bytes32 object with the first 32 bytes of data -// contained in s32. -func NewBytes32(s32 []byte) *Bytes32 { - b32 := new(Bytes32) - copy(b32[:], s32) - return b32 -} - -// NewBytes32FromString allocates a new Bytes32 object with the hex encoded -// string data contained in s32. -func NewBytes32FromString(s32 string) *Bytes32 { - b32 := new(Bytes32) - b32.Set(s32) - return b32 -} - -// Set decodes a string with exactly 32 bytes of hex encoded data. -func (b *Bytes32) Set(hexStr string) error { - if len(hexStr) == 0 { - return nil - } - if len(hexStr) != hex.EncodedLen(len(b)) { - return fmt.Errorf("invalid length") - } - if _, err := hex.Decode(b[:], []byte(hexStr)); err != nil { - return err - } - return nil -} - -// Set decodes a string with hex encoded data. -func (b *Bytes) Set(hexStr string) error { - *b = make(Bytes, hex.DecodedLen(len(hexStr))) - if _, err := hex.Decode(*b, []byte(hexStr)); err != nil { - return err - } - return nil -} - -// UnmarshalJSON decodes a JSON string with exactly 32 bytes of hex encoded -// data. -func (b *Bytes32) UnmarshalJSON(data []byte) error { - var hexStr string - if err := json.Unmarshal(data, &hexStr); err != nil { - return err - } - return b.Set(hexStr) -} - -// UnmarshalJSON decodes a JSON string with hex encoded data. -func (b *Bytes) UnmarshalJSON(data []byte) error { - var hexStr string - if err := json.Unmarshal(data, &hexStr); err != nil { - return err - } - return b.Set(hexStr) -} - -// String encodes b as a hex string. -func (b Bytes32) String() string { - return hex.EncodeToString(b[:]) -} - -// String encodes b as a hex string. -func (b Bytes) String() string { - return hex.EncodeToString(b[:]) -} - -// Type returns "Bytes32". Satisfies pflag.Value interface. -func (b Bytes32) Type() string { - return "Bytes32" -} - -// Type returns "Bytes". Satisfies pflag.Value interface. -func (b Bytes) Type() string { - return "Bytes" -} - -// MarshalJSON encodes b as a hex JSON string. -func (b Bytes32) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("%#v", b.String())), nil -} - -// MarshalJSON encodes b as a hex JSON string. -func (b Bytes) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("%#v", b.String())), nil -} - -// Scan expects v to be a byte slice with exactly 32 bytes of data. -func (b *Bytes32) Scan(v interface{}) error { - data, ok := v.([]byte) - if !ok { - return fmt.Errorf("invalid type") - } - if len(data) != 32 { - return fmt.Errorf("invalid length") - } - copy(b[:], data) - return nil -} - -// Value expects b to be a byte slice with exactly 32 bytes of data. -func (b Bytes32) Value() (driver.Value, error) { - return b[:], nil -} - -var _ sql.Scanner = &Bytes32{} -var _ driver.Valuer = Bytes32{} - -var zeroBytes32 Bytes32 - -// ZeroBytes32 returns an all zero Byte32. -func ZeroBytes32() Bytes32 { - return zeroBytes32 -} diff --git a/factom/bytes_test.go b/factom/bytes_test.go deleted file mode 100644 index d68a091..0000000 --- a/factom/bytes_test.go +++ /dev/null @@ -1,174 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "encoding/json" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - JSONBytesInvalidTypes = []string{`{}`, `5.5`, `["hello"]`} - JSONBytes32InvalidLengths = []string{ - `"00"`, - `"000000000000000000000000000000000000000000000000000000000000000000"`} - JSONBytesInvalidSymbol = `"x000000000000000000000000000000000000000000000000000000000000000"` - JSONBytes32Valid = `"da56930e8693fb7c0a13aac4d01cf26184d760f2fd92d2f0a62aa630b1a25fa7"` -) - -type unmarshalJSONTest struct { - Name string - Data string - Err string - Un interface { - json.Unmarshaler - json.Marshaler - } - Exp interface { - json.Unmarshaler - json.Marshaler - } -} - -var unmarshalJSONtests = []unmarshalJSONTest{{ - Name: "Bytes32/valid", - Data: `"DA56930e8693fb7c0a13aac4d01cf26184d760f2fd92d2f0a62aa630b1a25fa7"`, - Un: new(Bytes32), - Exp: &Bytes32{0xDA, 0x56, 0x93, 0x0e, 0x86, 0x93, 0xfb, 0x7c, 0x0a, 0x13, - 0xaa, 0xc4, 0xd0, 0x1c, 0xf2, 0x61, 0x84, 0xd7, 0x60, 0xf2, 0xfd, - 0x92, 0xd2, 0xf0, 0xa6, 0x2a, 0xa6, 0x30, 0xb1, 0xa2, 0x5f, 0xa7}, -}, { - Name: "Bytes/valid", - Data: `"DA56930e8693fb7c0a13aac4d01cf26184d760f2fd92d2f0a62aa630b1a25fa7"`, - Un: new(Bytes), - Exp: &Bytes{0xDA, 0x56, 0x93, 0x0e, 0x86, 0x93, 0xfb, 0x7c, 0x0a, 0x13, - 0xaa, 0xc4, 0xd0, 0x1c, 0xf2, 0x61, 0x84, 0xd7, 0x60, 0xf2, 0xfd, - 0x92, 0xd2, 0xf0, 0xa6, 0x2a, 0xa6, 0x30, 0xb1, 0xa2, 0x5f, 0xa7}, -}, { - Name: "Bytes32/valid", - Data: `"0000000000000000000000000000000000000000000000000000000000000000"`, - Un: new(Bytes32), - Exp: &Bytes32{}, -}, { - Name: "Bytes/valid", - Data: `"0000000000000000000000000000000000000000000000000000000000000000"`, - Un: new(Bytes), - Exp: func() *Bytes { b := make(Bytes, 32); return &b }(), -}, { - Name: "invalid symbol", - Data: `"DA56930e8693fb7c0a13aac4d01cf26184d760f2fd92d2f0a62aa630b1zxcva7"`, - Err: "encoding/hex: invalid byte: U+007A 'z'", -}, { - Name: "invalid type", - Data: `{}`, - Err: "json: cannot unmarshal object into Go value of type string", -}, { - Name: "invalid type", - Data: `5.5`, - Err: "json: cannot unmarshal number into Go value of type string", -}, { - Name: "invalid type", - Data: `["asdf"]`, - Err: "json: cannot unmarshal array into Go value of type string", -}, { - Name: "too long", - Data: `"DA56930e8693fb7c0a13aac4d01cf26184d760f2fd92d2f0a62aa630b1a25fa71234"`, - Err: "invalid length", - Un: new(Bytes32), -}, { - Name: "too short", - Data: `"DA56930e8693fb7c0a13aac4d01cf26184d760f2fd92d2f0a62aa630b1a25fa71234"`, - Err: "invalid length", - Un: new(Bytes32), -}} - -func testUnmarshalJSON(t *testing.T, test unmarshalJSONTest) { - assert := assert.New(t) - err := test.Un.UnmarshalJSON([]byte(test.Data)) - if len(test.Err) > 0 { - assert.EqualError(err, test.Err) - return - } - assert.NoError(err) - assert.Equal(test.Exp, test.Un) -} - -func TestBytes(t *testing.T) { - for _, test := range unmarshalJSONtests { - if test.Un != nil { - t.Run("UnmarshalJSON/"+test.Name, func(t *testing.T) { - testUnmarshalJSON(t, test) - }) - if test.Exp != nil { - t.Run("MarshalJSON/"+test.Name, func(t *testing.T) { - data, err := test.Un.MarshalJSON() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(strings.ToLower(test.Data), - string(data)) - }) - } - continue - } - test.Un = new(Bytes32) - t.Run("UnmarshalJSON/Bytes32/"+test.Name, func(t *testing.T) { - testUnmarshalJSON(t, test) - }) - test.Un = new(Bytes) - t.Run("UnmarshalJSON/Bytes/"+test.Name, func(t *testing.T) { - testUnmarshalJSON(t, test) - }) - } - - t.Run("Scan", func(t *testing.T) { - var b Bytes32 - err := b.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = b.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = b.Scan(in) - assert.NoError(err) - assert.EqualValues(in, b[:]) - }) - - t.Run("Value", func(t *testing.T) { - var b Bytes32 - b[0] = 0xff - val, err := b.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(b[:], val) - }) - - t.Run("ZeroBytes32", func(t *testing.T) { - assert.Equal(t, Bytes32{}, ZeroBytes32()) - }) -} diff --git a/factom/client.go b/factom/client.go deleted file mode 100644 index 3befc4e..0000000 --- a/factom/client.go +++ /dev/null @@ -1,76 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "fmt" - "time" - - jrpc "github.com/AdamSLevy/jsonrpc2/v11" -) - -// Client makes RPC requests to factomd's and factom-walletd's APIs. Client -// embeds two jsonrpc2.Clients, and thus also two http.Client, one for requests -// to factomd and one for requests to factom-walletd. Use jsonrpc2.Client's -// BasicAuth settings to set up BasicAuth and http.Client's transport settings -// to configure TLS. -type Client struct { - Factomd jrpc.Client - FactomdServer string - Walletd jrpc.Client - WalletdServer string -} - -// Defaults for the factomd and factom-walletd endpoints. -const ( - FactomdDefault = "http://localhost:8088" - WalletdDefault = "http://localhost:8089" -) - -// NewClient returns a pointer to a Client initialized with the default -// localhost endpoints for factomd and factom-walletd, and 15 second timeouts -// for each of the http.Clients. -func NewClient() *Client { - c := &Client{FactomdServer: FactomdDefault, WalletdServer: WalletdDefault} - c.Factomd.Timeout = 20 * time.Second - c.Walletd.Timeout = 10 * time.Second - return c -} - -// FactomdRequest makes a request to factomd's v2 API. -func (c *Client) FactomdRequest(method string, params, result interface{}) error { - url := c.FactomdServer + "/v2" - if c.Factomd.DebugRequest { - fmt.Println("factomd:", url) - } - return c.Factomd.Request(url, method, params, result) -} - -// WalletdRequest makes a request to factom-walletd's v2 API. -func (c *Client) WalletdRequest(method string, params, result interface{}) error { - url := c.WalletdServer + "/v2" - if c.Walletd.DebugRequest { - fmt.Println("factom-walletd:", url) - } - return c.Walletd.Request(url, method, params, result) -} diff --git a/factom/dblock.go b/factom/dblock.go deleted file mode 100644 index e40cbd8..0000000 --- a/factom/dblock.go +++ /dev/null @@ -1,354 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "bytes" - "crypto/sha256" - "encoding/binary" - "encoding/json" - "fmt" - "math" - "sort" - "time" - - merkle "github.com/AdamSLevy/go-merkle" -) - -var ( - mainnetID = [...]byte{0xFA, 0x92, 0xE5, 0xA2} - testnetID = [...]byte{0xFA, 0x92, 0xE5, 0xA3} - - adminBlockChainID = Bytes32{31: 0x0a} - entryCreditBlockChainID = Bytes32{31: 0x0c} - factoidBlockChainID = Bytes32{31: 0x0f} -) - -// DBlock represents a Factom Directory Block. -type DBlock struct { - KeyMR *Bytes32 `json:"keymr"` - - FullHash *Bytes32 `json:"dbhash"` - - Header DBlockHeader `json:"header"` - - // DBlock.Get populates EBlocks with their ChainID and KeyMR. - EBlocks []EBlock `json:"dbentries,omitempty"` -} - -type DBlockHeader struct { - NetworkID [4]byte `json:"networkid"` - - BodyMR *Bytes32 `json:"bodymr"` - PrevKeyMR *Bytes32 `json:"prevkeymr"` - PrevFullHash *Bytes32 `json:"prevfullhash"` - - Height uint32 `json:"dbheight"` - - Timestamp time.Time `json:"-"` -} - -type dBH DBlockHeader -type dBHs struct { - *dBH - NetworkID uint32 `json:"networkid"` - Timestamp int64 `json:"timestamp"` -} - -func (dbh *DBlockHeader) UnmarshalJSON(data []byte) error { - d := dBHs{dBH: (*dBH)(dbh)} - if err := json.Unmarshal(data, &d); err != nil { - return err - } - dbh.Timestamp = time.Unix(d.Timestamp*60, 0) - binary.BigEndian.PutUint32(dbh.NetworkID[:], d.NetworkID) - return nil -} - -func (dbh *DBlockHeader) MarshalJSON() ([]byte, error) { - d := dBHs{ - (*dBH)(dbh), - binary.BigEndian.Uint32(dbh.NetworkID[:]), - dbh.Timestamp.Unix() / 60, - } - return json.Marshal(d) -} - -// IsPopulated returns true if db has already been successfully populated by a -// call to Get. IsPopulated returns false if db.EBlocks is nil. -func (db DBlock) IsPopulated() bool { - return len(db.EBlocks) > 0 && - db.Header.BodyMR != nil && - db.Header.PrevKeyMR != nil && - db.Header.PrevFullHash != nil -} - -// Get queries factomd for the Directory Block at db.Header.Height. After a -// successful call, the EBlocks will all have their ChainID and KeyMR, but not -// their Entries. Call Get on the EBlocks individually to populate their -// Entries. -func (db *DBlock) Get(c *Client) (err error) { - if db.IsPopulated() { - return nil - } - defer func() { - if err != nil { - return - } - var keyMR Bytes32 - if keyMR, err = db.ComputeKeyMR(); err != nil { - return - } - if *db.KeyMR != keyMR { - err = fmt.Errorf("invalid key merkle root") - return - } - }() - - if db.KeyMR != nil { - params := struct { - Hash *Bytes32 `json:"hash"` - }{Hash: db.KeyMR} - var result struct { - Data Bytes `json:"data"` - } - if err := c.FactomdRequest("raw-data", params, &result); err != nil { - return err - } - return db.UnmarshalBinary(result.Data) - } - - params := struct { - Height uint32 `json:"height"` - }{db.Header.Height} - result := struct { - DBlock *DBlock - }{DBlock: db} - if err := c.FactomdRequest("dblock-by-height", params, &result); err != nil { - return err - } - - for ebi := range db.EBlocks { - eb := &db.EBlocks[ebi] - eb.Timestamp = db.Header.Timestamp - eb.Height = db.Header.Height - } - - return nil -} - -const ( - DBlockHeaderLen = 1 + // [Version byte (0x00)] - 4 + // NetworkID - 32 + // BodyMR - 32 + // PrevKeyMR - 32 + // PrevFullHash - 4 + // Timestamp - 4 + // DB Height - 4 // EBlock Count - - DBlockEBlockLen = 32 + // ChainID - 32 // KeyMR - - DBlockMinBodyLen = DBlockEBlockLen + // Admin Block - DBlockEBlockLen + // EC Block - DBlockEBlockLen // FCT Block - DBlockMinTotalLen = DBlockHeaderLen + DBlockMinBodyLen - - DBlockMaxBodyLen = math.MaxUint32 * DBlockEBlockLen - DBlockMaxTotalLen = DBlockHeaderLen + DBlockMaxBodyLen -) - -// UnmarshalBinary unmarshals raw directory block data. -// -// Header -// [Version byte (0x00)] + -// [NetworkID (4 bytes)] + -// [BodyMR (Bytes32)] + -// [PrevKeyMR (Bytes32)] + -// [PrevFullHash (Bytes32)] + -// [Timestamp (4 bytes)] + -// [DB Height (4 bytes)] + -// [EBlock Count (4 bytes)] -// -// Body -// [Admin Block ChainID (Bytes32{31:0x0a})] + -// [Admin Block LookupHash (Bytes32)] + -// [EC Block ChainID (Bytes32{31:0x0c})] + -// [EC Block HeaderHash (Bytes32)] + -// [FCT Block ChainID (Bytes32{31:0x0f})] + -// [FCT Block KeyMR (Bytes32)] + -// [ChainID 0 (Bytes32)] + -// [KeyMR 0 (Bytes32)] + -// ... + -// [ChainID N (Bytes32)] + -// [KeyMR N (Bytes32)] + -// -// https://github.com/FactomProject/FactomDocs/blob/master/factomDataStructureDetails.md#directory-block -func (db *DBlock) UnmarshalBinary(data []byte) error { - if len(data) < DBlockMinTotalLen { - return fmt.Errorf("insufficient length") - } - if len(data) > DBlockMaxTotalLen { - return fmt.Errorf("invalid length") - } - if data[0] != 0x00 { - return fmt.Errorf("invalid version byte") - } - i := 1 - i += copy(db.Header.NetworkID[:], data[i:i+len(db.Header.NetworkID)]) - db.Header.BodyMR = new(Bytes32) - i += copy(db.Header.BodyMR[:], data[i:i+len(db.Header.BodyMR)]) - db.Header.PrevKeyMR = new(Bytes32) - i += copy(db.Header.PrevKeyMR[:], data[i:i+len(db.Header.PrevKeyMR)]) - db.Header.PrevFullHash = new(Bytes32) - i += copy(db.Header.PrevFullHash[:], data[i:i+len(db.Header.PrevFullHash)]) - db.Header.Timestamp = time.Unix(int64(binary.BigEndian.Uint32(data[i:i+4]))*60, 0) - i += 4 - db.Header.Height = binary.BigEndian.Uint32(data[i : i+4]) - i += 4 - ebsLen := int(binary.BigEndian.Uint32(data[i : i+4])) - i += 4 - if len(data[i:]) < ebsLen*DBlockEBlockLen { - return fmt.Errorf("insufficient length") - } - db.EBlocks = make([]EBlock, ebsLen) - var lastChainID Bytes32 - for ebi := range db.EBlocks { - eb := &db.EBlocks[ebi] - // Ensure that EBlocks are ordered by Chain ID with no - // duplicates. - if bytes.Compare(data[i:i+len(eb.ChainID)], lastChainID[:]) <= 0 { - return fmt.Errorf("out of order or duplicate Chain ID") - } - eb.ChainID = new(Bytes32) - i += copy(eb.ChainID[:], data[i:i+len(eb.ChainID)]) - lastChainID = *eb.ChainID - eb.KeyMR = new(Bytes32) - i += copy(eb.KeyMR[:], data[i:i+len(eb.KeyMR)]) - - eb.Timestamp = db.Header.Timestamp - eb.Height = db.Header.Height - } - return nil -} - -func (db *DBlock) MarshalBinary() ([]byte, error) { - data, err := db.MarshalBinaryHeader() - if err != nil { - return nil, err - } - i := DBlockHeaderLen - for _, eb := range db.EBlocks { - i += copy(data[i:], eb.ChainID[:]) - i += copy(data[i:], eb.KeyMR[:]) - } - return data, nil -} - -func (db *DBlock) MarshalBinaryHeader() ([]byte, error) { - if !db.IsPopulated() { - return nil, fmt.Errorf("not populated") - } - totalLen := db.MarshalBinaryLen() - if totalLen > DBlockMaxTotalLen { - return nil, fmt.Errorf("too many EBlocks") - } - data := make([]byte, totalLen) - i := 1 // Skip version byte - i += copy(data[i:], db.Header.NetworkID[:]) - i += copy(data[i:], db.Header.BodyMR[:]) - i += copy(data[i:], db.Header.PrevKeyMR[:]) - i += copy(data[i:], db.Header.PrevFullHash[:]) - binary.BigEndian.PutUint32(data[i:], uint32(db.Header.Timestamp.Unix()/60)) - i += 4 - binary.BigEndian.PutUint32(data[i:], db.Header.Height) - i += 4 - binary.BigEndian.PutUint32(data[i:], uint32(len(db.EBlocks))) - i += 4 - return data, nil -} - -func (db *DBlock) MarshalBinaryLen() int { - return DBlockHeaderLen + len(db.EBlocks)*DBlockEBlockLen -} - -func (db DBlock) ComputeBodyMR() (Bytes32, error) { - blocks := make([][]byte, len(db.EBlocks)) - for i, eb := range db.EBlocks { - blocks[i] = make([]byte, len(eb.ChainID)+len(eb.KeyMR)) - j := copy(blocks[i], eb.ChainID[:]) - copy(blocks[i][j:], eb.KeyMR[:]) - } - tree := merkle.NewTreeWithOpts(merkle.TreeOptions{DoubleOddNodes: true}) - if err := tree.Generate(blocks, sha256.New()); err != nil { - return Bytes32{}, err - } - root := tree.Root() - var bodyMR Bytes32 - copy(bodyMR[:], root.Hash) - return bodyMR, nil -} - -func (db DBlock) ComputeFullHash() (Bytes32, error) { - data, err := db.MarshalBinary() - if err != nil { - return Bytes32{}, err - } - return sha256.Sum256(data), nil -} - -func (db DBlock) ComputeHeaderHash() (Bytes32, error) { - header, err := db.MarshalBinaryHeader() - if err != nil { - return Bytes32{}, err - } - return sha256.Sum256(header[:DBlockHeaderLen]), nil -} - -func (db DBlock) ComputeKeyMR() (Bytes32, error) { - headerHash, err := db.ComputeHeaderHash() - if err != nil { - return Bytes32{}, err - } - bodyMR, err := db.ComputeBodyMR() - if err != nil { - return Bytes32{}, err - } - data := make([]byte, len(headerHash)+len(bodyMR)) - i := copy(data, headerHash[:]) - copy(data[i:], bodyMR[:]) - return sha256.Sum256(data), nil -} - -// EBlock efficiently finds and returns the *EBlock in db.EBlocks for the given -// chainID, if it exists. Otherwise, EBlock returns nil. -func (db DBlock) EBlock(chainID Bytes32) *EBlock { - ei := sort.Search(len(db.EBlocks), func(i int) bool { - return bytes.Compare(db.EBlocks[i].ChainID[:], chainID[:]) >= 0 - }) - if ei < len(db.EBlocks) && *db.EBlocks[ei].ChainID == chainID { - return &db.EBlocks[ei] - } - return nil -} diff --git a/factom/dblock_eblock_entry_test.go b/factom/dblock_eblock_entry_test.go deleted file mode 100644 index c5cd2a1..0000000 --- a/factom/dblock_eblock_entry_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom_test - -import ( - "testing" - "time" - - . "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var courtesyNode = "https://courtesy-node.factom.com" - -func TestDataStructures(t *testing.T) { - height := uint32(166587) - c := NewClient() - c.Factomd.DebugRequest = true - db := &DBlock{} - db.Header.Height = height - t.Run("DBlock", func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // We should start off unpopulated. - require.False(db.IsPopulated()) - - // A bad URL will cause an error. - c.FactomdServer = "http://example.com" - assert.Error(db.Get(c)) - - c.FactomdServer = courtesyNode - require.NoError(db.Get(c)) - - require.True(db.IsPopulated(), db) - assert.NoError(db.Get(c)) // Take the early exit code path. - - // Validate this DBlock. - assert.Len(db.EBlocks, 7) - assert.Equal(height, db.Header.Height) - for _, eb := range db.EBlocks { - assert.NotNil(eb.ChainID) - assert.NotNil(eb.KeyMR) - } - - dbk := DBlock{KeyMR: db.KeyMR, FullHash: db.FullHash} - require.NoError(dbk.Get(c)) - assert.Equal(*db, dbk) - - params := struct { - Hash *Bytes32 `json:"hash"` - }{Hash: db.KeyMR} - var result struct { - Data Bytes `json:"data"` - } - require.NoError(c.FactomdRequest("raw-data", params, &result)) - - data, err := db.MarshalBinary() - require.NoError(err) - for i := range result.Data { - assert.Equal(result.Data[i], data[i], i) - } - - full, err := dbk.ComputeFullHash() - require.NoError(err, "ComputeFullHash()") - assert.Equal(*db.FullHash, full, "ComputeFullHash()") - - bodyMR, err := dbk.ComputeBodyMR() - require.NoError(err, "ComputeBodyMR()") - assert.Equal(*db.Header.BodyMR, bodyMR, "ComputeBodyMR()") - - keyMR, err := dbk.ComputeKeyMR() - require.NoError(err, "ComputeKeyMR()") - assert.Equal(*db.KeyMR, keyMR, "ComputeKeyMR()") - - eb := &db.EBlocks[len(db.EBlocks)-1] - assert.Equal(eb, db.EBlock(*eb.ChainID)) - assert.Nil(db.EBlock(Bytes32{})) - }) - t.Run("EBlock", func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // An EBlock without a KeyMR or ChainID should cause an error. - blank := EBlock{} - assert.EqualError(blank.Get(c), "no KeyMR or ChainID specified") - - // We'll use the DBlock from the last test, so it must be - // populated to proceed. - require.True(db.IsPopulated()) - - // This EBlock has multiple entries we can validate against. - // We'll use a pointer here so that we can reuse this EBlock in - // the next test. - eb := &db.EBlocks[4] - - // We start off unpopulated. - require.False(eb.IsPopulated()) - - // A bad URL will cause an error. - c.FactomdServer = "example.com" - assert.Error(eb.Get(c)) - - c.FactomdServer = courtesyNode - require.NoError(eb.Get(c)) - - require.True(eb.IsPopulated()) - assert.NoError(eb.Get(c)) // Take the early exit code path. - - // Validate the entries. - assert.Len(eb.Entries, 5) - assert.Equal(height, eb.Height) - require.NotNil(eb.PrevKeyMR) - for _, e := range eb.Entries { - assert.True(e.ChainID == eb.ChainID) - assert.NotNil(e.Hash) - assert.NotNil(e.Timestamp) - assert.Equal(height, e.Height) - } - - assert.False(eb.IsFirst()) - - // A bad URL will cause an error. - c.FactomdServer = "example.com" - _, err := eb.GetAllPrev(c) - assert.Error(err) - - c.FactomdServer = courtesyNode - ebs, err := eb.GetAllPrev(c) - assert.NoError(err) - assert.Len(ebs, 6) - assert.True(ebs[0].IsFirst()) - first := ebs[0].Prev() - assert.Equal(first.KeyMR, ebs[0].KeyMR, - "Prev() should return a copy of itself if it is first") - assert.Equal(eb.KeyMR, ebs[len(ebs)-1].KeyMR) - - // Fetch the chain head EBlock via the ChainID. - // First use an invalid ChainID and an invalid URL. - eb2 := EBlock{ChainID: NewBytes32(nil)} - c.FactomdServer = "example.com" - assert.Error(eb2.Get(c)) - assert.Error(eb2.GetFirst(c)) - - c.FactomdServer = courtesyNode - require.Error(eb2.Get(c)) - require.False(eb2.IsPopulated()) - assert.EqualError(eb2.GetFirst(c), - `jsonrpc2.Error{Code:-32009, Message:"Missing Chain Head"}`) - ebs, err = eb2.GetAllPrev(c) - assert.EqualError(err, - `jsonrpc2.Error{Code:-32009, Message:"Missing Chain Head"}`) - assert.Nil(ebs) - - // A valid ChainID should allow it to be populated. - eb2.ChainID = eb.ChainID - require.NoError(eb2.Get(c)) - require.True(eb2.IsPopulated()) - assert.NoError(eb2.GetFirst(c)) - assert.Equal(first.KeyMR, eb2.KeyMR) - - // Make RPC request for this Entry Block. - params := struct { - KeyMR *Bytes32 `json:"hash"` - }{KeyMR: eb2.KeyMR} - var result struct { - Data Bytes `json:"data"` - } - require.NoError(c.FactomdRequest("raw-data", params, &result)) - data, err := eb2.MarshalBinary() - require.NoError(err) - assert.Equal(result.Data, Bytes(data)) - - bodyMR, err := eb2.ComputeBodyMR() - require.NoError(err) - assert.Equal(*eb2.BodyMR, bodyMR) - - keyMR, err := eb2.ComputeKeyMR() - require.NoError(err) - assert.Equal(*eb2.KeyMR, keyMR) - }) - t.Run("Entry", func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - // An EBlock without a KeyMR or ChainID should cause an error. - blank := Entry{} - assert.EqualError(blank.Get(c), "Hash is nil") - - // We'll use the DBlock and EBlock from the last test, so they - // must be populated to proceed. - require.True(db.IsPopulated()) - eb := db.EBlocks[4] - require.True(eb.IsPopulated()) - - e := eb.Entries[0] - // We start off unpopulated. - require.False(e.IsPopulated()) - - // A bad URL will cause an error. - c.FactomdServer = "example.com" - assert.Error(e.Get(c)) - - c.FactomdServer = courtesyNode - require.NoError(e.Get(c)) - - require.True(e.IsPopulated()) - assert.NoError(e.Get(c)) // Take the early exit code path. - - // Validate the entry. - assert.Len(e.ExtIDs, 6) - assert.NotEmpty(e.Content) - assert.Equal(height, e.Height) - assert.Equal(time.Unix(1542223080, 0), e.Timestamp) - hash, err := e.ComputeHash() - assert.NoError(err) - assert.Equal(*e.Hash, hash) - - e = eb.Entries[1] - require.NoError(e.Get(c)) - hash, err = e.ComputeHash() - assert.NoError(err) - assert.Equal(*e.Hash, hash) - }) - - assert.Equal(t, Bytes32{}, ZeroBytes32()) -} diff --git a/factom/doc.go b/factom/doc.go deleted file mode 100644 index 7fc4487..0000000 --- a/factom/doc.go +++ /dev/null @@ -1,60 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Package factom provides data types corresponding to some of the Factom -// blockchain's data structures, as well as methods on those types for querying -// the data from factomd and factom-walletd's APIs. -// -// All of the Factom data structure types in this package have the Get and -// IsPopulated methods. -// -// Methods that accept a *Client, like those that start with Get, may make -// calls to the factomd or factom-walletd API queries to populate the data in -// the variable on which it is called. The returned error can be checked to see -// if it is a jsonrpc2.Error type, indicating that the networking calls were -// successful, but that there is some error returned by the RPC method. -// -// IsPopulated methods return whether the data in the variable has been -// populated by a successful call to Get. -// -// The DBlock, EBlock and Entry types allow for exploring the Factom -// blockchain. -// -// The Bytes and Bytes32 types are used by other types when JSON marshaling and -// unmarshaling to and from hex encoded data is required. Bytes32 is used for -// Chain IDs and KeyMRs. -// -// The Address interfaces and types allow for working with the four Factom -// address types. -// -// The IDKey interfaces and types allow for working with the id/sk key pairs -// for server identities. -// -// Currently this package supports creating new chains and entries using both -// the factom-walletd "compose" methods, and by locally generating the commit -// and reveal data, if the private entry credit key is available locally. See -// Entry.Create and Entry.ComposeCreate. -// -// This package does not yet support Factoid transactions, nor does it support -// the binary data structures for DBlocks or EBlocks. Additionally, working -// with Identity Chains is not yet supported beyond querying the ID1Key. -package factom diff --git a/factom/eblock.go b/factom/eblock.go deleted file mode 100644 index 378cf4a..0000000 --- a/factom/eblock.go +++ /dev/null @@ -1,421 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "bytes" - "crypto/sha256" - "encoding/binary" - "fmt" - "math" - "time" - - merkle "github.com/AdamSLevy/go-merkle" - jrpc "github.com/AdamSLevy/jsonrpc2/v11" -) - -// EBlock represents a Factom Entry Block. -type EBlock struct { - // DBlock.Get populates the ChainID, KeyMR, and Height. - ChainID *Bytes32 `json:"chainid,omitempty"` - KeyMR *Bytes32 `json:"keymr,omitempty"` - - PrevKeyMR *Bytes32 `json:"-"` - - PrevFullHash *Bytes32 `json:"-"` - BodyMR *Bytes32 `json:"-"` - - Height uint32 `json:"-"` - Sequence uint32 `json:"-"` - ObjectCount uint32 `json:"-"` - - Timestamp time.Time `json:"-"` - - // EBlock.Get populates the EBlockHeader.PrevKeyMR and the Entries with - // their Hash and Timestamp. - Entries []Entry `json:"-"` -} - -func (eb EBlock) IsPopulated() bool { - return len(eb.Entries) > 0 && - eb.ChainID != nil && - eb.PrevKeyMR != nil && - eb.PrevFullHash != nil && - eb.BodyMR != nil && - eb.Height > 0 && - eb.ObjectCount > 1 -} - -// Get queries factomd for the Entry Block corresponding to eb.KeyMR, if not -// nil, and otherwise the Entry Block chain head for eb.ChainID. Either -// eb.KeyMR or eb.ChainID must be not nil or else Get will fail to populate the -// EBlock. After a successful call, EBlockHeader and Entries will be populated. -// Each Entry will be populated with its Hash, Timestamp, ChainID, and Height, -// but not its Content or ExtIDs. Call Get on the individual Entries to -// populate their Content and ExtIDs. -func (eb *EBlock) Get(c *Client) error { - // If the EBlock is already populated then there is nothing to do. - if eb.IsPopulated() { - return nil - } - - // If we don't have a KeyMR, fetch the chain head KeyMR. - if eb.KeyMR == nil { - // If the KeyMR and ChainID are both nil we have nothing to - // query for. - if eb.ChainID == nil { - return fmt.Errorf("no KeyMR or ChainID specified") - } - if err := eb.GetChainHead(c); err != nil { - return err - } - } - - // Make RPC request for this Entry Block. - params := struct { - KeyMR *Bytes32 `json:"hash"` - }{KeyMR: eb.KeyMR} - var result struct { - Data Bytes `json:"data"` - } - if err := c.FactomdRequest("raw-data", params, &result); err != nil { - return err - } - height := eb.Height - if err := eb.UnmarshalBinary(result.Data); err != nil { - return err - } - // Verify height if it was initialized - if height > 0 && height != eb.Height { - return fmt.Errorf("height does not match") - } - keyMR, err := eb.ComputeKeyMR() - if err != nil { - return err - } - if *eb.KeyMR != keyMR { - return fmt.Errorf("invalid key merkle root") - } - return nil -} - -// GetChainHead queries factomd for the latest eb.KeyMR for chain eb.ChainID. -func (eb *EBlock) GetChainHead(c *Client) error { - params := eb - result := struct { - KeyMR *Bytes32 `json:"chainhead"` - ChainInProcessList bool `json:"chaininprocesslist"` - }{} - if err := c.FactomdRequest("chain-head", params, &result); err != nil { - return err - } - var zero Bytes32 - if *result.KeyMR == zero { - if result.ChainInProcessList { - return jrpc.Error{Message: "new chain in process list"} - } else { - return jrpc.Error{Code: -32009, Message: "Missing Chain Head"} - } - } - eb.KeyMR = result.KeyMR - return nil -} - -// IsFirst returns true if this is the first EBlock in its chain, indicated by -// the PrevKeyMR being all zeroes. IsFirst returns false if eb is not populated -// or if the PrevKeyMR is not all zeroes. -func (eb EBlock) IsFirst() bool { - return eb.IsPopulated() && *eb.PrevKeyMR == zeroBytes32 -} - -// Prev returns the an EBlock with its KeyMR initialized to eb.PrevKeyMR and -// ChainID initialized to eb.ChainID. If eb is the first Entry Block in the -// chain, then eb is returned. -func (eb EBlock) Prev() EBlock { - if eb.IsFirst() { - return eb - } - return EBlock{ChainID: eb.ChainID, KeyMR: eb.PrevKeyMR} -} - -// GetAllPrev returns a slice of all preceding EBlocks in eb's chain, in order -// from earliest to latest, up to and including eb. So the last element of the -// returned slice is always equal to eb. If eb is the first entry block in its -// chain, then it is the only element in the slice. -// -// If you are only interested in obtaining the first entry block in eb's chain, -// and not all of the intermediary ones, then use GetFirst to reduce network -// calls and memory usage. -func (eb EBlock) GetAllPrev(c *Client) ([]EBlock, error) { - ebs := []EBlock{eb} - for ; !ebs[0].IsFirst(); ebs = append([]EBlock{ebs[0].Prev()}, ebs...) { - if err := ebs[0].Get(c); err != nil { - return nil, err - } - } - return ebs, nil -} - -// GetFirst finds the first Entry Block in eb's chain, and populates eb as -// such. -// -// GetFirst differs from GetAllPrev in that it does not allocate any additional -// EBlocks. GetFirst avoids allocating any new EBlocks by reusing eb to -// traverse up to the first entry block. -func (eb *EBlock) GetFirst(c *Client) error { - for ; !eb.IsFirst(); *eb = eb.Prev() { - if err := eb.Get(c); err != nil { - return err - } - } - return nil -} - -const ( - EBlockHeaderLen = 32 + // [ChainID (Bytes32)] + - 32 + // [BodyMR (Bytes32)] + - 32 + // [PrevKeyMR (Bytes32)] + - 32 + // [PrevFullHash (Bytes32)] + - 4 + // [EB Sequence (uint32 BE)] + - 4 + // [DB Height (uint32 BE)] + - 4 // [Entry Count (uint32 BE)] - - EBlockObjectLen = 32 // Entry hash or minute marker - - EBlockMinBodyLen = EBlockObjectLen * 2 // one entry hash & one minute marker - EBlockMinTotalLen = EBlockHeaderLen + EBlockMinBodyLen - - EBlockMaxBodyLen = math.MaxUint32 * EBlockObjectLen - EBlockMaxTotalLen = EBlockHeaderLen + EBlockMaxBodyLen -) - -// UnmarshalBinary unmarshals raw entry block data. -// -// Header -// [ChainID (Bytes32)] + -// [BodyMR (Bytes32)] + -// [PrevKeyMR (Bytes32)] + -// [PrevFullHash (Bytes32)] + -// [EB Sequence (uint32 BE)] + -// [DB Height (uint32 BE)] + -// [Object Count (uint32 BE)] -// -// Body -// [Object 0 (Bytes32)] + // entry hash or minute marker -// ... + -// [Object N (Bytes32)] -// -// https://github.com/FactomProject/FactomDocs/blob/master/factomDataStructureDetails.md#entry-block -func (eb *EBlock) UnmarshalBinary(data []byte) error { - if len(data) < EBlockMinTotalLen { - return fmt.Errorf("insufficient length") - } - if len(data) > EBlockMaxTotalLen { - return fmt.Errorf("invalid length") - } - if eb.ChainID == nil { - eb.ChainID = new(Bytes32) - } - eb.ChainID = eb.ChainID - i := copy(eb.ChainID[:], data[:len(eb.ChainID)]) - eb.BodyMR = new(Bytes32) - i += copy(eb.BodyMR[:], data[i:i+len(eb.BodyMR)]) - eb.PrevKeyMR = new(Bytes32) - i += copy(eb.PrevKeyMR[:], data[i:i+len(eb.PrevKeyMR)]) - eb.PrevFullHash = new(Bytes32) - i += copy(eb.PrevFullHash[:], data[i:i+len(eb.PrevFullHash)]) - eb.Sequence = binary.BigEndian.Uint32(data[i : i+4]) - i += 4 - eb.Height = binary.BigEndian.Uint32(data[i : i+4]) - i += 4 - eb.ObjectCount = binary.BigEndian.Uint32(data[i : i+4]) - i += 4 - if len(data[i:]) != int(eb.ObjectCount*32) { - return fmt.Errorf("invalid length") - } - - // Parse all objects into Bytes32 - objects := make([]Bytes32, eb.ObjectCount) - maxMinute := Bytes32{31: 10} - var numMins int - for oi := range objects { - obj := &objects[len(objects)-1-oi] // Reverse object order - i += copy(obj[:], data[i:i+len(obj)]) - if bytes.Compare(obj[:], maxMinute[:]) <= 0 { - numMins++ - } - } - if bytes.Compare(objects[0][:], maxMinute[:]) > 0 { - return fmt.Errorf("invalid minute marker") - } - - // Populate Entries from objects. - eb.Entries = make([]Entry, int(eb.ObjectCount)-numMins) - ei := len(eb.Entries) - 1 - var ts time.Time - for _, obj := range objects { - if bytes.Compare(obj[:], maxMinute[:]) <= 0 { - ts = eb.Timestamp. - Add(time.Duration(obj[31]) * time.Minute) - continue - } - e := &eb.Entries[ei] - e.Timestamp = ts - e.ChainID = eb.ChainID - e.Height = eb.Height - obj := obj - e.Hash = &obj - ei-- - } - return nil -} - -func (eb *EBlock) MarshalBinaryHeader() ([]byte, error) { - if !eb.IsPopulated() { - return nil, fmt.Errorf("not populated") - } - data := make([]byte, eb.MarshalBinaryLen()) - i := copy(data, eb.ChainID[:]) - i += copy(data[i:], eb.BodyMR[:]) - i += copy(data[i:], eb.PrevKeyMR[:]) - i += copy(data[i:], eb.PrevFullHash[:]) - binary.BigEndian.PutUint32(data[i:], eb.Sequence) - i += 4 - binary.BigEndian.PutUint32(data[i:], eb.Height) - i += 4 - binary.BigEndian.PutUint32(data[i:], eb.ObjectCount) - i += 4 - return data, nil -} - -func (eb *EBlock) MarshalBinary() ([]byte, error) { - data, err := eb.MarshalBinaryHeader() - if err != nil { - return nil, err - } - objects, err := eb.Objects() - if err != nil { - return nil, err - } - i := EBlockHeaderLen - for _, obj := range objects { - i += copy(data[i:], obj[:]) - } - return data, nil -} - -func (eb *EBlock) Objects() ([]Bytes32, error) { - objects := make([]Bytes32, eb.ObjectCount) - var lastMin, oi int - lastMin = int(eb.Entries[0].Timestamp.Sub(eb.Timestamp).Minutes()) - for _, e := range eb.Entries { - min := int(e.Timestamp.Sub(eb.Timestamp).Minutes()) - if min > 10 { - return nil, fmt.Errorf("invalid entry timestamp") - } - if min > lastMin { - objects[oi][31] = byte(lastMin) - oi++ - lastMin = min - } - objects[oi] = *e.Hash - oi++ - } - // Insert last minute marker - lastE := eb.Entries[len(eb.Entries)-1] - lastMin = int(lastE.Timestamp.Sub(eb.Timestamp).Minutes()) - objects[oi][31] = byte(lastMin) - oi++ - return objects, nil -} - -func (eb *EBlock) CountObjects() uint32 { - var numMins, lastMin int - for _, e := range eb.Entries { - min := int(e.Timestamp.Sub(eb.Timestamp).Minutes()) - if min > lastMin { - numMins++ - lastMin = min - } - } - return uint32(len(eb.Entries) + numMins) -} - -func (eb *EBlock) MarshalBinaryLen() int { - if eb.ObjectCount == 0 { - eb.ObjectCount = eb.CountObjects() - } - return EBlockHeaderLen + int(eb.ObjectCount)*len(Bytes32{}) -} - -func (eb EBlock) ComputeBodyMR() (Bytes32, error) { - var bodyMR Bytes32 - objects, err := eb.Objects() - if err != nil { - return Bytes32{}, err - } - blocks := make([][]byte, len(objects)) - for i := range objects { - blocks[i] = objects[i][:] - } - tree := merkle.NewTreeWithOpts(merkle.TreeOptions{ - DoubleOddNodes: true, - DisableHashLeaves: true}) - if err := tree.Generate(blocks, sha256.New()); err != nil { - return Bytes32{}, err - } - root := tree.Root() - copy(bodyMR[:], root.Hash) - return bodyMR, nil -} - -func (eb EBlock) ComputeFullHash() (Bytes32, error) { - data, err := eb.MarshalBinary() - if err != nil { - return Bytes32{}, err - } - return sha256.Sum256(data), nil -} - -func (eb EBlock) ComputeHeaderHash() (Bytes32, error) { - header, err := eb.MarshalBinaryHeader() - if err != nil { - return Bytes32{}, err - } - return sha256.Sum256(header[:EBlockHeaderLen]), nil -} - -func (eb EBlock) ComputeKeyMR() (Bytes32, error) { - headerHash, err := eb.ComputeHeaderHash() - if err != nil { - return Bytes32{}, err - } - bodyMR, err := eb.ComputeBodyMR() - if err != nil { - return Bytes32{}, err - } - data := make([]byte, len(headerHash)+len(bodyMR)) - i := copy(data, headerHash[:]) - copy(data[i:], bodyMR[:]) - return sha256.Sum256(data), nil -} diff --git a/factom/entry.go b/factom/entry.go deleted file mode 100644 index 4f4543b..0000000 --- a/factom/entry.go +++ /dev/null @@ -1,442 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "bytes" - "crypto/sha256" - "crypto/sha512" - "encoding/binary" - "encoding/json" - "fmt" - "math/rand" - "time" - - "golang.org/x/crypto/ed25519" -) - -// ChainID returns the chain ID for a set of NameIDs. -func ChainID(nameIDs []Bytes) Bytes32 { - hash := sha256.New() - for _, id := range nameIDs { - idSum := sha256.Sum256(id) - hash.Write(idSum[:]) - } - c := hash.Sum(nil) - var chainID Bytes32 - copy(chainID[:], c) - return chainID -} - -// Entry represents a Factom Entry with some additional useful fields. Both -// Timestamp and Height are not included in the entry binary structure or hash. -// These fields will only be populated if this Entry was initially part of a -// populated EBlock, and can be manipulated in this type without affecting the -// Entry Hash. -type Entry struct { - // EBlock.Get populates the Hash, Timestamp, ChainID, and Height. - Hash *Bytes32 `json:"entryhash,omitempty"` - Timestamp time.Time `json:"-"` - ChainID *Bytes32 `json:"chainid,omitempty"` - Height uint32 - - // Entry.Get populates the Content and ExtIDs. - ExtIDs []Bytes `json:"extids"` - Content Bytes `json:"content"` -} - -// IsPopulated returns true if e has already been successfully populated by a -// call to Get. IsPopulated returns false if e.ExtIDs, e.Content, or e.Hash are -// nil, or if e.Timestamp is zero. -func (e Entry) IsPopulated() bool { - return e.ExtIDs != nil && - e.Content != nil && - e.ChainID != nil && - e.Hash != nil -} - -// Get queries factomd for the entry corresponding to e.Hash, which must be not -// nil. After a successful call e.Content, e.ExtIDs, and e.Timestamp will be -// populated. -func (e *Entry) Get(c *Client) error { - // If the Hash is nil then we have nothing to query for. - if e.Hash == nil { - return fmt.Errorf("Hash is nil") - } - if e.ChainID == nil { - return fmt.Errorf("ChainID is nil") - } - // If the Entry is already populated then there is nothing to do. If - // the Hash is nil, we cannot populate it anyway. - if e.IsPopulated() { - return nil - } - params := struct { - Hash *Bytes32 `json:"hash"` - }{Hash: e.Hash} - var result struct { - Data Bytes `json:"data"` - } - if err := c.FactomdRequest("raw-data", params, &result); err != nil { - return err - } - if EntryHash(result.Data) != *e.Hash { - return fmt.Errorf("invalid hash") - } - return e.UnmarshalBinary(result.Data) -} - -type chainFirstEntryParams struct { - Entry *Entry `json:"firstentry"` -} -type composeChainParams struct { - Chain chainFirstEntryParams `json:"chain"` - EC ECAddress `json:"ecpub"` -} -type composeEntryParams struct { - Entry *Entry `json:"entry"` - EC ECAddress `json:"ecpub"` -} - -type composeJRPC struct { - Method string `json:"method"` - Params json.RawMessage `json:"params"` -} -type composeResult struct { - Commit composeJRPC `json:"commit"` - Reveal composeJRPC `json:"reveal"` -} -type commitResult struct { - TxID *Bytes32 -} - -// Create queries factom-walletd to compose and factomd to commit and reveal a -// new Entry or new Chain, if e.ChainID is nil. ec must exist in -// factom-walletd's keystore. -func (e *Entry) Create(c *Client, ec ECAddress) (*Bytes32, error) { - var params interface{} - var method string - if e.ChainID == nil { - method = "compose-chain" - params = composeChainParams{ - Chain: chainFirstEntryParams{Entry: e}, - EC: ec, - } - } else { - method = "compose-entry" - params = composeEntryParams{Entry: e, EC: ec} - } - result := composeResult{} - if err := c.WalletdRequest(method, params, &result); err != nil { - return nil, err - } - if len(result.Commit.Method) == 0 { - return nil, fmt.Errorf("Wallet request error: method: %#v", method) - } - - var commit commitResult - if err := c.FactomdRequest(result.Commit.Method, result.Commit.Params, - &commit); err != nil { - return nil, err - } - if err := c.FactomdRequest(result.Reveal.Method, result.Reveal.Params, - e); err != nil { - return nil, err - } - return commit.TxID, nil -} - -// ComposeCreate Composes e locally and then Commit and Reveals it using -// factomd. This does not make any calls to factom-walletd. The Transaction ID -// is returned. -func (e *Entry) ComposeCreate(c *Client, es EsAddress) (*Bytes32, error) { - var commit, reveal []byte - commit, reveal, txID, err := e.Compose(es) - if err != nil { - return nil, err - } - if err := c.Commit(commit); err != nil { - return txID, err - } - if err := c.Reveal(reveal); err != nil { - return txID, err - } - return txID, nil -} - -// Commit sends an entry or new chain commit to factomd. -func (c *Client) Commit(commit []byte) error { - var method string - switch len(commit) { - case commitLen: - method = "commit-entry" - case chainCommitLen: - method = "commit-chain" - default: - return fmt.Errorf("invalid length") - } - - params := struct { - Commit Bytes `json:"message"` - }{Commit: commit} - if err := c.FactomdRequest(method, params, nil); err != nil { - return err - } - return nil -} - -// Reveal reveals an entry or new chain entry to factomd. -func (c *Client) Reveal(reveal []byte) error { - params := struct { - Reveal Bytes `json:"entry"` - }{Reveal: reveal} - if err := c.FactomdRequest("reveal-entry", params, nil); err != nil { - return err - } - return nil -} - -const ( - commitLen = 1 + // version - 6 + // timestamp - 32 + // entry hash - 1 + // ec cost - 32 + // ec pub - 64 // sig - chainCommitLen = 1 + // version - 6 + // timestamp - 32 + // chain id hash - 32 + // commit weld - 32 + // entry hash - 1 + // ec cost - 32 + // ec pub - 64 // sig -) - -// Compose generates the commit and reveal data required to submit an entry to -// factomd. If e.ChainID is nil, then the ChainID is computed from the e.ExtIDs -// and a new chain commit is created. -func (e *Entry) Compose(es EsAddress) (commit []byte, reveal []byte, txID *Bytes32, - err error) { - var newChain bool - if e.ChainID == nil { - newChain = true - } - reveal, err = e.MarshalBinary() // Populates ChainID and Hash - if err != nil { - return - } - - size := commitLen - if newChain { - size = chainCommitLen - } - commit = make([]byte, size) - - // Timestamp - ms := time.Now(). - Add(time.Duration(-rand.Int63n(int64(1*time.Hour)))). - UnixNano() / 1e6 - buf := bytes.NewBuffer(make([]byte, 0, 8)) - binary.Write(buf, binary.BigEndian, ms) - i := 1 // Skip version byte - i += copy(commit[i:], buf.Bytes()[2:]) - - if newChain { - // ChainID Hash - chainIDHash := sha256d(e.ChainID[:]) - i += copy(commit[i:], chainIDHash[:]) - - // Commit Weld sha256d(entryhash | chainid) - weld := sha256d(append(e.Hash[:], e.ChainID[:]...)) - i += copy(commit[i:], weld[:]) - } - - // Entry Hash - i += copy(commit[i:], e.Hash[:]) - - // Cost - cost, _ := EntryCost(len(reveal)) - if newChain { - cost += NewChainCost - } - commit[i] = byte(cost) - i++ - signedDataEndIndex := i - txID = new(Bytes32) - *txID = sha256.Sum256(commit[:i]) - - // Public Key - i += copy(commit[i:], es.PublicKey()) - - // Signature - sig := ed25519.Sign(es.PrivateKey(), commit[:signedDataEndIndex]) - copy(commit[i:], sig) - - return -} - -// NewChainCost is the fixed added cost of creating a new chain. -const NewChainCost = 10 - -// EntryCost returns the required Entry Credit cost for an entry with encoded -// length equal to size. An error is returned if size exceeds 10275. -func EntryCost(size int) (int8, error) { - size -= EntryHeaderLen - if size > 10240 { - return 0, fmt.Errorf("Entry cannot be larger than 10KB") - } - cost := int8(size / 1024) - if size%1024 > 0 { - cost++ - } - if cost < 1 { - cost = 1 - } - return cost, nil -} - -func (e Entry) Cost() (int8, error) { - cost, err := EntryCost(e.MarshalBinaryLen()) - if err != nil { - return 0, err - } - if e.ChainID == nil { - cost += NewChainCost - } - return cost, nil -} - -func (e Entry) MarshalBinaryLen() int { - extIDTotalLen := len(e.ExtIDs) * 2 // Two byte len(ExtID) per ExtID - for _, extID := range e.ExtIDs { - extIDTotalLen += len(extID) - } - return extIDTotalLen + len(e.Content) + EntryHeaderLen -} - -// MarshalBinary marshals the entry to its binary representation. See -// UnmarshalBinary for encoding details. MarshalBinary populates e.ChainID if -// nil, and always overwrites e.Hash with the computed EntryHash. This is also -// the reveal data. -func (e *Entry) MarshalBinary() ([]byte, error) { - totalLen := e.MarshalBinaryLen() - if totalLen > EntryMaxTotalLen { - return nil, fmt.Errorf("Entry cannot be larger than 10KB") - } - if e.ChainID == nil { - e.ChainID = new(Bytes32) - *e.ChainID = ChainID(e.ExtIDs) - } - // Header, version byte 0x00 - data := make([]byte, totalLen) - i := 1 - i += copy(data[i:], e.ChainID[:]) - binary.BigEndian.PutUint16(data[i:i+2], - uint16(totalLen-len(e.Content)-EntryHeaderLen)) - i += 2 - - // Payload - for _, extID := range e.ExtIDs { - n := len(extID) - binary.BigEndian.PutUint16(data[i:i+2], uint16(n)) - i += 2 - i += copy(data[i:], extID) - } - copy(data[i:], e.Content) - // Compute and save entry hash for later use - e.Hash = new(Bytes32) - *e.Hash = EntryHash(data) - return data, nil -} - -const ( - EntryHeaderLen = 1 + // version - 32 + // chain id - 2 // total len - - EntryMaxDataLen = 10240 - EntryMaxTotalLen = EntryMaxDataLen + EntryHeaderLen -) - -// UnmarshalBinary unmarshals raw entry data. It does not populate the -// Entry.Hash. Entries are encoded as follows: -// -// [Version byte (0x00)] + -// [ChainID (Bytes32)] + -// [Total ExtID encoded length (uint16 BE)] + -// [ExtID 0 length (uint16)] + [ExtID 0 (Bytes)] + -// ... + -// [ExtID X length (uint16)] + [ExtID X (Bytes)] + -// [Content (Bytes)] -// -// https://github.com/FactomProject/FactomDocs/blob/master/factomDataStructureDetails.md#entry -func (e *Entry) UnmarshalBinary(data []byte) error { - if len(data) < EntryHeaderLen { - return fmt.Errorf("insufficient length") - } - if len(data) > EntryMaxTotalLen { - return fmt.Errorf("invalid length") - } - if data[0] != 0x00 { - return fmt.Errorf("invalid version byte") - } - chainID := data[1:33] - extIDTotalLen := int(binary.BigEndian.Uint16(data[33:35])) - if extIDTotalLen == 1 || EntryHeaderLen+extIDTotalLen > len(data) { - return fmt.Errorf("invalid ExtIDs length") - } - - extIDs := []Bytes{} - pos := EntryHeaderLen - for pos < EntryHeaderLen+extIDTotalLen { - extIDLen := int(binary.BigEndian.Uint16(data[pos : pos+2])) - if pos+2+extIDLen > EntryHeaderLen+extIDTotalLen { - return fmt.Errorf("error parsing ExtIDs") - } - pos += 2 - extIDs = append(extIDs, Bytes(data[pos:pos+extIDLen])) - pos += extIDLen - } - e.Content = data[pos:] - e.ExtIDs = extIDs - e.ChainID = NewBytes32(chainID) - return nil -} - -// ComputeHash returns the Entry's hash as computed by hashing the binary -// representation of the Entry. -func (e Entry) ComputeHash() (Bytes32, error) { - data, err := e.MarshalBinary() - return EntryHash(data), err -} - -// EntryHash returns the Entry hash of data. Entry's are hashed via: -// sha256(sha512(data) + data). -func EntryHash(data []byte) Bytes32 { - sum := sha512.Sum512(data) - saltedSum := make([]byte, len(sum)+len(data)) - i := copy(saltedSum, sum[:]) - copy(saltedSum[i:], data) - return sha256.Sum256(saltedSum) -} diff --git a/factom/entry_test.go b/factom/entry_test.go deleted file mode 100644 index f98bb27..0000000 --- a/factom/entry_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom_test - -import ( - "encoding/hex" - "fmt" - "testing" - - "github.com/AdamSLevy/jsonrpc2/v11" - . "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var marshalBinaryTests = []struct { - Name string - Hash *Bytes32 - Entry -}{{ - Name: "valid", - Entry: Entry{ - Hash: NewBytes32(hexToBytes( - "72177d733dcd0492066b79c5f3e417aef7f22909674f7dc351ca13b04742bb91")), - ChainID: func() *Bytes32 { c := ChainID([]Bytes{Bytes("test")}); return &c }(), - Content: hexToBytes("5061796c6f616448657265"), - }, -}} - -func TestEntryMarshalBinary(t *testing.T) { - for _, test := range marshalBinaryTests { - t.Run(test.Name, func(t *testing.T) { - e := test.Entry - hash, err := e.ComputeHash() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(*e.Hash, hash) - }) - } -} - -var unmarshalBinaryTests = []struct { - Name string - Data []byte - Error string - Hash *Bytes32 -}{{ - Name: "valid", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc20757700530009436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002c4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Hash: NewBytes32(hexToBytes( - "a5e49c1c14762f067b4132c5aa3abf03efdf2569de5d68a3f7cd539577f54942")), -}, { - Name: "invalid (too short)", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc207577"), - Error: "insufficient length", -}, { - Name: "invalid (version byte)", - Data: hexToBytes( - "019005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc20757700530009436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002c4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Error: "invalid version byte", -}, { - Name: "invalid (ext ID Total Len)", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc20757700010009436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002c4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Error: "invalid ExtIDs length", -}, { - Name: "invalid (ext ID Total Len)", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc207577ffff0009436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002c4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Error: "invalid ExtIDs length", -}, { - Name: "invalid (ext ID len)", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc20757700530008436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002c4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Error: "error parsing ExtIDs", -}, { - Name: "invalid (ext ID len)", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc2075770053000a436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002c4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Error: "error parsing ExtIDs", -}, { - Name: "invalid (ext ID len)", - Data: hexToBytes( - "009005bb7dd69fb9910ee0b0db7b8a01198f03623eab6dadf1eba01f9dbc20757700530009436861696e54797065001253494e474c455f50524f4f465f434841494e000448617368002b4a74446f413157476a784f63584a67496365574e6336396a5551524867506835414e337848646b6a7158303d48796742426b32317a79384c576e5a56785a48526c38706b502f366e34377546317664324a4378654238593d"), - Error: "error parsing ExtIDs", -}} - -func TestEntry(t *testing.T) { - for _, test := range unmarshalBinaryTests { - t.Run("UnmarshalBinary/"+test.Name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - e := Entry{} - err := e.UnmarshalBinary(test.Data) - if len(test.Error) == 0 { - require.NoError(err) - require.NotNil(e.ChainID) - hash, err := e.ComputeHash() - assert.NoError(err) - assert.Equal(*test.Hash, hash) - } else { - require.EqualError(err, test.Error) - assert.Nil(e.ChainID) - assert.Nil(e.Content) - assert.Nil(e.ExtIDs) - } - }) - } - - var ecAddressStr = "EC1zANmWuEMYoH6VizJg6uFaEdi8Excn1VbLN99KRuxh3GSvB7YQ" - ec, _ := NewECAddress(ecAddressStr) - chainID := ChainID([]Bytes{Bytes(ec[:])}) - t.Run("ComposeCreate", func(t *testing.T) { - c := NewClient() - es, err := ec.GetEsAddress(c) - if _, ok := err.(jsonrpc2.Error); ok { - // Skip if the EC address is not in the wallet. - t.SkipNow() - } - assert := assert.New(t) - assert.NoError(err) - balance, err := ec.GetBalance(c) - assert.NoError(err) - if balance == 0 { - // Skip if the EC address is not funded. - t.SkipNow() - } - - randData, err := GenerateEsAddress() - assert.NoError(err) - e := Entry{Content: Bytes(randData[:]), - ExtIDs: []Bytes{Bytes(ec[:])}, - ChainID: &chainID} - tx, err := e.ComposeCreate(c, es) - assert.NoError(err) - assert.NotNil(tx) - fmt.Println("Tx: ", tx) - fmt.Println("Entry Hash: ", e.Hash) - fmt.Println("Chain ID: ", e.ChainID) - - e.ChainID = nil - e.Content = Bytes(randData[:]) - e.ExtIDs = []Bytes{Bytes(randData[:])} - tx, err = e.ComposeCreate(c, es) - assert.NoError(err) - assert.NotNil(tx) - fmt.Println("Tx: ", tx) - fmt.Println("Entry Hash: ", e.Hash) - fmt.Println("Chain ID: ", e.ChainID) - }) - t.Run("Create", func(t *testing.T) { - c := NewClient() - c.Factomd.DebugRequest = true - c.Walletd.DebugRequest = true - balance, err := ec.GetBalance(c) - assert := assert.New(t) - require := require.New(t) - require.NoError(err) - if balance == 0 { - // Skip if the EC address is not funded. - t.SkipNow() - } - - randData, err := GenerateEsAddress() - assert.NoError(err) - e := Entry{Content: Bytes(randData[:]), - ExtIDs: []Bytes{Bytes(ec[:])}, - ChainID: &chainID} - tx, err := e.Create(c, ec) - assert.NoError(err) - assert.NotNil(tx) - fmt.Println("Tx: ", tx) - fmt.Println("Entry Hash: ", e.Hash) - fmt.Println("Chain ID: ", e.ChainID) - - e.ChainID = nil - e.Content = Bytes(randData[:]) - e.ExtIDs = []Bytes{Bytes(randData[:])} - tx, err = e.Create(c, ec) - assert.NoError(err) - assert.NotNil(tx) - fmt.Println("Tx: ", tx) - fmt.Println("Entry Hash: ", e.Hash) - fmt.Println("Chain ID: ", e.ChainID) - }) - t.Run("Compose/too large", func(t *testing.T) { - assert := assert.New(t) - e := Entry{Content: make(Bytes, 11000), - ExtIDs: []Bytes{Bytes(ec[:])}, - ChainID: &chainID} - _, _, _, err := e.Compose(EsAddress(ec)) - assert.EqualError(err, "Entry cannot be larger than 10KB") - }) - t.Run("EntryCost", func(t *testing.T) { - assert := assert.New(t) - _, err := EntryCost(11000) - assert.EqualError(err, "Entry cannot be larger than 10KB") - cost, _ := EntryCost(0) - assert.Equal(int8(1), cost) - }) -} - -func hexToBytes(hexStr string) Bytes { - raw, err := hex.DecodeString(hexStr) - if err != nil { - panic(err) - } - return Bytes(raw) -} diff --git a/factom/genmain.go b/factom/genmain.go deleted file mode 100644 index 93da74f..0000000 --- a/factom/genmain.go +++ /dev/null @@ -1,92 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// +build ignore - -package main - -import ( - "log" - "os" - . "text/template" -) - -var idKeys = []struct { - ID int - IDPrefix string - SKPrefix string - IDStr string - SKStr string -}{{ - ID: 1, - IDPrefix: "0x3f, 0xbe, 0xba", - SKPrefix: "0x4d, 0xb6, 0xc9", - IDStr: "id12K4tCXKcJJYxJmZ1UY9EuKPvtGVAjo32xySMKNUahbmRcsqFgW", - SKStr: "sk13iLKJfxNQg8vpSmjacEgEQAnXkn7rbjd5ewexc1Un5wVPa7KTk", -}, { - ID: 2, - IDPrefix: "0x3f, 0xbe, 0xd8", - SKPrefix: "0x4d, 0xb6, 0xe7", - IDStr: "id22pNvsaMWf9qxWFrmfQpwFJiKQoWfKmBwVgQtdvqVZuqzGmrFNY", - SKStr: "sk22UaDys2Mzg2pUCsToo9aKgxubJFnZN5Bc2LXfV59VxMvXXKwXa", -}, { - ID: 3, - IDPrefix: "0x3f, 0xbe, 0xf6", - SKPrefix: "0x4d, 0xb7, 0x05", - IDStr: "id33pRgpm8ufXNGxtW7n5FgdGP6afXKjU4LfVmgfC8Yaq6LyYq2wA", - SKStr: "sk32Xyo9kmjtNqRUfRd3ZhU56NZd8M1nR61tdBaCLSQRdhUCk4yiM", -}, { - ID: 4, - IDPrefix: "0x3f, 0xbf, 0x14", - SKPrefix: "0x4d, 0xb7, 0x23", - IDStr: "id42vYqBB63eoSz8DHozEwtCaLbEwvBTG9pWgD3D5CCaHWy1gCjF5", - SKStr: "sk43eMusQuvvChoGNn1VZZwbAH8BtKJSZNC7ZWoz1Vc4Y3greLA45", -}} - -func main() { - idKeyGoTmplt := Must(ParseFiles("./idkey.tmpl")) - idKeyTestGoTmplt := Must(ParseFiles("./idkey_test.tmpl")) - - idKeyGoFile, err := os.OpenFile("./idkey_gen.go", - os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - log.Fatal(err) - } - defer idKeyGoFile.Close() - - idKeyTestGoFile, err := os.OpenFile("./idkey_gen_test.go", - os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - log.Fatal(err) - } - defer idKeyTestGoFile.Close() - - err = idKeyGoTmplt.Execute(idKeyGoFile, idKeys) - if err != nil { - log.Fatal(err) - } - - err = idKeyTestGoTmplt.Execute(idKeyTestGoFile, idKeys) - if err != nil { - log.Fatal(err) - } -} diff --git a/factom/heights.go b/factom/heights.go deleted file mode 100644 index 7ed5797..0000000 --- a/factom/heights.go +++ /dev/null @@ -1,54 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -// Heights contains all of the distinct heights for a factomd node and the -// Factom network. -type Heights struct { - // The current directory block height of the local factomd node. - DirectoryBlock uint32 `json:"directoryblockheight"` - - // The current block being worked on by the leaders in the network. - // This block is not yet complete, but all transactions submitted will - // go into this block (depending on network conditions, the transaction - // may be delayed into the next block) - Leader uint32 `json:"leaderheight"` - - // The height at which the factomd node has all the entry blocks. - // Directory blocks are obtained first, entry blocks could be lagging - // behind the directory block when syncing. - EntryBlock uint32 `json:"entryblockheight"` - - // The height at which the local factomd node has all the entries. If - // you added entries at a block height above this, they will not be - // able to be retrieved by the local factomd until it syncs further. - Entry uint32 `json:"entryheight"` -} - -// Get uses c to call the "heights" RPC method and populates h with the result. -func (h *Heights) Get(c *Client) error { - if err := c.FactomdRequest("heights", nil, &h); err != nil { - return err - } - return nil -} diff --git a/factom/identity.go b/factom/identity.go deleted file mode 100644 index 1eb26e5..0000000 --- a/factom/identity.go +++ /dev/null @@ -1,110 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import "fmt" - -// ValidIdentityChainID returns true if the chainID matches the pattern for an -// Identity Chain ID. -// -// The Identity Chain specification can be found here: -// https://github.com/FactomProject/FactomDocs/blob/master/Identity.md#factom-identity-chain-creation -func ValidIdentityChainID(chainID Bytes) bool { - if len(chainID) == len(Bytes32{}) && - chainID[0] == 0x88 && - chainID[1] == 0x88 && - chainID[2] == 0x88 { - return true - } - return false -} - -// ValidIdentityNameIDs returns true if the nameIDs match the pattern for a -// valid Identity Chain. The nameIDs for a chain are the ExtIDs of the first -// entry in the chain. -// -// The Identity Chain specification can be found here: -// https://github.com/FactomProject/FactomDocs/blob/master/Identity.md#factom-identity-chain-creation -func ValidIdentityNameIDs(nameIDs []Bytes) bool { - if len(nameIDs) == 7 && - len(nameIDs[0]) == 1 && nameIDs[0][0] == 0x00 && - string(nameIDs[1]) == "Identity Chain" && - len(nameIDs[2]) == len(ID1Key{}) && - len(nameIDs[3]) == len(ID2Key{}) && - len(nameIDs[4]) == len(ID3Key{}) && - len(nameIDs[5]) == len(ID4Key{}) { - return true - } - return false -} - -// Identity represents the Token Issuer's Identity Chain and the public ID1Key. -type Identity struct { - ID1 ID1Key - Entry -} - -// NewIdentity initializes an Identity with the given chainID. -func NewIdentity(chainID *Bytes32) (i Identity) { - i.ChainID = chainID - return -} - -// IsPopulated returns true if the Identity has been populated with an ID1Key. -func (i Identity) IsPopulated() bool { - return i.ID1 != ID1Key(zeroBytes32) -} - -// Get validates i.ChainID as an Identity Chain and parses out the ID1Key. -func (i *Identity) Get(c *Client) error { - if i.ChainID == nil { - return fmt.Errorf("ChainID is nil") - } - if i.IsPopulated() { - return nil - } - if !ValidIdentityChainID(i.ChainID[:]) { - return nil - } - - // Get first entry block of Identity Chain. - eb := EBlock{ChainID: i.ChainID} - if err := eb.GetFirst(c); err != nil { - return err - } - - // Get first entry of first entry block. - first := eb.Entries[0] - if err := first.Get(c); err != nil { - return err - } - - if !ValidIdentityNameIDs(first.ExtIDs) { - return nil - } - - i.Entry = first - copy(i.ID1[:], first.ExtIDs[2]) - - return nil -} diff --git a/factom/identity_test.go b/factom/identity_test.go deleted file mode 100644 index 9854c87..0000000 --- a/factom/identity_test.go +++ /dev/null @@ -1,242 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "encoding/hex" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var validIdentityChainIDStr = "88888807e4f3bbb9a2b229645ab6d2f184224190f83e78761674c2362aca4425" - -func validIdentityChainID() Bytes { - return hexToBytes(validIdentityChainIDStr) -} - -func hexToBytes(hexStr string) Bytes { - raw, err := hex.DecodeString(hexStr) - if err != nil { - panic(err) - } - return Bytes(raw) -} - -func newID1Key(b []byte) *ID1Key { - key := new(ID1Key) - copy(key[:], b) - return key -} - -var validIdentityChainIDTests = []struct { - Name string - Valid bool - ChainID Bytes -}{{ - Name: "valid", - ChainID: validIdentityChainID(), - Valid: true, -}, { - Name: "nil", - ChainID: nil, -}, { - Name: "invalid length (short)", - ChainID: validIdentityChainID()[0:15], -}, { - Name: "invalid length (long)", - ChainID: append(validIdentityChainID(), 0x00), -}, { - Name: "invalid header", - ChainID: func() Bytes { c := validIdentityChainID(); c[0]++; return c }(), -}, { - Name: "invalid header", - ChainID: func() Bytes { c := validIdentityChainID(); c[1]++; return c }(), -}, { - Name: "invalid header", - ChainID: func() Bytes { c := validIdentityChainID(); c[2]++; return c }(), -}} - -func TestValidIdentityChainID(t *testing.T) { - for _, test := range validIdentityChainIDTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - valid := ValidIdentityChainID(test.ChainID) - if test.Valid { - assert.True(valid) - } else { - assert.False(valid) - } - }) - } -} - -func validIdentityNameIDs() []Bytes { - return []Bytes{ - Bytes{0x00}, - Bytes("Identity Chain"), - hexToBytes("f825c5629772afb5bce0464e5ea1af244be853a692d16360b8e03d6164b6adb5"), - hexToBytes("28baa7d04e6c102991a184533b9f2443c9c314cc0327cc3a2f2adc0f3d7373a1"), - hexToBytes("6095733cf6f5d0b5411d1eeb9f6699fad1ae27f9d4da64583bef97008d7bf0c9"), - hexToBytes("966ebc2a0e3877ed846167e95ba3dde8561d90ee9eddd1bb74fbd6d1d25dba0f"), - hexToBytes("33363533323533"), - } -} - -func invalidIdentityNameIDs(i int) []Bytes { - n := validIdentityNameIDs() - n[i] = Bytes{} - return n -} - -var validIdentityNameIDsTests = []struct { - Name string - Valid bool - NameIDs []Bytes -}{{ - Name: "valid", - NameIDs: validIdentityNameIDs(), - Valid: true, -}, { - Name: "nil", - NameIDs: nil, -}, { - Name: "invalid length (short)", - NameIDs: validIdentityNameIDs()[0:6], -}, { - Name: "invalid length (long)", - NameIDs: append(validIdentityNameIDs(), Bytes{}), -}, { - Name: "invalid length (long)", - NameIDs: append(validIdentityNameIDs(), Bytes{}), -}, { - Name: "invalid ExtID", - NameIDs: invalidIdentityNameIDs(0), -}, { - Name: "invalid ExtID", - NameIDs: invalidIdentityNameIDs(1), -}, { - Name: "invalid ExtID", - NameIDs: invalidIdentityNameIDs(2), -}, { - Name: "invalid ExtID", - NameIDs: invalidIdentityNameIDs(3), -}, { - Name: "invalid ExtID", - NameIDs: invalidIdentityNameIDs(4), -}, { - Name: "invalid ExtID", - NameIDs: invalidIdentityNameIDs(5), -}} - -func TestValidIdentityNameIDs(t *testing.T) { - for _, test := range validIdentityNameIDsTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - valid := ValidIdentityNameIDs(test.NameIDs) - if test.Valid { - assert.True(valid) - } else { - assert.False(valid) - } - }) - } -} - -func validIdentity() (i Identity) { - i.ChainID = NewBytes32(validIdentityChainID()) - return -} - -var identityTests = []struct { - Name string - FactomServer string - Valid bool - Error string - Height uint64 - ID1Key *ID1Key - Identity -}{{ - Name: "valid", - Valid: true, - Identity: validIdentity(), - Height: 140744, - ID1Key: newID1Key(hexToBytes( - "9656dbf91feb7d464971f31b28bfbf38ab201b8e33ec69ea4681e3bef779858e")), -}, { - Name: "nil chain ID", - Error: "ChainID is nil", - Identity: Identity{}, -}, { - Name: "bad factomd endpoint", - FactomServer: "http://localhost:1000", - Identity: validIdentity(), - Error: "Post http://localhost:1000/v2: dial tcp [::1]:1000: connect: connection refused", -}, { - Name: "malformed chain", - Identity: NewIdentity(NewBytes32(hexToBytes( - "8888885c2e0b523d9b8ab6d2975639e431eaba3fc9039ead32ce5065dcde86e4"))), -}, { - Name: "invalid chain id", - Identity: NewIdentity(NewBytes32(hexToBytes( - "0088885c2e0b523d9b8ab6d2975639e431eaba3fc9039ead32ce5065dcde86e4"))), -}, { - Name: "non-existent chain id", - Identity: NewIdentity(NewBytes32(hexToBytes( - "8888880000000000000000000000000000000000000000000000000000000000"))), - Error: `jsonrpc2.Error{Code:-32009, Message:"Missing Chain Head"}`, -}} - -var factomServer = "https://courtesy-node.factom.com" - -var c = NewClient() - -func TestIdentity(t *testing.T) { - for _, test := range identityTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - if len(test.FactomServer) == 0 { - test.FactomServer = factomServer - } - c.FactomdServer = test.FactomServer - i := test.Identity - err := i.Get(c) - populated := i.IsPopulated() - if len(test.Error) > 0 { - assert.EqualError(err, test.Error) - } else { - require.NoError(err) - } - if !test.Valid { - assert.False(populated) - return - } - assert.True(populated) - assert.Equal(int(test.Height), int(i.Height)) - assert.Equal(*test.ID1Key, i.ID1) - assert.NoError(i.Get(c)) - }) - } -} diff --git a/factom/idkey.go b/factom/idkey.go deleted file mode 100644 index a3eb228..0000000 --- a/factom/idkey.go +++ /dev/null @@ -1,70 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "crypto/sha256" - - "golang.org/x/crypto/ed25519" -) - -// IDKey is the interface implemented by the four ID and SK Key types. -type IDKey interface { - // PrefixBytes returns the prefix bytes for the key. - PrefixBytes() []byte - // PrefixString returns the encoded prefix string for the key. - PrefixString() string - - // String encodes the key to a base58check string with the appropriate - // prefix. - String() string - // Payload returns the key as a byte array. - Payload() [sha256.Size]byte - // RCDHash returns the RCDHash as a byte array. For IDxKeys, this is - // identical to Payload. For SKxKeys the RCDHash is computed. - RCDHash() [sha256.Size]byte - - // IDKey returns the corresponding IDxKey in an IDKey interface. - // IDxKeys return themselves. Private SKxKeys compute the - // corresponding IDxKey. - IDKey() IDKey -} - -// SKKey is the interface implemented by the four SK Key types. -type SKKey interface { - IDKey - - // SKKey returns the SKKey interface. IDxKeys return themselves. - // Private SKxKeys compute the corresponding IDxKey. - SKKey() SKKey - - // RCD returns the RCD corresponding to the private key. - RCD() []byte - - // PrivateKey returns the ed25519.PrivateKey which can be used for - // signing data. - PrivateKey() ed25519.PrivateKey - // PublicKey returns the ed25519.PublicKey which can be used for - // verifying signatures. - PublicKey() ed25519.PublicKey -} diff --git a/factom/idkey.tmpl b/factom/idkey.tmpl deleted file mode 100644 index 74f67cc..0000000 --- a/factom/idkey.tmpl +++ /dev/null @@ -1,250 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Code generated DO NOT EDIT - -package factom - -// Defines IDKeys ID1Key - ID4Key and corresponding SKKeys SK1Key - SK4Key. - -var ( -{{range . -}} - id{{.ID}}PrefixBytes = [...]byte{ {{.IDPrefix}} } -{{end}} - -{{range . -}} - sk{{.ID}}PrefixBytes = [...]byte{ {{.SKPrefix}} } -{{end}} -) - -const ( -{{range . -}} - id{{.ID}}PrefixStr = "id{{.ID}}" -{{end}} - -{{range . -}} - sk{{.ID}}PrefixStr = "sk{{.ID}}" -{{end}} -) - -var ( -{{range . -}} - _ IDKey = ID{{.ID}}Key{} -{{end}} - -{{range . -}} - _ SKKey = SK{{.ID}}Key{} -{{end}} -) - -{{range .}} -// ID{{.ID}}Key is the id{{.ID}} public key for an identity. -type ID{{.ID}}Key [sha256.Size]byte - -// SK{{.ID}}Key is the sk{{.ID}} secret key for an identity. -type SK{{.ID}}Key [sha256.Size]byte - -// Payload returns key as a byte array. -func (key ID{{.ID}}Key) Payload() [sha256.Size]byte { - return key -} - -// Payload returns key as a byte array. -func (key SK{{.ID}}Key) Payload() [sha256.Size]byte { - return key -} - -// payload returns adr as payload. This is syntactic sugar useful in other -// methods that leverage payload. -func (key ID{{.ID}}Key) payload() payload { - return payload(key) -} -func (key SK{{.ID}}Key) payload() payload { - return payload(key) -} - -// payloadPtr returns adr as *payload. This is syntactic sugar useful in other -// methods that leverage *payload. -func (key *ID{{.ID}}Key) payloadPtr() *payload { - return (*payload)(key) -} -func (key *SK{{.ID}}Key) payloadPtr() *payload { - return (*payload)(key) -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{ {{- .IDPrefix -}} }. -func (ID{{.ID}}Key) PrefixBytes() []byte { - prefix := id{{.ID}}PrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{ {{- .SKPrefix -}} }. -func (SK{{.ID}}Key) PrefixBytes() []byte { - prefix := sk{{.ID}}PrefixBytes - return prefix[:] -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "id{{.ID}}". -func (ID{{.ID}}Key) PrefixString() string { - return id{{.ID}}PrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "sk{{.ID}}". -func (SK{{.ID}}Key) PrefixString() string { - return sk{{.ID}}PrefixStr -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key ID{{.ID}}Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key SK{{.ID}}Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (ID{{.ID}}Key) Type() string { - return id{{.ID}}PrefixStr -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (SK{{.ID}}Key) Type() string { - return sk{{.ID}}PrefixStr -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key ID{{.ID}}Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key SK{{.ID}}Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// NewID{{.ID}}Key attempts to parse keyStr into a new ID{{.ID}}Key. -func NewID{{.ID}}Key(keyStr string) (key ID{{.ID}}Key, err error) { - err = key.Set(keyStr) - return -} - -// NewSK{{.ID}}Key attempts to parse keyStr into a new SK{{.ID}}Key. -func NewSK{{.ID}}Key(keyStr string) (key SK{{.ID}}Key, err error) { - err = key.Set(keyStr) - return -} - -// GenerateSK{{.ID}}Key generates a secure random private Entry Credit address using -// crypto/rand.Random as the source of randomness. -func GenerateSK{{.ID}}Key() (SK{{.ID}}Key, error) { - return generatePrivKey() -} - -// Set attempts to parse keyStr into key. -func (key *ID{{.ID}}Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// Set attempts to parse keyStr into key. -func (key *SK{{.ID}}Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable id{{.ID}} key into key. -func (key *ID{{.ID}}Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable sk{{.ID}} key into key. -func (key *SK{{.ID}}Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// IDKey returns key as an IDKey. -func (key ID{{.ID}}Key) IDKey() IDKey { - return key -} - -// IDKey returns the ID{{.ID}}Key corresponding to key as an IDKey. -func (key SK{{.ID}}Key) IDKey() IDKey { - return key.ID{{.ID}}Key() -} - -// SKKey returns key as an SKKey. -func (key SK{{.ID}}Key) SKKey() SKKey { - return key -} - -// ID{{.ID}}Key computes the ID{{.ID}}Key corresponding to key. -func (key SK{{.ID}}Key) ID{{.ID}}Key() ID{{.ID}}Key { - return key.RCDHash() -} - -// RCDHash returns the RCD hash encoded in key. -func (key ID{{.ID}}Key) RCDHash() [sha256.Size]byte { - return key -} - -// RCDHash computes the RCD hash corresponding to key. -func (key SK{{.ID}}Key) RCDHash() [sha256.Size]byte { - return sha256d(key.RCD()) -} - -// RCD computes the RCD for key. -func (key SK{{.ID}}Key) RCD() []byte { - return append([]byte{RCDType}, key.PublicKey()[:]...) -} - -// PublicKey computes the ed25519.PublicKey for key. -func (key SK{{.ID}}Key) PublicKey() ed25519.PublicKey { - return key.PrivateKey().Public().(ed25519.PublicKey) -} - -// PrivateKey returns the ed25519.PrivateKey for key. -func (key SK{{.ID}}Key) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(key[:]) -} - -// Scan implements sql.Scanner for key using Bytes32.Scan. The ID{{.ID}}Key type is -// not encoded and is assumed. -func (key *ID{{.ID}}Key) Scan(v interface{}) error { - return (*Bytes32)(key).Scan(v) -} - -// Value implements driver.Valuer for key using Bytes32.Value. The ID{{.ID}}Key type -// is not encoded. -func (key ID{{.ID}}Key) Value() (driver.Value, error) { - return (Bytes32)(key).Value() -} -{{end}} diff --git a/factom/idkey_gen.go b/factom/idkey_gen.go deleted file mode 100644 index 3ee389d..0000000 --- a/factom/idkey_gen.go +++ /dev/null @@ -1,834 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Code generated DO NOT EDIT - -package factom - -import ( - "crypto/sha256" - "database/sql/driver" - - "golang.org/x/crypto/ed25519" -) - -// Defines IDKeys ID1Key - ID4Key and corresponding SKKeys SK1Key - SK4Key. - -var ( - id1PrefixBytes = [...]byte{0x3f, 0xbe, 0xba} - id2PrefixBytes = [...]byte{0x3f, 0xbe, 0xd8} - id3PrefixBytes = [...]byte{0x3f, 0xbe, 0xf6} - id4PrefixBytes = [...]byte{0x3f, 0xbf, 0x14} - - sk1PrefixBytes = [...]byte{0x4d, 0xb6, 0xc9} - sk2PrefixBytes = [...]byte{0x4d, 0xb6, 0xe7} - sk3PrefixBytes = [...]byte{0x4d, 0xb7, 0x05} - sk4PrefixBytes = [...]byte{0x4d, 0xb7, 0x23} -) - -const ( - id1PrefixStr = "id1" - id2PrefixStr = "id2" - id3PrefixStr = "id3" - id4PrefixStr = "id4" - - sk1PrefixStr = "sk1" - sk2PrefixStr = "sk2" - sk3PrefixStr = "sk3" - sk4PrefixStr = "sk4" -) - -var ( - _ IDKey = ID1Key{} - _ IDKey = ID2Key{} - _ IDKey = ID3Key{} - _ IDKey = ID4Key{} - - _ SKKey = SK1Key{} - _ SKKey = SK2Key{} - _ SKKey = SK3Key{} - _ SKKey = SK4Key{} -) - -// ID1Key is the id1 public key for an identity. -type ID1Key [sha256.Size]byte - -// SK1Key is the sk1 secret key for an identity. -type SK1Key [sha256.Size]byte - -// Payload returns key as a byte array. -func (key ID1Key) Payload() [sha256.Size]byte { - return key -} - -// Payload returns key as a byte array. -func (key SK1Key) Payload() [sha256.Size]byte { - return key -} - -// payload returns adr as payload. This is syntactic sugar useful in other -// methods that leverage payload. -func (key ID1Key) payload() payload { - return payload(key) -} -func (key SK1Key) payload() payload { - return payload(key) -} - -// payloadPtr returns adr as *payload. This is syntactic sugar useful in other -// methods that leverage *payload. -func (key *ID1Key) payloadPtr() *payload { - return (*payload)(key) -} -func (key *SK1Key) payloadPtr() *payload { - return (*payload)(key) -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x3f, 0xbe, 0xba}. -func (ID1Key) PrefixBytes() []byte { - prefix := id1PrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x4d, 0xb6, 0xc9}. -func (SK1Key) PrefixBytes() []byte { - prefix := sk1PrefixBytes - return prefix[:] -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "id1". -func (ID1Key) PrefixString() string { - return id1PrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "sk1". -func (SK1Key) PrefixString() string { - return sk1PrefixStr -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key ID1Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key SK1Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (ID1Key) Type() string { - return id1PrefixStr -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (SK1Key) Type() string { - return sk1PrefixStr -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key ID1Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key SK1Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// NewID1Key attempts to parse keyStr into a new ID1Key. -func NewID1Key(keyStr string) (key ID1Key, err error) { - err = key.Set(keyStr) - return -} - -// NewSK1Key attempts to parse keyStr into a new SK1Key. -func NewSK1Key(keyStr string) (key SK1Key, err error) { - err = key.Set(keyStr) - return -} - -// GenerateSK1Key generates a secure random private Entry Credit address using -// crypto/rand.Random as the source of randomness. -func GenerateSK1Key() (SK1Key, error) { - return generatePrivKey() -} - -// Set attempts to parse keyStr into key. -func (key *ID1Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// Set attempts to parse keyStr into key. -func (key *SK1Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable id1 key into key. -func (key *ID1Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable sk1 key into key. -func (key *SK1Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// IDKey returns key as an IDKey. -func (key ID1Key) IDKey() IDKey { - return key -} - -// IDKey returns the ID1Key corresponding to key as an IDKey. -func (key SK1Key) IDKey() IDKey { - return key.ID1Key() -} - -// SKKey returns key as an SKKey. -func (key SK1Key) SKKey() SKKey { - return key -} - -// ID1Key computes the ID1Key corresponding to key. -func (key SK1Key) ID1Key() ID1Key { - return key.RCDHash() -} - -// RCDHash returns the RCD hash encoded in key. -func (key ID1Key) RCDHash() [sha256.Size]byte { - return key -} - -// RCDHash computes the RCD hash corresponding to key. -func (key SK1Key) RCDHash() [sha256.Size]byte { - return sha256d(key.RCD()) -} - -// RCD computes the RCD for key. -func (key SK1Key) RCD() []byte { - return append([]byte{RCDType}, key.PublicKey()[:]...) -} - -// PublicKey computes the ed25519.PublicKey for key. -func (key SK1Key) PublicKey() ed25519.PublicKey { - return key.PrivateKey().Public().(ed25519.PublicKey) -} - -// PrivateKey returns the ed25519.PrivateKey for key. -func (key SK1Key) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(key[:]) -} - -// Scan implements sql.Scanner for key using Bytes32.Scan. The ID1Key type is -// not encoded and is assumed. -func (key *ID1Key) Scan(v interface{}) error { - return (*Bytes32)(key).Scan(v) -} - -// Value implements driver.Valuer for key using Bytes32.Value. The ID1Key type -// is not encoded. -func (key ID1Key) Value() (driver.Value, error) { - return (Bytes32)(key).Value() -} - -// ID2Key is the id2 public key for an identity. -type ID2Key [sha256.Size]byte - -// SK2Key is the sk2 secret key for an identity. -type SK2Key [sha256.Size]byte - -// Payload returns key as a byte array. -func (key ID2Key) Payload() [sha256.Size]byte { - return key -} - -// Payload returns key as a byte array. -func (key SK2Key) Payload() [sha256.Size]byte { - return key -} - -// payload returns adr as payload. This is syntactic sugar useful in other -// methods that leverage payload. -func (key ID2Key) payload() payload { - return payload(key) -} -func (key SK2Key) payload() payload { - return payload(key) -} - -// payloadPtr returns adr as *payload. This is syntactic sugar useful in other -// methods that leverage *payload. -func (key *ID2Key) payloadPtr() *payload { - return (*payload)(key) -} -func (key *SK2Key) payloadPtr() *payload { - return (*payload)(key) -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x3f, 0xbe, 0xd8}. -func (ID2Key) PrefixBytes() []byte { - prefix := id2PrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x4d, 0xb6, 0xe7}. -func (SK2Key) PrefixBytes() []byte { - prefix := sk2PrefixBytes - return prefix[:] -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "id2". -func (ID2Key) PrefixString() string { - return id2PrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "sk2". -func (SK2Key) PrefixString() string { - return sk2PrefixStr -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key ID2Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key SK2Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (ID2Key) Type() string { - return id2PrefixStr -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (SK2Key) Type() string { - return sk2PrefixStr -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key ID2Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key SK2Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// NewID2Key attempts to parse keyStr into a new ID2Key. -func NewID2Key(keyStr string) (key ID2Key, err error) { - err = key.Set(keyStr) - return -} - -// NewSK2Key attempts to parse keyStr into a new SK2Key. -func NewSK2Key(keyStr string) (key SK2Key, err error) { - err = key.Set(keyStr) - return -} - -// GenerateSK2Key generates a secure random private Entry Credit address using -// crypto/rand.Random as the source of randomness. -func GenerateSK2Key() (SK2Key, error) { - return generatePrivKey() -} - -// Set attempts to parse keyStr into key. -func (key *ID2Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// Set attempts to parse keyStr into key. -func (key *SK2Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable id2 key into key. -func (key *ID2Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable sk2 key into key. -func (key *SK2Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// IDKey returns key as an IDKey. -func (key ID2Key) IDKey() IDKey { - return key -} - -// IDKey returns the ID2Key corresponding to key as an IDKey. -func (key SK2Key) IDKey() IDKey { - return key.ID2Key() -} - -// SKKey returns key as an SKKey. -func (key SK2Key) SKKey() SKKey { - return key -} - -// ID2Key computes the ID2Key corresponding to key. -func (key SK2Key) ID2Key() ID2Key { - return key.RCDHash() -} - -// RCDHash returns the RCD hash encoded in key. -func (key ID2Key) RCDHash() [sha256.Size]byte { - return key -} - -// RCDHash computes the RCD hash corresponding to key. -func (key SK2Key) RCDHash() [sha256.Size]byte { - return sha256d(key.RCD()) -} - -// RCD computes the RCD for key. -func (key SK2Key) RCD() []byte { - return append([]byte{RCDType}, key.PublicKey()[:]...) -} - -// PublicKey computes the ed25519.PublicKey for key. -func (key SK2Key) PublicKey() ed25519.PublicKey { - return key.PrivateKey().Public().(ed25519.PublicKey) -} - -// PrivateKey returns the ed25519.PrivateKey for key. -func (key SK2Key) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(key[:]) -} - -// Scan implements sql.Scanner for key using Bytes32.Scan. The ID2Key type is -// not encoded and is assumed. -func (key *ID2Key) Scan(v interface{}) error { - return (*Bytes32)(key).Scan(v) -} - -// Value implements driver.Valuer for key using Bytes32.Value. The ID2Key type -// is not encoded. -func (key ID2Key) Value() (driver.Value, error) { - return (Bytes32)(key).Value() -} - -// ID3Key is the id3 public key for an identity. -type ID3Key [sha256.Size]byte - -// SK3Key is the sk3 secret key for an identity. -type SK3Key [sha256.Size]byte - -// Payload returns key as a byte array. -func (key ID3Key) Payload() [sha256.Size]byte { - return key -} - -// Payload returns key as a byte array. -func (key SK3Key) Payload() [sha256.Size]byte { - return key -} - -// payload returns adr as payload. This is syntactic sugar useful in other -// methods that leverage payload. -func (key ID3Key) payload() payload { - return payload(key) -} -func (key SK3Key) payload() payload { - return payload(key) -} - -// payloadPtr returns adr as *payload. This is syntactic sugar useful in other -// methods that leverage *payload. -func (key *ID3Key) payloadPtr() *payload { - return (*payload)(key) -} -func (key *SK3Key) payloadPtr() *payload { - return (*payload)(key) -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x3f, 0xbe, 0xf6}. -func (ID3Key) PrefixBytes() []byte { - prefix := id3PrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x4d, 0xb7, 0x05}. -func (SK3Key) PrefixBytes() []byte { - prefix := sk3PrefixBytes - return prefix[:] -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "id3". -func (ID3Key) PrefixString() string { - return id3PrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "sk3". -func (SK3Key) PrefixString() string { - return sk3PrefixStr -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key ID3Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key SK3Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (ID3Key) Type() string { - return id3PrefixStr -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (SK3Key) Type() string { - return sk3PrefixStr -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key ID3Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key SK3Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// NewID3Key attempts to parse keyStr into a new ID3Key. -func NewID3Key(keyStr string) (key ID3Key, err error) { - err = key.Set(keyStr) - return -} - -// NewSK3Key attempts to parse keyStr into a new SK3Key. -func NewSK3Key(keyStr string) (key SK3Key, err error) { - err = key.Set(keyStr) - return -} - -// GenerateSK3Key generates a secure random private Entry Credit address using -// crypto/rand.Random as the source of randomness. -func GenerateSK3Key() (SK3Key, error) { - return generatePrivKey() -} - -// Set attempts to parse keyStr into key. -func (key *ID3Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// Set attempts to parse keyStr into key. -func (key *SK3Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable id3 key into key. -func (key *ID3Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable sk3 key into key. -func (key *SK3Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// IDKey returns key as an IDKey. -func (key ID3Key) IDKey() IDKey { - return key -} - -// IDKey returns the ID3Key corresponding to key as an IDKey. -func (key SK3Key) IDKey() IDKey { - return key.ID3Key() -} - -// SKKey returns key as an SKKey. -func (key SK3Key) SKKey() SKKey { - return key -} - -// ID3Key computes the ID3Key corresponding to key. -func (key SK3Key) ID3Key() ID3Key { - return key.RCDHash() -} - -// RCDHash returns the RCD hash encoded in key. -func (key ID3Key) RCDHash() [sha256.Size]byte { - return key -} - -// RCDHash computes the RCD hash corresponding to key. -func (key SK3Key) RCDHash() [sha256.Size]byte { - return sha256d(key.RCD()) -} - -// RCD computes the RCD for key. -func (key SK3Key) RCD() []byte { - return append([]byte{RCDType}, key.PublicKey()[:]...) -} - -// PublicKey computes the ed25519.PublicKey for key. -func (key SK3Key) PublicKey() ed25519.PublicKey { - return key.PrivateKey().Public().(ed25519.PublicKey) -} - -// PrivateKey returns the ed25519.PrivateKey for key. -func (key SK3Key) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(key[:]) -} - -// Scan implements sql.Scanner for key using Bytes32.Scan. The ID3Key type is -// not encoded and is assumed. -func (key *ID3Key) Scan(v interface{}) error { - return (*Bytes32)(key).Scan(v) -} - -// Value implements driver.Valuer for key using Bytes32.Value. The ID3Key type -// is not encoded. -func (key ID3Key) Value() (driver.Value, error) { - return (Bytes32)(key).Value() -} - -// ID4Key is the id4 public key for an identity. -type ID4Key [sha256.Size]byte - -// SK4Key is the sk4 secret key for an identity. -type SK4Key [sha256.Size]byte - -// Payload returns key as a byte array. -func (key ID4Key) Payload() [sha256.Size]byte { - return key -} - -// Payload returns key as a byte array. -func (key SK4Key) Payload() [sha256.Size]byte { - return key -} - -// payload returns adr as payload. This is syntactic sugar useful in other -// methods that leverage payload. -func (key ID4Key) payload() payload { - return payload(key) -} -func (key SK4Key) payload() payload { - return payload(key) -} - -// payloadPtr returns adr as *payload. This is syntactic sugar useful in other -// methods that leverage *payload. -func (key *ID4Key) payloadPtr() *payload { - return (*payload)(key) -} -func (key *SK4Key) payloadPtr() *payload { - return (*payload)(key) -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x3f, 0xbf, 0x14}. -func (ID4Key) PrefixBytes() []byte { - prefix := id4PrefixBytes - return prefix[:] -} - -// PrefixBytes returns the two byte prefix for the address type as a byte -// array. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns []byte{0x4d, 0xb7, 0x23}. -func (SK4Key) PrefixBytes() []byte { - prefix := sk4PrefixBytes - return prefix[:] -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "id4". -func (ID4Key) PrefixString() string { - return id4PrefixStr -} - -// PrefixString returns the two prefix bytes for the address type as an encoded -// string. Note that the prefix for a given address type is always the same and -// does not depend on the address value. Returns "sk4". -func (SK4Key) PrefixString() string { - return sk4PrefixStr -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key ID4Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// String encodes key into its human readable form: a base58check string with -// key.PrefixBytes(). -func (key SK4Key) String() string { - return key.payload().StringPrefix(key.PrefixBytes()) -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (ID4Key) Type() string { - return id4PrefixStr -} - -// Type returns PrefixString() satisfies the pflag.Value interface. -func (SK4Key) Type() string { - return sk4PrefixStr -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key ID4Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// MarshalJSON encodes key as a JSON string using key.String(). -func (key SK4Key) MarshalJSON() ([]byte, error) { - return key.payload().MarshalJSONPrefix(key.PrefixBytes()) -} - -// NewID4Key attempts to parse keyStr into a new ID4Key. -func NewID4Key(keyStr string) (key ID4Key, err error) { - err = key.Set(keyStr) - return -} - -// NewSK4Key attempts to parse keyStr into a new SK4Key. -func NewSK4Key(keyStr string) (key SK4Key, err error) { - err = key.Set(keyStr) - return -} - -// GenerateSK4Key generates a secure random private Entry Credit address using -// crypto/rand.Random as the source of randomness. -func GenerateSK4Key() (SK4Key, error) { - return generatePrivKey() -} - -// Set attempts to parse keyStr into key. -func (key *ID4Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// Set attempts to parse keyStr into key. -func (key *SK4Key) Set(keyStr string) error { - return key.payloadPtr().SetPrefix(keyStr, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable id4 key into key. -func (key *ID4Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// UnmarshalJSON decodes a JSON string with a human readable sk4 key into key. -func (key *SK4Key) UnmarshalJSON(data []byte) error { - return key.payloadPtr().UnmarshalJSONPrefix(data, key.PrefixString()) -} - -// IDKey returns key as an IDKey. -func (key ID4Key) IDKey() IDKey { - return key -} - -// IDKey returns the ID4Key corresponding to key as an IDKey. -func (key SK4Key) IDKey() IDKey { - return key.ID4Key() -} - -// SKKey returns key as an SKKey. -func (key SK4Key) SKKey() SKKey { - return key -} - -// ID4Key computes the ID4Key corresponding to key. -func (key SK4Key) ID4Key() ID4Key { - return key.RCDHash() -} - -// RCDHash returns the RCD hash encoded in key. -func (key ID4Key) RCDHash() [sha256.Size]byte { - return key -} - -// RCDHash computes the RCD hash corresponding to key. -func (key SK4Key) RCDHash() [sha256.Size]byte { - return sha256d(key.RCD()) -} - -// RCD computes the RCD for key. -func (key SK4Key) RCD() []byte { - return append([]byte{RCDType}, key.PublicKey()[:]...) -} - -// PublicKey computes the ed25519.PublicKey for key. -func (key SK4Key) PublicKey() ed25519.PublicKey { - return key.PrivateKey().Public().(ed25519.PublicKey) -} - -// PrivateKey returns the ed25519.PrivateKey for key. -func (key SK4Key) PrivateKey() ed25519.PrivateKey { - return ed25519.NewKeyFromSeed(key[:]) -} - -// Scan implements sql.Scanner for key using Bytes32.Scan. The ID4Key type is -// not encoded and is assumed. -func (key *ID4Key) Scan(v interface{}) error { - return (*Bytes32)(key).Scan(v) -} - -// Value implements driver.Valuer for key using Bytes32.Value. The ID4Key type -// is not encoded. -func (key ID4Key) Value() (driver.Value, error) { - return (Bytes32)(key).Value() -} diff --git a/factom/idkey_gen_test.go b/factom/idkey_gen_test.go deleted file mode 100644 index d22932e..0000000 --- a/factom/idkey_gen_test.go +++ /dev/null @@ -1,488 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Code generated DO NOT EDIT - -package factom - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -var ( - // Test id/sk key pairs with all zeros. - // OBVIOUSLY NEVER USE THESE FOR ANYTHING! - id1KeyStr = "id12K4tCXKcJJYxJmZ1UY9EuKPvtGVAjo32xySMKNUahbmRcsqFgW" - id2KeyStr = "id22pNvsaMWf9qxWFrmfQpwFJiKQoWfKmBwVgQtdvqVZuqzGmrFNY" - id3KeyStr = "id33pRgpm8ufXNGxtW7n5FgdGP6afXKjU4LfVmgfC8Yaq6LyYq2wA" - id4KeyStr = "id42vYqBB63eoSz8DHozEwtCaLbEwvBTG9pWgD3D5CCaHWy1gCjF5" - - sk1KeyStr = "sk13iLKJfxNQg8vpSmjacEgEQAnXkn7rbjd5ewexc1Un5wVPa7KTk" - sk2KeyStr = "sk22UaDys2Mzg2pUCsToo9aKgxubJFnZN5Bc2LXfV59VxMvXXKwXa" - sk3KeyStr = "sk32Xyo9kmjtNqRUfRd3ZhU56NZd8M1nR61tdBaCLSQRdhUCk4yiM" - sk4KeyStr = "sk43eMusQuvvChoGNn1VZZwbAH8BtKJSZNC7ZWoz1Vc4Y3greLA45" -) - -type idKeyUnmarshalJSONTest struct { - Name string - ID IDKey - ExpID IDKey - Data string - Err string -} - -var idKeyUnmarshalJSONTests = []idKeyUnmarshalJSONTest{{ - Name: "valid/ID1", - Data: fmt.Sprintf("%q", id1KeyStr), - ID: new(ID1Key), - ExpID: func() *ID1Key { - sk, _ := NewSK1Key(sk1KeyStr) - id := sk.ID1Key() - return &id - }(), -}, { - Name: "valid/ID2", - Data: fmt.Sprintf("%q", id2KeyStr), - ID: new(ID2Key), - ExpID: func() *ID2Key { - sk, _ := NewSK2Key(sk2KeyStr) - id := sk.ID2Key() - return &id - }(), -}, { - Name: "valid/ID3", - Data: fmt.Sprintf("%q", id3KeyStr), - ID: new(ID3Key), - ExpID: func() *ID3Key { - sk, _ := NewSK3Key(sk3KeyStr) - id := sk.ID3Key() - return &id - }(), -}, { - Name: "valid/ID4", - Data: fmt.Sprintf("%q", id4KeyStr), - ID: new(ID4Key), - ExpID: func() *ID4Key { - sk, _ := NewSK4Key(sk4KeyStr) - id := sk.ID4Key() - return &id - }(), -}, { - - Name: "valid/SK1", - Data: fmt.Sprintf("%q", sk1KeyStr), - ID: new(SK1Key), - ExpID: func() *SK1Key { - key, _ := NewSK1Key(sk1KeyStr) - return &key - }(), -}, { - Name: "valid/SK2", - Data: fmt.Sprintf("%q", sk2KeyStr), - ID: new(SK2Key), - ExpID: func() *SK2Key { - key, _ := NewSK2Key(sk2KeyStr) - return &key - }(), -}, { - Name: "valid/SK3", - Data: fmt.Sprintf("%q", sk3KeyStr), - ID: new(SK3Key), - ExpID: func() *SK3Key { - key, _ := NewSK3Key(sk3KeyStr) - return &key - }(), -}, { - Name: "valid/SK4", - Data: fmt.Sprintf("%q", sk4KeyStr), - ID: new(SK4Key), - ExpID: func() *SK4Key { - key, _ := NewSK4Key(sk4KeyStr) - return &key - }(), -}, { - - Name: "invalid type", - Data: `{}`, - Err: "json: cannot unmarshal object into Go value of type string", -}, { - Name: "invalid type", - Data: `5.5`, - Err: "json: cannot unmarshal number into Go value of type string", -}, { - Name: "invalid type", - Data: `["hello"]`, - Err: "json: cannot unmarshal array into Go value of type string", -}, { - Name: "invalid length", - Data: fmt.Sprintf("%q", id1KeyStr[0:len(id1KeyStr)-1]), - Err: "invalid length", -}, { - Name: "invalid length", - Data: fmt.Sprintf("%q", id1KeyStr+"Q"), - Err: "invalid length", -}, { - Name: "invalid prefix", - Data: fmt.Sprintf("%q", func() string { - key, _ := NewSK1Key(sk1KeyStr) - return key.payload().StringPrefix([]byte{0x50, 0x50, 0x50}) - }()), - Err: "invalid prefix", -}, { - Name: "invalid symbol/ID1", - Data: fmt.Sprintf("%q", id1KeyStr[0:len(id1KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(ID1Key), - ExpID: new(ID1Key), -}, { - Name: "invalid symbol/SK1", - Data: fmt.Sprintf("%q", sk1KeyStr[0:len(sk1KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(SK1Key), - ExpID: new(SK1Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", id1KeyStr[0:len(id1KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(ID1Key), - ExpID: new(ID1Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", sk1KeyStr[0:len(sk1KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(SK1Key), - ExpID: new(SK1Key), -}, { - Name: "invalid symbol/ID2", - Data: fmt.Sprintf("%q", id2KeyStr[0:len(id2KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(ID2Key), - ExpID: new(ID2Key), -}, { - Name: "invalid symbol/SK2", - Data: fmt.Sprintf("%q", sk2KeyStr[0:len(sk2KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(SK2Key), - ExpID: new(SK2Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", id2KeyStr[0:len(id2KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(ID2Key), - ExpID: new(ID2Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", sk2KeyStr[0:len(sk2KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(SK2Key), - ExpID: new(SK2Key), -}, { - Name: "invalid symbol/ID3", - Data: fmt.Sprintf("%q", id3KeyStr[0:len(id3KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(ID3Key), - ExpID: new(ID3Key), -}, { - Name: "invalid symbol/SK3", - Data: fmt.Sprintf("%q", sk3KeyStr[0:len(sk3KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(SK3Key), - ExpID: new(SK3Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", id3KeyStr[0:len(id3KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(ID3Key), - ExpID: new(ID3Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", sk3KeyStr[0:len(sk3KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(SK3Key), - ExpID: new(SK3Key), -}, { - Name: "invalid symbol/ID4", - Data: fmt.Sprintf("%q", id4KeyStr[0:len(id4KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(ID4Key), - ExpID: new(ID4Key), -}, { - Name: "invalid symbol/SK4", - Data: fmt.Sprintf("%q", sk4KeyStr[0:len(sk4KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(SK4Key), - ExpID: new(SK4Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", id4KeyStr[0:len(id4KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(ID4Key), - ExpID: new(ID4Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", sk4KeyStr[0:len(sk4KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(SK4Key), - ExpID: new(SK4Key), -}} - -func testIDKeyUnmarshalJSON(t *testing.T, test idKeyUnmarshalJSONTest) { - err := json.Unmarshal([]byte(test.Data), test.ID) - assert := assert.New(t) - if len(test.Err) > 0 { - assert.EqualError(err, test.Err) - return - } - assert.NoError(err) - assert.Equal(test.ExpID, test.ID) -} - -func TestIDKey(t *testing.T) { - for _, test := range idKeyUnmarshalJSONTests { - if test.ID != nil { - t.Run("UnmarshalJSON/"+test.Name, func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - continue - } - - test.ExpID, test.ID = new(ID1Key), new(ID1Key) - t.Run("UnmarshalJSON/"+test.Name+"/ID1Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - test.ExpID, test.ID = new(SK1Key), new(SK1Key) - t.Run("UnmarshalJSON/"+test.Name+"/SK1Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - - test.ExpID, test.ID = new(ID2Key), new(ID2Key) - t.Run("UnmarshalJSON/"+test.Name+"/ID2Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - test.ExpID, test.ID = new(SK2Key), new(SK2Key) - t.Run("UnmarshalJSON/"+test.Name+"/SK2Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - - test.ExpID, test.ID = new(ID3Key), new(ID3Key) - t.Run("UnmarshalJSON/"+test.Name+"/ID3Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - test.ExpID, test.ID = new(SK3Key), new(SK3Key) - t.Run("UnmarshalJSON/"+test.Name+"/SK3Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - - test.ExpID, test.ID = new(ID4Key), new(ID4Key) - t.Run("UnmarshalJSON/"+test.Name+"/ID4Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - test.ExpID, test.ID = new(SK4Key), new(SK4Key) - t.Run("UnmarshalJSON/"+test.Name+"/SK4Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - - } - - id1, _ := NewID1Key(id1KeyStr) - sk1, _ := NewSK1Key(sk1KeyStr) - id2, _ := NewID2Key(id2KeyStr) - sk2, _ := NewSK2Key(sk2KeyStr) - id3, _ := NewID3Key(id3KeyStr) - sk3, _ := NewSK3Key(sk3KeyStr) - id4, _ := NewID4Key(id4KeyStr) - sk4, _ := NewSK4Key(sk4KeyStr) - - strToKey := map[string]IDKey{ - id1KeyStr: id1, sk1KeyStr: sk1, - id2KeyStr: id2, sk2KeyStr: sk2, - id3KeyStr: id3, sk3KeyStr: sk3, - id4KeyStr: id4, sk4KeyStr: sk4, - } - for keyStr, key := range strToKey { - t.Run("MarshalJSON/"+key.PrefixString(), func(t *testing.T) { - data, err := json.Marshal(key) - assert := assert.New(t) - assert.NoError(err) - assert.Equal(fmt.Sprintf("%q", keyStr), string(data)) - }) - t.Run("Payload/"+key.PrefixString(), func(t *testing.T) { - assert.EqualValues(t, key, key.Payload()) - }) - t.Run("String/"+key.PrefixString(), func(t *testing.T) { - assert.Equal(t, keyStr, key.String()) - }) - } - - t.Run("SKKey/SK1", func(t *testing.T) { - id, _ := NewID1Key(id1KeyStr) - sk, _ := NewSK1Key(sk1KeyStr) - assert := assert.New(t) - assert.Equal(id, sk.ID1Key()) - assert.Equal(id.IDKey(), sk.IDKey()) - assert.Equal(SKKey(sk), sk.SKKey()) - assert.Equal(id.RCDHash(), sk.RCDHash(), "RCDHash") - }) - t.Run("SKKey/SK2", func(t *testing.T) { - id, _ := NewID2Key(id2KeyStr) - sk, _ := NewSK2Key(sk2KeyStr) - assert := assert.New(t) - assert.Equal(id, sk.ID2Key()) - assert.Equal(id.IDKey(), sk.IDKey()) - assert.Equal(SKKey(sk), sk.SKKey()) - assert.Equal(id.RCDHash(), sk.RCDHash(), "RCDHash") - }) - t.Run("SKKey/SK3", func(t *testing.T) { - id, _ := NewID3Key(id3KeyStr) - sk, _ := NewSK3Key(sk3KeyStr) - assert := assert.New(t) - assert.Equal(id, sk.ID3Key()) - assert.Equal(id.IDKey(), sk.IDKey()) - assert.Equal(SKKey(sk), sk.SKKey()) - assert.Equal(id.RCDHash(), sk.RCDHash(), "RCDHash") - }) - t.Run("SKKey/SK4", func(t *testing.T) { - id, _ := NewID4Key(id4KeyStr) - sk, _ := NewSK4Key(sk4KeyStr) - assert := assert.New(t) - assert.Equal(id, sk.ID4Key()) - assert.Equal(id.IDKey(), sk.IDKey()) - assert.Equal(SKKey(sk), sk.SKKey()) - assert.Equal(id.RCDHash(), sk.RCDHash(), "RCDHash") - }) - - t.Run("Generate/SK1", func(t *testing.T) { - _, err := GenerateSK1Key() - assert.NoError(t, err) - }) - t.Run("Generate/SK2", func(t *testing.T) { - _, err := GenerateSK2Key() - assert.NoError(t, err) - }) - t.Run("Generate/SK3", func(t *testing.T) { - _, err := GenerateSK3Key() - assert.NoError(t, err) - }) - t.Run("Generate/SK4", func(t *testing.T) { - _, err := GenerateSK4Key() - assert.NoError(t, err) - }) - - t.Run("Scan", func(t *testing.T) { - var id ID1Key - err := id.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = id.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = id.Scan(in) - assert.NoError(err) - assert.EqualValues(in, id[:]) - }) - t.Run("Scan", func(t *testing.T) { - var id ID2Key - err := id.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = id.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = id.Scan(in) - assert.NoError(err) - assert.EqualValues(in, id[:]) - }) - t.Run("Scan", func(t *testing.T) { - var id ID3Key - err := id.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = id.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = id.Scan(in) - assert.NoError(err) - assert.EqualValues(in, id[:]) - }) - t.Run("Scan", func(t *testing.T) { - var id ID4Key - err := id.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = id.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = id.Scan(in) - assert.NoError(err) - assert.EqualValues(in, id[:]) - }) - - t.Run("Value", func(t *testing.T) { - var id ID1Key - id[0] = 0xff - val, err := id.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(id[:], val) - }) - t.Run("Value", func(t *testing.T) { - var id ID2Key - id[0] = 0xff - val, err := id.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(id[:], val) - }) - t.Run("Value", func(t *testing.T) { - var id ID3Key - id[0] = 0xff - val, err := id.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(id[:], val) - }) - t.Run("Value", func(t *testing.T) { - var id ID4Key - id[0] = 0xff - val, err := id.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(id[:], val) - }) - -} diff --git a/factom/idkey_test.tmpl b/factom/idkey_test.tmpl deleted file mode 100644 index d5d0d94..0000000 --- a/factom/idkey_test.tmpl +++ /dev/null @@ -1,225 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Code generated DO NOT EDIT - -package factom - -var ( - // Test id/sk key pairs with all zeros. - // OBVIOUSLY NEVER USE THESE FOR ANYTHING! -{{range . -}} - id{{.ID}}KeyStr = "{{.IDStr}}" -{{end}} - -{{range . -}} - sk{{.ID}}KeyStr = "{{.SKStr}}" -{{end}} -) - -type idKeyUnmarshalJSONTest struct { - Name string - ID IDKey - ExpID IDKey - Data string - Err string -} - -var idKeyUnmarshalJSONTests = []idKeyUnmarshalJSONTest{ { -{{ range . -}} - Name: "valid/ID{{.ID}}", - Data: fmt.Sprintf("%q", id{{.ID}}KeyStr), - ID: new(ID{{.ID}}Key), - ExpID: func() *ID{{.ID}}Key { - sk, _ := NewSK{{.ID}}Key(sk{{.ID}}KeyStr) - id := sk.ID{{.ID}}Key() - return &id - }(), -}, { -{{ end }} -{{ range . -}} - Name: "valid/SK{{.ID}}", - Data: fmt.Sprintf("%q", sk{{.ID}}KeyStr), - ID: new(SK{{.ID}}Key), - ExpID: func() *SK{{.ID}}Key { - key, _ := NewSK{{.ID}}Key(sk{{.ID}}KeyStr) - return &key - }(), -}, { -{{end}} - Name: "invalid type", - Data: `{}`, - Err: "json: cannot unmarshal object into Go value of type string", -}, { - Name: "invalid type", - Data: `5.5`, - Err: "json: cannot unmarshal number into Go value of type string", -}, { - Name: "invalid type", - Data: `["hello"]`, - Err: "json: cannot unmarshal array into Go value of type string", -}, { - Name: "invalid length", - Data: fmt.Sprintf("%q", id1KeyStr[0:len(id1KeyStr)-1]), - Err: "invalid length", -}, { - Name: "invalid length", - Data: fmt.Sprintf("%q", id1KeyStr+"Q"), - Err: "invalid length", -}, { - Name: "invalid prefix", - Data: fmt.Sprintf("%q", func() string { - key, _ := NewSK1Key(sk1KeyStr) - return key.payload().StringPrefix([]byte{0x50, 0x50, 0x50}) - }()), - Err: "invalid prefix", -{{ range . -}} -}, { - Name: "invalid symbol/ID{{.ID}}", - Data: fmt.Sprintf("%q", id{{.ID}}KeyStr[0:len(id{{.ID}}KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(ID{{.ID}}Key), - ExpID: new(ID{{.ID}}Key), -}, { - Name: "invalid symbol/SK{{.ID}}", - Data: fmt.Sprintf("%q", sk{{.ID}}KeyStr[0:len(sk{{.ID}}KeyStr)-1]+"0"), - Err: "invalid format: version and/or checksum bytes missing", - ID: new(SK{{.ID}}Key), - ExpID: new(SK{{.ID}}Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", id{{.ID}}KeyStr[0:len(id{{.ID}}KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(ID{{.ID}}Key), - ExpID: new(ID{{.ID}}Key), -}, { - Name: "invalid checksum", - Data: fmt.Sprintf("%q", sk{{.ID}}KeyStr[0:len(sk{{.ID}}KeyStr)-1]+"e"), - Err: "checksum error", - ID: new(SK{{.ID}}Key), - ExpID: new(SK{{.ID}}Key), -{{end}} -} } - -func testIDKeyUnmarshalJSON(t *testing.T, test idKeyUnmarshalJSONTest) { - err := json.Unmarshal([]byte(test.Data), test.ID) - assert := assert.New(t) - if len(test.Err) > 0 { - assert.EqualError(err, test.Err) - return - } - assert.NoError(err) - assert.Equal(test.ExpID, test.ID) -} - -func TestIDKey(t *testing.T) { - for _, test := range idKeyUnmarshalJSONTests { - if test.ID != nil { - t.Run("UnmarshalJSON/"+test.Name, func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - continue - } -{{range .}} - test.ExpID, test.ID = new(ID{{.ID}}Key), new(ID{{.ID}}Key) - t.Run("UnmarshalJSON/"+test.Name+"/ID{{.ID}}Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) - test.ExpID, test.ID = new(SK{{.ID}}Key), new(SK{{.ID}}Key) - t.Run("UnmarshalJSON/"+test.Name+"/SK{{.ID}}Key", func(t *testing.T) { - testIDKeyUnmarshalJSON(t, test) - }) -{{end}} - } - -{{range . -}} - id{{.ID}}, _ := NewID{{.ID}}Key(id{{.ID}}KeyStr) - sk{{.ID}}, _ := NewSK{{.ID}}Key(sk{{.ID}}KeyStr) -{{end}} - strToKey := map[string]IDKey{ -{{range . -}} - id{{.ID}}KeyStr: id{{.ID}}, sk{{.ID}}KeyStr: sk{{.ID}}, -{{end}} - } - for keyStr, key := range strToKey { - t.Run("MarshalJSON/"+key.PrefixString(), func(t *testing.T) { - data, err := json.Marshal(key) - assert := assert.New(t) - assert.NoError(err) - assert.Equal(fmt.Sprintf("%q", keyStr), string(data)) - }) - t.Run("Payload/"+key.PrefixString(), func(t *testing.T) { - assert.EqualValues(t, key, key.Payload()) - }) - t.Run("String/"+key.PrefixString(), func(t *testing.T) { - assert.Equal(t, keyStr, key.String()) - }) - } - -{{range . -}} - t.Run("SKKey/SK{{.ID}}", func(t *testing.T) { - id, _ := NewID{{.ID}}Key(id{{.ID}}KeyStr) - sk, _ := NewSK{{.ID}}Key(sk{{.ID}}KeyStr) - assert := assert.New(t) - assert.Equal(id, sk.ID{{.ID}}Key()) - assert.Equal(id.IDKey(), sk.IDKey()) - assert.Equal(SKKey(sk), sk.SKKey()) - assert.Equal(id.RCDHash(), sk.RCDHash(), "RCDHash") - }) -{{end}} - -{{range . -}} - t.Run("Generate/SK{{.ID}}", func(t *testing.T) { - _, err := GenerateSK{{.ID}}Key() - assert.NoError(t, err) - }) -{{end}} - -{{range . -}} - t.Run("Scan", func(t *testing.T) { - var id ID{{.ID}}Key - err := id.Scan(5) - assert := assert.New(t) - assert.EqualError(err, "invalid type") - - in := make([]byte, 32) - in[0] = 0xff - err = id.Scan(in[:10]) - assert.EqualError(err, "invalid length") - - err = id.Scan(in) - assert.NoError(err) - assert.EqualValues(in, id[:]) - }) -{{end}} - -{{range . -}} - t.Run("Value", func(t *testing.T) { - var id ID{{.ID}}Key - id[0] = 0xff - val, err := id.Value() - assert := assert.New(t) - assert.NoError(err) - assert.Equal(id[:], val) - }) -{{end}} -} diff --git a/factom/payload.go b/factom/payload.go deleted file mode 100644 index e078918..0000000 --- a/factom/payload.go +++ /dev/null @@ -1,72 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "crypto/sha256" - "encoding/json" - "fmt" - - "github.com/Factom-Asset-Tokens/base58" -) - -// payload implements helper functions used by all Address and IDKey types. -type payload [sha256.Size]byte - -// StringPrefix encodes payload as a base58check string with the given prefix. -func (pld payload) StringPrefix(prefix []byte) string { - return base58.CheckEncode(pld[:], prefix...) -} - -// MarshalJSONPrefix encodes payload as a base58check JSON string with the -// given prefix. -func (pld payload) MarshalJSONPrefix(prefix []byte) ([]byte, error) { - return []byte(fmt.Sprintf("%q", pld.StringPrefix(prefix))), nil -} - -// SetPrefix attempts to parse adrStr into adr enforcing that adrStr -// starts with prefix if prefix is not empty. -func (pld *payload) SetPrefix(str, prefix string) error { - if len(str) != 50+len(prefix) { - return fmt.Errorf("invalid length") - } - if len(prefix) > 0 && str[:len(prefix)] != prefix { - return fmt.Errorf("invalid prefix") - } - b, _, err := base58.CheckDecode(str, len(prefix)) - if err != nil { - return err - } - copy(pld[:], b) - return nil -} - -// UnmarshalJSONPrefix unmarshals a human readable address JSON string with the -// given prefix. -func (pld *payload) UnmarshalJSONPrefix(data []byte, prefix string) error { - var str string - if err := json.Unmarshal(data, &str); err != nil { - return err - } - return pld.SetPrefix(str, prefix) -} diff --git a/factom/pendingentries.go b/factom/pendingentries.go deleted file mode 100644 index 42a1c87..0000000 --- a/factom/pendingentries.go +++ /dev/null @@ -1,79 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "bytes" - "sort" -) - -// PendingEntries is a list of pending entries which may or may not be -// revealed. If the entry's ChainID is not nil, then its data has been revealed -// and can be queried from factomd. -type PendingEntries []Entry - -// Get returns all pending entries sorted by ChainID, and then order they were -// originally returned. -func (pe *PendingEntries) Get(c *Client) error { - if err := c.FactomdRequest("pending-entries", nil, pe); err != nil { - return err - } - sort.SliceStable(*pe, func(i, j int) bool { - pe := *pe - var ci, cj []byte - ei, ej := pe[i], pe[j] - if ei.ChainID != nil { - ci = ei.ChainID[:] - } - if ej.ChainID != nil { - cj = ej.ChainID[:] - } - return bytes.Compare(ci, cj) < 0 - }) - return nil -} - -// Entries efficiently finds and returns all entries in pe for the given -// chainID, if any exist. Otherwise, Entries returns nil. -func (pe PendingEntries) Entries(chainID Bytes32) []Entry { - // Find the first index of the entry with this chainID. - ei := sort.Search(len(pe), func(i int) bool { - var c []byte - e := pe[i] - if e.ChainID != nil { - c = e.ChainID[:] - } - return bytes.Compare(c, chainID[:]) >= 0 - }) - if ei < len(pe) && *pe[ei].ChainID == chainID { - // Find all remaining entries with the chainID. - for i, e := range pe[ei:] { - if *e.ChainID != chainID { - return pe[ei : ei+i] - } - } - return pe[ei:] - } - // There are no entries for this ChainID. - return nil -} diff --git a/factom/pendingentries_test.go b/factom/pendingentries_test.go deleted file mode 100644 index 16ca46d..0000000 --- a/factom/pendingentries_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import ( - "bytes" - "fmt" - "sort" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var searchID = NewBytes32FromString( - "b0bb9c3e14514d7109b18b8edf3875618bac0881935e20a14b69d81fdc39e624") - -// TestPendingEntries is not a stable test. It depends on the current pending -// entries. It could be improved with using static data, but it works for now. -// Some tests are only performed if the pending entries for the test are -// available. But all of these test cases have been executed and manually -// verified. -func TestPendingEntries(t *testing.T) { - var pe PendingEntries - c := NewClient() - c.Factomd.DebugRequest = true - assert := assert.New(t) - require := require.New(t) - require.NoError(pe.Get(c)) - - if len(pe) == 0 { - return - } - - fmt.Printf("%+v\n", pe) - - require.True(sort.SliceIsSorted(pe, func(i, j int) bool { - var ci, cj []byte - ei, ej := pe[i], pe[j] - if ei.ChainID != nil { - ci = ei.ChainID[:] - } - if ej.ChainID != nil { - cj = ej.ChainID[:] - } - return bytes.Compare(ci, cj) < 0 - }), "not sorted") - - es := pe.Entries(Bytes32{}) - if len(es) > 0 { - assert.Nil(es[0].ChainID) - } - chainID := pe[len(pe)-1].ChainID - if chainID != nil { - es := pe.Entries(*chainID) - require.NotEmpty(es) - for _, e := range es { - assert.Equal(*e.ChainID, *chainID) - } - } - - es = pe.Entries(*searchID) - if len(es) == 0 { - return - } - fmt.Printf("%+v\n", es) - for _, e := range es { - assert.Equal(*e.ChainID, *searchID) - } -} diff --git a/factom/rcdprivatekey.go b/factom/rcdprivatekey.go deleted file mode 100644 index 32a647f..0000000 --- a/factom/rcdprivatekey.go +++ /dev/null @@ -1,39 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package factom - -import "golang.org/x/crypto/ed25519" - -// RCDPrivateKey is the interface implemented by the four SK Key types and the -// Fs Address type. -type RCDPrivateKey interface { - // RCD returns the RCD corresponding to the private key. - RCD() []byte - - // PrivateKey returns the ed25519.PrivateKey which can be used for - // signing data. - PrivateKey() ed25519.PrivateKey - // PublicKey returns the ed25519.PublicKey which can be used for - // verifying signatures. - PublicKey() ed25519.PublicKey -} diff --git a/factom/varintf/varintf.go b/factom/varintf/varintf.go deleted file mode 100644 index efadd2d..0000000 --- a/factom/varintf/varintf.go +++ /dev/null @@ -1,71 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -// Package varintf implements Factom's varInt_F specification. -// -// The varInt_F specifications uses the top bit (0x80) in each byte as the -// continuation bit. If this bit is set, continue to read the next byte. If -// this bit is not set, then this is the last byte. The remaining 7 bits are -// the actual data of the number. The bytes are ordered big endian, unlike the -// varInt used by protobuf or provided by package encoding/binary. -// -// https://github.com/FactomProject/FactomDocs/blob/master/factomDataStructureDetails.md#variable-integers-varint_f -package varintf - -import ( - "math/bits" -) - -const continuationBitMask = 0x80 - -// Encode x into varInt_F bytes. -func Encode(x uint64) []byte { - bitlen := bits.Len64(x) - buflen := bitlen / 7 - if bitlen == 0 || bitlen%7 > 0 { - buflen++ - } - buf := make([]byte, buflen) - for i := range buf { - buf[i] = continuationBitMask | uint8(x>>uint((buflen-i-1)*7)) - } - // Unset continuation bit in last byte. - buf[buflen-1] &^= continuationBitMask - return buf -} - -// Decode varInt_F bytes into a uint64 and return the number of bytes used. If -// buf encodes a number larger than 64 bits, 0 and -1 is returned. -func Decode(buf []byte) (uint64, int) { - buflen := 1 - for b := buf[0]; b&continuationBitMask > 0; b = buf[buflen-1] { - buflen++ - } - if buflen > 10 || (buflen == 10 && buf[0] > 0x81) { - return 0, -1 - } - var x uint64 - for i := 0; i < buflen; i++ { - x |= uint64(buf[i]&^continuationBitMask) << uint((buflen-i-1)*7) - } - return x, buflen -} diff --git a/factom/varintf/varintf_test.go b/factom/varintf/varintf_test.go deleted file mode 100644 index 2e2c66a..0000000 --- a/factom/varintf/varintf_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package varintf - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEncodeDecode(t *testing.T) { - assert := assert.New(t) - for x := uint64(1); x > 0; x <<= 1 { - buf := Encode(x) - d, l := Decode(buf) - assert.Equalf(x, d, "%x", int(x)) - assert.Equalf(len(buf), l, "%x", int(x)) - } -} - -var testFactomSpecExamples = []struct { - X uint64 - Buf []byte -}{{ - X: 0, - Buf: []byte{0}, -}, { - X: 3, - Buf: []byte{3}, -}, { - X: 127, - Buf: []byte{127}, -}, { - X: 128, - // 10000001 00000000 - Buf: []byte{0x81, 0}, -}, { - X: 130, - // 10000001 00000010 - Buf: []byte{0x81, 2}, -}, { - X: (1 << 16) - 1, // 2^16 - 1 - // 10000011 11111111 01111111 - Buf: []byte{0x83, 0xff, 0x7f}, -}, { - X: 1 << 16, // 2^16 - // 10000100 10000000 00000000 - Buf: []byte{0x84, 0x80, 0}, -}, { - X: (1 << 32) - 1, // 2^32 - 1 - // 10001111 11111111 11111111 11111111 01111111 - Buf: []byte{0x8f, 0xff, 0xff, 0xff, 0x7f}, -}, { - X: 1 << 32, // 2^32 - // 10010000 10000000 10000000 10000000 00000000 - Buf: []byte{0x90, 0x80, 0x80, 0x80, 0x00}, -}, { - X: (1 << 63) - 1, // 2^63 - 1 - // 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 01111111 - Buf: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, -}, { - X: (1 << 64) - 1, // 2^64 - 1 - // 10000001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 01111111 - Buf: []byte{0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, -}} - -func TestFactomSpecExamples(t *testing.T) { - assert := assert.New(t) - for _, test := range testFactomSpecExamples { - buf := Encode(test.X) - x, l := Decode(test.Buf) - assert.Equalf(test.Buf, buf, "%x", int(test.X)) - assert.Equalf(test.X, x, "%x", int(test.X)) - assert.Equalf(len(buf), l, "%x", int(test.X)) - } -} - -func BenchmarkDecode(b *testing.B) { - var buf []byte - for i := 0; i < b.N; i++ { - buf = Encode(uint64((1 << uint(i%64)) - i)) - } - _ = buf -} -func BenchmarkEncodeDecode(b *testing.B) { - var buf []byte - var x uint64 - var l int - for i := 0; i < b.N; i++ { - buf = Encode(uint64((1 << uint(i%64)) - i)) - x, l = Decode(buf) - } - _ = buf - _ = x - _ = l - -} diff --git a/fat/chainid.go b/fat/chainid.go index e0dcdf3..271a083 100644 --- a/fat/chainid.go +++ b/fat/chainid.go @@ -25,12 +25,12 @@ package fat import ( "unicode/utf8" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" ) -// ValidTokenNameIDs returns true if the nameIDs match the pattern for a valid -// token chain. -func ValidTokenNameIDs(nameIDs []factom.Bytes) bool { +// ValidNameIDs returns true if the nameIDs match the pattern for a valid token +// chain. +func ValidNameIDs(nameIDs []factom.Bytes) bool { if len(nameIDs) == 4 && len(nameIDs[1]) > 0 && string(nameIDs[0]) == "token" && string(nameIDs[2]) == "issuer" && factom.ValidIdentityChainID(nameIDs[3]) && @@ -41,14 +41,25 @@ func ValidTokenNameIDs(nameIDs []factom.Bytes) bool { } // NameIDs returns valid NameIDs -func NameIDs(tokenID string, issuerChainID factom.Bytes32) []factom.Bytes { +func NameIDs(tokenID string, issuerChainID *factom.Bytes32) []factom.Bytes { return []factom.Bytes{ []byte("token"), []byte(tokenID), []byte("issuer"), issuerChainID[:], } } -// ChainID returns the chain ID for a given token ID and issuer Chain ID. -func ChainID(tokenID string, issuerChainID factom.Bytes32) factom.Bytes32 { - return factom.ChainID(NameIDs(tokenID, issuerChainID)) +// ComputeChainID returns the ChainID for a given tokenID and issuerChainID. +func ComputeChainID(tokenID string, issuerChainID *factom.Bytes32) factom.Bytes32 { + return factom.ComputeChainID(NameIDs(tokenID, issuerChainID)) +} + +// ParseTokenIssuer returns the tokenID and identityChainID for a given set of +// nameIDs. +// +// The caller must ensure that ValidNameIDs(nameIDs) returns true or else +// TokenIssuer will return garbage data or may panic. +func ParseTokenIssuer(nameIDs []factom.Bytes) (string, factom.Bytes32) { + var identityChainID factom.Bytes32 + copy(identityChainID[:], nameIDs[3]) + return string(nameIDs[1]), identityChainID } diff --git a/fat/chainid_test.go b/fat/chainid_test.go new file mode 100644 index 0000000..14eaf91 --- /dev/null +++ b/fat/chainid_test.go @@ -0,0 +1,85 @@ +package fat + +import ( + "testing" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/stretchr/testify/assert" +) + +var validIdentityChainID = factom.NewBytes32( + "88888807e4f3bbb9a2b229645ab6d2f184224190f83e78761674c2362aca4425") + +func validNameIDs() []factom.Bytes { + return []factom.Bytes{ + factom.Bytes("token"), + factom.Bytes("valid"), + factom.Bytes("issuer"), + validIdentityChainID[:], + } +} + +func TestNameIDs(t *testing.T) { + nameIDs := NameIDs("valid", &validIdentityChainID) + assert.ElementsMatch(t, validNameIDs(), nameIDs) +} +func TestParseTokenIssuer(t *testing.T) { + token, identity := ParseTokenIssuer(validNameIDs()) + assert.Equal(t, "valid", token) + assert.Equal(t, validIdentityChainID, identity) +} + +func TestChainID(t *testing.T) { + expected := factom.NewBytes32( + "b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb") + computed := ComputeChainID("test", &validIdentityChainID) + assert.Equal(t, expected, computed) +} + +func invalidNameIDs(i int) []factom.Bytes { + n := validNameIDs() + n[i] = factom.Bytes{} + return n +} + +var validNameIDsTests = []struct { + Name string + NameIDs []factom.Bytes + Valid bool +}{{ + Name: "valid", + Valid: true, + NameIDs: validNameIDs(), +}, { + Name: "invalid length (short)", + NameIDs: validNameIDs()[0:3], +}, { + Name: "invalid length (long)", + NameIDs: append(validNameIDs()[:], factom.Bytes{}), +}, { + Name: "invalid", + NameIDs: invalidNameIDs(0), +}, { + Name: "invalid ExtID", + NameIDs: invalidNameIDs(1), +}, { + Name: "invalid ExtID", + NameIDs: invalidNameIDs(2), +}, { + Name: "invalid ExtID", + NameIDs: invalidNameIDs(3), +}} + +func TestValidNameIDs(t *testing.T) { + for _, test := range validNameIDsTests { + t.Run(test.Name, func(t *testing.T) { + assert := assert.New(t) + valid := ValidNameIDs(test.NameIDs) + if test.Valid { + assert.True(valid) + } else { + assert.False(valid) + } + }) + } +} diff --git a/fat/entry.go b/fat/entry.go deleted file mode 100644 index 84fa4f3..0000000 --- a/fat/entry.go +++ /dev/null @@ -1,181 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat - -import ( - "crypto/sha256" - "crypto/sha512" - "encoding/json" - "fmt" - "math/rand" - "strconv" - "time" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" - - "golang.org/x/crypto/ed25519" -) - -// Entry has variables and methods common to all fat0 entries. -type Entry struct { - Metadata json.RawMessage `json:"metadata,omitempty"` - - factom.Entry `json:"-"` -} - -// UnmarshalEntry unmarshals the content of the factom.Entry into the provided -// variable v, disallowing all unknown fields. -func (e Entry) UnmarshalEntry(v interface{}) error { - return json.Unmarshal(e.Content, v) -} - -func (e Entry) MetadataJSONLen() int { - if e.Metadata == nil { - return 0 - } - return len(`,"metadata":`) + len(e.Metadata) -} -func (e *Entry) MarshalEntry(v interface{}) error { - var err error - e.Content, err = json.Marshal(v) - return err -} - -// ValidExtIDs validates the structure of the ExtIDs of the factom.Entry to -// make sure that it has a valid timestamp salt and a valid set of -// RCD/signature pairs. -func (e Entry) ValidExtIDs(numRCDSigPairs int) error { - if numRCDSigPairs == 0 || len(e.ExtIDs) != 2*numRCDSigPairs+1 { - return fmt.Errorf("invalid number of ExtIDs") - } - if err := e.validTimestamp(); err != nil { - return err - } - extIDs := e.ExtIDs[1:] - for i := 0; i < len(extIDs)/2; i++ { - rcd := extIDs[i*2] - if len(rcd) != factom.RCDSize { - return fmt.Errorf("ExtIDs[%v]: invalid RCD size", i+1) - } - if rcd[0] != factom.RCDType { - return fmt.Errorf("ExtIDs[%v]: invalid RCD type", i+1) - } - sig := extIDs[i*2+1] - if len(sig) != factom.SignatureSize { - return fmt.Errorf("ExtIDs[%v]: invalid signature size", i+1) - } - } - return e.validSignatures() -} -func (e Entry) validTimestamp() error { - sec, err := strconv.ParseInt(string(e.ExtIDs[0]), 10, 64) - if err != nil { - return fmt.Errorf("timestamp salt: %v", err) - } - ts := time.Unix(sec, 0) - diff := e.Timestamp.Sub(ts) - if -12*time.Hour > diff || diff > 12*time.Hour { - return fmt.Errorf("timestamp salt expired") - } - return nil -} -func (e Entry) validSignatures() error { - // Compose the signed message data using exactly allocated bytes. - numRcdSigPairs := len(e.ExtIDs) / 2 - maxRcdSigIDSalt := numRcdSigPairs - 1 - maxRcdSigIDSaltStrLen := jsonlen.Uint64(uint64(maxRcdSigIDSalt)) - timeSalt := e.ExtIDs[0] - maxMsgLen := maxRcdSigIDSaltStrLen + len(timeSalt) + len(e.ChainID) + len(e.Content) - msg := make([]byte, maxMsgLen) - i := maxRcdSigIDSaltStrLen - i += copy(msg[i:], timeSalt[:]) - i += copy(msg[i:], e.ChainID[:]) - copy(msg[i:], e.Content) - - rcdSigs := e.ExtIDs[1:] // Skip over timestamp salt in ExtID[0] - for rcdSigID := 0; rcdSigID < numRcdSigPairs; rcdSigID++ { - // Prepend the RCD Sig ID Salt to the message data - rcdSigIDSaltStr := strconv.FormatUint(uint64(rcdSigID), 10) - start := maxRcdSigIDSaltStrLen - len(rcdSigIDSaltStr) - copy(msg[start:], rcdSigIDSaltStr) - - msgHash := sha512.Sum512(msg[start:]) - pubKey := []byte(rcdSigs[rcdSigID*2][1:]) // Omit RCD Type byte - sig := rcdSigs[rcdSigID*2+1] - if !ed25519.Verify(pubKey, msgHash[:], sig) { - return fmt.Errorf("ExtIDs[%v]: invalid signature", rcdSigID*2+2) - } - } - return nil -} - -// Sign the RCD/Sig ID Salt + Timestamp Salt + Chain ID Salt + Content of the -// factom.Entry and add the RCD + signature pairs for the given addresses to -// the ExtIDs. This clears any existing ExtIDs. -func (e *Entry) Sign(signingSet ...factom.RCDPrivateKey) { - // Set the Entry's timestamp so that the signatures will verify against - // this time salt. - timeSalt := newTimestampSalt() - e.Timestamp = time.Now() - - // Compose the signed message data using exactly allocated bytes. - maxRcdSigIDSaltStrLen := jsonlen.Uint64(uint64(len(signingSet))) - maxMsgLen := maxRcdSigIDSaltStrLen + len(timeSalt) + len(e.ChainID) + len(e.Content) - msg := make(factom.Bytes, maxMsgLen) - i := maxRcdSigIDSaltStrLen - i += copy(msg[i:], timeSalt[:]) - i += copy(msg[i:], e.ChainID[:]) - copy(msg[i:], e.Content) - - // Generate the ExtIDs for each address in the signing set. - e.ExtIDs = make([]factom.Bytes, 1, len(signingSet)*2+1) - e.ExtIDs[0] = timeSalt - for rcdSigID, a := range signingSet { - // Compose the RcdSigID salt and prepend it to the message. - rcdSigIDSalt := strconv.FormatUint(uint64(rcdSigID), 10) - start := maxRcdSigIDSaltStrLen - len(rcdSigIDSalt) - copy(msg[start:], rcdSigIDSalt) - - msgHash := sha512.Sum512(msg[start:]) - sig := ed25519.Sign(a.PrivateKey(), msgHash[:]) - e.ExtIDs = append(e.ExtIDs, a.RCD(), sig) - } -} -func newTimestampSalt() []byte { - timestamp := time.Now().Add(time.Duration(-rand.Int63n(int64(1 * time.Hour)))) - return []byte(strconv.FormatInt(timestamp.Unix(), 10)) -} - -// FAAddress computes the FAAddress corresponding to the rcdSigID'th RCD/Sig -// pair. -func (e Entry) FAAddress(rcdSigID int) factom.FAAddress { - id := rcdSigID*2 + 1 - return factom.FAAddress(sha256d(e.ExtIDs[id])) -} - -// sha256d computes two rounds of the sha256 hash. -func sha256d(data []byte) [sha256.Size]byte { - hash := sha256.Sum256(data) - return sha256.Sum256(hash[:]) -} diff --git a/fat/entry_test.go b/fat/entry_test.go deleted file mode 100644 index ae80d2b..0000000 --- a/fat/entry_test.go +++ /dev/null @@ -1,199 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat_test - -import ( - "math/rand" - "strconv" - "testing" - "time" - - "github.com/Factom-Asset-Tokens/fatd/factom" - . "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/stretchr/testify/assert" -) - -var validExtIDsTests = []struct { - Name string - Error string - Entry -}{{ - Name: "valid", - Entry: validEntry(), -}, { - Name: "valid (large signing set)", - Entry: func() Entry { - e := validEntry() - adrs := make([]factom.RCDPrivateKey, 100) - for i := range adrs { - adr, _ := factom.GenerateFsAddress() - adrs[i] = adr - } - e.Sign(adrs...) - return e - }(), -}, { - Name: "nil ExtIDs", - Error: "invalid number of ExtIDs", - Entry: func() Entry { - e := validEntry() - e.ExtIDs = nil - return e - }(), -}, { - Name: "extra ExtIDs", - Error: "invalid number of ExtIDs", - Entry: func() Entry { - e := validEntry() - e.ExtIDs = append(e.ExtIDs, factom.Bytes{}) - return e - }(), -}, { - Name: "invalid timestamp (format)", - Error: "timestamp salt: strconv.ParseInt: parsing \"xxxx\": invalid syntax", - Entry: func() Entry { - e := validEntry() - e.ExtIDs[0] = []byte("xxxx") - return e - }(), -}, { - Name: "invalid timestamp (expired)", - Error: "timestamp salt expired", - Entry: func() Entry { - e := validEntry() - e.Timestamp = new(factom.Time) - *e.Timestamp = factom.Time(time.Now().Add(-48 * time.Hour)) - return e - }(), -}, { - Name: "invalid timestamp (expired)", - Error: "timestamp salt expired", - Entry: func() Entry { - e := validEntry() - e.Timestamp = new(factom.Time) - *e.Timestamp = factom.Time(time.Now().Add(48 * time.Hour)) - return e - }(), -}, { - Name: "invalid RCD size", - Error: "ExtIDs[1]: invalid RCD size", - Entry: func() Entry { - e := validEntry() - e.ExtIDs[1] = append(e.ExtIDs[1], 0x00) - return e - }(), -}, { - Name: "invalid RCD type", - Error: "ExtIDs[1]: invalid RCD type", - Entry: func() Entry { - e := validEntry() - e.ExtIDs[1][0]++ - return e - }(), -}, { - Name: "invalid signature size", - Entry: func() Entry { - e := validEntry() - e.ExtIDs[2] = append(e.ExtIDs[2], 0x00) - return e - }(), - Error: "ExtIDs[1]: invalid signature size", -}, { - Name: "invalid signatures", - Entry: func() Entry { - e := validEntry() - e.ExtIDs[2][0]++ - return e - }(), - Error: "ExtIDs[2]: invalid signature", -}, { - Name: "invalid signatures (transpose)", - Entry: func() Entry { - e := validEntry() - rcdSig := e.ExtIDs[1:3] - e.ExtIDs[1] = e.ExtIDs[3] - e.ExtIDs[2] = e.ExtIDs[4] - e.ExtIDs[3] = rcdSig[0] - e.ExtIDs[4] = rcdSig[1] - return e - }(), - Error: "ExtIDs[2]: invalid signature", -}, { - Name: "invalid signatures (timestamp)", - Entry: func() Entry { - e := validEntry() - ts := time.Now().Add(time.Duration( - -rand.Int63n(int64(12 * time.Hour)))) - timeSalt := []byte(strconv.FormatInt(ts.Unix(), 10)) - e.ExtIDs[0] = timeSalt - return e - }(), - Error: "ExtIDs[2]: invalid signature", -}, { - Name: "invalid signatures (chain ID)", - Entry: func() Entry { - e := validEntry() - e.ChainID = factom.NewBytes32(factom.Bytes{0x01, 0x02}) - return e - }(), - Error: "ExtIDs[2]: invalid signature", -}, -} - -func TestEntryValidExtIDs(t *testing.T) { - for _, test := range validExtIDsTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - err := test.Entry.ValidExtIDs(len(test.Entry.ExtIDs) / 2) - if len(test.Error) == 0 { - assert.NoError(err) - } else { - assert.EqualError(err, test.Error) - } - }) - } -} - -var randSource = rand.New(rand.NewSource(100)) - -func validEntry() Entry { - var e Entry - e.Content = factom.Bytes{0x00, 0x01, 0x02} - e.ChainID = factom.NewBytes32(nil) - // Generate valid signatures with blank Addresses. - adrs := twoAddresses() - e.Sign(adrs[0], adrs[1]) - return e -} - -func twoAddresses() []factom.FsAddress { - adrs := make([]factom.FsAddress, 2) - for i := range adrs { - adr, err := factom.GenerateFsAddress() - if err != nil { - panic(err) - } - adrs[i] = adr - } - return adrs -} diff --git a/fat/fat0/fat0.go b/fat/fat0/fat0.go deleted file mode 100644 index b21e2cb..0000000 --- a/fat/fat0/fat0.go +++ /dev/null @@ -1,27 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat0 - -import "github.com/Factom-Asset-Tokens/fatd/fat" - -const Type = fat.Type(0) diff --git a/fat/fat0/fat0_test.go b/fat/fat0/fat0_test.go deleted file mode 100644 index 775f1e0..0000000 --- a/fat/fat0/fat0_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat0_test - -import ( - "testing" - - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat0" - "github.com/stretchr/testify/assert" -) - -func TestType(t *testing.T) { - assert.Equal(t, fat.TypeFAT0, fat0.Type) -} diff --git a/fat/fat0/transaction.go b/fat/fat0/transaction.go deleted file mode 100644 index d767c40..0000000 --- a/fat/fat0/transaction.go +++ /dev/null @@ -1,177 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat0 - -import ( - "encoding/json" - "fmt" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" -) - -// Transaction represents a fat0 transaction, which can be a normal account -// transaction or a coinbase transaction depending on the Inputs and the -// RCD/signature pair. -type Transaction struct { - Inputs AddressAmountMap `json:"inputs"` - Outputs AddressAmountMap `json:"outputs"` - fat.Entry -} - -// NewTransaction returns a Transaction initialized with the given entry. -func NewTransaction(entry factom.Entry) Transaction { - return Transaction{Entry: fat.Entry{Entry: entry}} -} - -func (t *Transaction) UnmarshalJSON(data []byte) error { - data = jsonlen.Compact(data) - tRaw := struct { - Inputs json.RawMessage `json:"inputs"` - Outputs json.RawMessage `json:"outputs"` - fat.Entry - }{} - if err := json.Unmarshal(data, &tRaw); err != nil { - return fmt.Errorf("%T: %v", t, err) - } - if err := t.Inputs.UnmarshalJSON(tRaw.Inputs); err != nil { - return fmt.Errorf("%T.Inputs: %v", t, err) - } - if err := t.Outputs.UnmarshalJSON(tRaw.Outputs); err != nil { - return fmt.Errorf("%T.Outputs: %v", t, err) - } - t.Metadata = tRaw.Metadata - - if err := t.ValidData(); err != nil { - return fmt.Errorf("%T: %v", t, err) - } - - expectedJSONLen := len(`{"inputs":,"outputs":}`) + - len(tRaw.Inputs) + len(tRaw.Outputs) + - tRaw.MetadataJSONLen() - if expectedJSONLen != len(data) { - return fmt.Errorf("%T: unexpected JSON length", t) - } - - return nil -} - -type transaction Transaction - -func (t Transaction) MarshalJSON() ([]byte, error) { - if err := t.ValidData(); err != nil { - return nil, err - } - return json.Marshal(transaction(t)) -} - -func (t Transaction) String() string { - data, err := t.MarshalJSON() - if err != nil { - return err.Error() - } - return string(data) -} - -// UnmarshalEntry unmarshals the entry content as a Transaction. -func (t *Transaction) UnmarshalEntry() error { - return t.Entry.UnmarshalEntry(t) -} - -// MarshalEntry marshals the Transaction into the Entry content. -func (t *Transaction) MarshalEntry() error { - return t.Entry.MarshalEntry(t) -} - -// IsCoinbase returns true if the coinbase address is in t.Input. This does not -// necessarily mean that t is a valid coinbase transaction. -func (t Transaction) IsCoinbase() bool { - amount := t.Inputs[fat.Coinbase()] - return amount != 0 -} - -// Valid performs all validation checks and returns nil if t is a valid -// Transaction. If t is a coinbase transaction then idKey is used to validate -// the RCD. Otherwise RCDs are checked against the input addresses. -func (t *Transaction) Valid(idKey factom.IDKey) error { - if err := t.UnmarshalEntry(); err != nil { - return err - } - if err := t.ValidExtIDs(); err != nil { - return err - } - if t.IsCoinbase() { - if t.FAAddress(0) != idKey.RCDHash() { - return fmt.Errorf("invalid RCD") - } - } else { - if !t.ValidRCDs() { - return fmt.Errorf("invalid RCDs") - } - } - return nil -} - -// ValidData validates the Transaction data and returns nil if no errors are -// present. ValidData assumes that the entry content has been unmarshaled. -func (t Transaction) ValidData() error { - if t.Inputs.Sum() != t.Outputs.Sum() { - return fmt.Errorf("sum(inputs) != sum(outputs)") - } - // Coinbase transactions must only have one input. - if t.IsCoinbase() && len(t.Inputs) != 1 { - return fmt.Errorf("invalid coinbase transaction") - } - // Ensure that no address exists in both the Inputs and Outputs. - if err := t.Inputs.NoAddressIntersection(t.Outputs); err != nil { - return err - } - return nil -} - -// ValidExtIDs validates the structure of the external IDs of the entry to make -// sure that it has the correct number of RCD/signature pairs. ValidExtIDs does -// not validate the content of the RCD or signature. ValidExtIDs assumes that -// the entry content has been unmarshaled and that ValidData returns nil. -func (t Transaction) ValidExtIDs() error { - return t.Entry.ValidExtIDs(len(t.Inputs)) -} - -func (t Transaction) ValidRCDs() bool { - // Create a map of all RCDs that are present in the ExtIDs. - rcdHashes := make(map[factom.FAAddress]struct{}, len(t.Inputs)) - extIDs := t.ExtIDs[1:] - for i := 0; i < len(extIDs)/2; i++ { - rcdHashes[t.FAAddress(i)] = struct{}{} - } - - // Ensure that for all Inputs there is a corresponding RCD in the - // ExtIDs. - for rcdHash := range t.Inputs { - if _, ok := rcdHashes[rcdHash]; !ok { - return false - } - } - return true -} diff --git a/fat/fat1/fat1.go b/fat/fat1/fat1.go deleted file mode 100644 index df32e5c..0000000 --- a/fat/fat1/fat1.go +++ /dev/null @@ -1,27 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat1 - -import "github.com/Factom-Asset-Tokens/fatd/fat" - -const Type = fat.Type(1) diff --git a/fat/fat1/fat1_test.go b/fat/fat1/fat1_test.go deleted file mode 100644 index 98e516f..0000000 --- a/fat/fat1/fat1_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat1_test - -import ( - "testing" - - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/stretchr/testify/assert" -) - -func TestType(t *testing.T) { - assert.Equal(t, fat.TypeFAT1, fat1.Type) -} diff --git a/fat/fat1/transaction.go b/fat/fat1/transaction.go deleted file mode 100644 index 74dad37..0000000 --- a/fat/fat1/transaction.go +++ /dev/null @@ -1,191 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package fat1 - -import ( - "encoding/json" - "fmt" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" -) - -// Transaction represents a fat1 transaction, which can be a normal account -// transaction or a coinbase transaction depending on the Inputs and the -// RCD/signature pair. -type Transaction struct { - Inputs AddressNFTokensMap `json:"inputs"` - Outputs AddressNFTokensMap `json:"outputs"` - TokenMetadata NFTokenIDMetadataMap `json:"tokenmetadata,omitempty"` - fat.Entry -} - -// NewTransaction returns a Transaction initialized with the given entry. -func NewTransaction(entry factom.Entry) Transaction { - return Transaction{Entry: fat.Entry{Entry: entry}} -} - -func (t *Transaction) UnmarshalJSON(data []byte) error { - tRaw := struct { - Inputs json.RawMessage `json:"inputs"` - Outputs json.RawMessage `json:"outputs"` - TokenMetadata json.RawMessage `json:"tokenmetadata"` - fat.Entry - }{} - if err := json.Unmarshal(data, &tRaw); err != nil { - return fmt.Errorf("%T: %v", t, err) - } - if err := t.Inputs.UnmarshalJSON(tRaw.Inputs); err != nil { - return fmt.Errorf("%T.Inputs: %v", t, err) - } - var expectedJSONLen int - if len(tRaw.TokenMetadata) > 0 { - if !t.IsCoinbase() { - return fmt.Errorf(`%T: %v`, t, - `invalid field for non-coinbase transaction: "tokenmetadata"`) - } - if err := t.TokenMetadata.UnmarshalJSON(tRaw.TokenMetadata); err != nil { - return fmt.Errorf("%T.TokenMetadata: %v", t, err) - - } - if err := t.TokenMetadata.IsSubsetOf(t.Inputs[fat.Coinbase()]); err != nil { - return fmt.Errorf("%T.TokenMetadata: %v", t, err) - } - - expectedJSONLen = len(`,"tokenmetadata":`) + - len(jsonlen.Compact(tRaw.TokenMetadata)) - } else { - if t.IsCoinbase() { - // Avoid a nil map. - t.TokenMetadata = make(NFTokenIDMetadataMap, 0) - } - } - - if err := t.Outputs.UnmarshalJSON(tRaw.Outputs); err != nil { - return fmt.Errorf("%T.Outputs: %v", t, err) - } - t.Metadata = tRaw.Metadata - - if err := t.ValidData(); err != nil { - return fmt.Errorf("%T: %v", t, err) - } - - expectedJSONLen += len(`{"inputs":,"outputs":}`) + - len(jsonlen.Compact(tRaw.Inputs)) + len(jsonlen.Compact(tRaw.Outputs)) + - tRaw.MetadataJSONLen() - if expectedJSONLen != len(jsonlen.Compact(data)) { - return fmt.Errorf("%T: unexpected JSON length", t) - } - - return nil -} - -type transaction Transaction - -func (t Transaction) MarshalJSON() ([]byte, error) { - if err := t.ValidData(); err != nil { - return nil, err - } - return json.Marshal(transaction(t)) -} - -func (t Transaction) String() string { - data, err := t.MarshalJSON() - if err != nil { - return err.Error() - } - return string(data) -} - -func (t Transaction) ValidData() error { - if err := t.Inputs.NoAddressIntersection(t.Outputs); err != nil { - return fmt.Errorf("Inputs and Outputs intersect: %v", err) - } - if err := t.Inputs.NFTokenIDsConserved(t.Outputs); err != nil { - return fmt.Errorf("Inputs and Outputs mismatch: %v", err) - } - // Coinbase transactions must only have one input. - if t.IsCoinbase() && len(t.Inputs) != 1 { - return fmt.Errorf("invalid coinbase transaction") - } - return nil -} - -// IsCoinbase returns true if the coinbase address is in t.Input. This does not -// necessarily mean that t is a valid coinbase transaction. -func (t Transaction) IsCoinbase() bool { - tkns := t.Inputs[fat.Coinbase()] - return len(tkns) != 0 -} - -// UnmarshalEntry unmarshals the entry content as a Transaction. -func (t *Transaction) UnmarshalEntry() error { - return t.Entry.UnmarshalEntry(t) -} - -// MarshalEntry marshals the Transaction into the Entry content. -func (t *Transaction) MarshalEntry() error { - return t.Entry.MarshalEntry(t) -} - -func (t *Transaction) Valid(idKey factom.IDKey) error { - if err := t.UnmarshalEntry(); err != nil { - return err - } - if err := t.ValidExtIDs(); err != nil { - return err - } - if t.IsCoinbase() { - if t.FAAddress(0) != idKey.RCDHash() { - return fmt.Errorf("invalid RCD") - } - } else { - if !t.ValidRCDs() { - return fmt.Errorf("invalid RCDs") - } - } - return nil -} - -func (t Transaction) ValidExtIDs() error { - return t.Entry.ValidExtIDs(len(t.Inputs)) -} - -func (t Transaction) ValidRCDs() bool { - // Create a map of all RCDs that are present in the ExtIDs. - adrs := make(map[factom.FAAddress]struct{}, len(t.Inputs)) - extIDs := t.ExtIDs[1:] - for i := 0; i < len(extIDs)/2; i++ { - adrs[t.FAAddress(i)] = struct{}{} - } - - // Ensure that for all Inputs there is a corresponding RCD in the - // ExtIDs. - for inputAdr := range t.Inputs { - if _, ok := adrs[inputAdr]; !ok { - return false - } - } - return true -} diff --git a/fat/issuance.go b/fat/issuance.go index ab2d7df..3904322 100644 --- a/fat/issuance.go +++ b/fat/issuance.go @@ -26,112 +26,102 @@ import ( "encoding/json" "fmt" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat103" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" ) -var ( - coinbase = func() factom.FAAddress { - priv := factom.FsAddress{} - return priv.FAAddress() - }() -) +var coinbase = factom.FsAddress{}.FAAddress() func Coinbase() factom.FAAddress { return coinbase } -// Issuance represents the Issuance of a token. +const MaxPrecision = 18 + type Issuance struct { - Type Type `json:"type"` - Supply int64 `json:"supply"` + Type Type `json:"type"` + Supply int64 `json:"supply"` + Precision uint `json:"precision,omitempty"` Symbol string `json:"symbol,omitempty"` - Entry -} -type issuance Issuance + Metadata json.RawMessage `json:"metadata,omitempty"` -func (i *Issuance) UnmarshalJSON(data []byte) error { - data = jsonlen.Compact(data) - if err := json.Unmarshal(data, (*issuance)(i)); err != nil { - return fmt.Errorf("%T: %v", i, err) + Entry factom.Entry `json:"-"` +} + +func NewIssuance(e factom.Entry, idKey *factom.Bytes32) (Issuance, error) { + var i Issuance + if err := i.UnmarshalJSON(e.Content); err != nil { + return i, err } - if err := i.ValidData(); err != nil { - return fmt.Errorf("%T: %v", i, err) + + if i.Supply == 0 || i.Supply < -1 { + return i, fmt.Errorf(`invalid "supply": must be positive or -1`) } - if i.expectedJSONLength() != len(data) { - return fmt.Errorf("%T: unexpected JSON length", i) + + if len(i.Symbol) > 4 { + return i, fmt.Errorf(`invalid "symbol": exceeds 4 characters`) } - return nil -} -func (i Issuance) expectedJSONLength() int { - l := len(`{}`) - l += len(`"type":""`) + len(i.Type.String()) - l += len(`,"supply":`) + jsonlen.Int64(i.Supply) - l += jsonStrLen("symbol", i.Symbol) - l += i.MetadataJSONLen() - return l -} -func jsonStrLen(name, value string) int { - if len(value) == 0 { - return 0 + + switch i.Type { + case TypeFAT0: + if i.Precision != 0 && i.Precision > MaxPrecision { + return i, fmt.Errorf(`invalid "precision": out of range [0-18]`) + } + case TypeFAT1: + if i.Precision != 0 { + return i, fmt.Errorf( + `invalid "precision": not allowed for FAT-1`) + } + default: + return i, fmt.Errorf(`invalid "type": %v`, i.Type) } - return len(`,"":""`) + len(name) + len(value) -} -func (i Issuance) MarshalJSON() ([]byte, error) { - if err := i.ValidData(); err != nil { - return nil, err + expected := map[factom.Bytes32]struct{}{*idKey: struct{}{}} + if err := fat103.Validate(e, expected); err != nil { + return i, err } - return json.Marshal(issuance(i)) -} -// NewIssuance returns an Issuance initialized with the given entry. -func NewIssuance(entry factom.Entry) Issuance { - return Issuance{Entry: Entry{Entry: entry}} -} + i.Entry = e -// UnmarshalEntry unmarshals the entry content as an Issuance. -func (i *Issuance) UnmarshalEntry() error { - return i.Entry.UnmarshalEntry(i) + return i, nil } -// MarshalEntry marshals the entry content as an Issuance. -func (i *Issuance) MarshalEntry() error { - return i.Entry.MarshalEntry(i) +func (i Issuance) Sign(idKey factom.RCDPrivateKey) (factom.Entry, error) { + e := i.Entry + content, err := json.Marshal(i) + if err != nil { + return e, err + } + e.Content = content + return fat103.Sign(e, idKey), nil } -// Valid performs all validation checks and returns nil if i is a valid -// Issuance. -func (i *Issuance) Valid(idKey factom.IDKey) error { - if err := i.UnmarshalEntry(); err != nil { - return err - } - if err := i.ValidExtIDs(); err != nil { - return err +func (i *Issuance) UnmarshalJSON(data []byte) error { + data = jsonlen.Compact(data) + type _i Issuance + if err := json.Unmarshal(data, (*_i)(i)); err != nil { + return fmt.Errorf("%T: %w", i, err) } - if i.FAAddress(0) != idKey.Payload() { - return fmt.Errorf("invalid RCD") + if i.expectedJSONLength() != len(data) { + return fmt.Errorf("%T: unexpected JSON length", i) } return nil } - -// ValidData validates the Issuance data and returns nil if no errors are -// present. ValidData assumes that the entry content has been unmarshaled. -func (i Issuance) ValidData() error { - if !i.Type.IsValid() { - return fmt.Errorf(`invalid "type": %v`, i.Type) +func (i Issuance) expectedJSONLength() int { + l := len(`{}`) + l += len(`"type":""`) + len(i.Type.String()) + l += len(`,"supply":`) + jsonlen.Int64(i.Supply) + if i.Precision != 0 { + l += len(`,"precision":`) + jsonlen.Uint64(uint64(i.Precision)) } - if i.Supply == 0 || i.Supply < -1 { - return fmt.Errorf(`invalid "supply": must be positive or -1`) + if len(i.Symbol) > 0 { + l += len(`,"symbol":""`) + len(i.Symbol) } - return nil -} - -// ValidExtIDs validates the structure of the external IDs of the entry to make -// sure that it has an RCD and signature. It does not validate the content of -// the RCD or signature. -func (i Issuance) ValidExtIDs() error { - return i.Entry.ValidExtIDs(1) + if i.Metadata != nil { + l += len(`,"metadata":`) + len(i.Metadata) + } + return l } diff --git a/fat/issuance_test.go b/fat/issuance_test.go index c8198fd..51107e0 100644 --- a/fat/issuance_test.go +++ b/fat/issuance_test.go @@ -20,322 +20,296 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package fat_test +package fat import ( - "encoding/hex" "encoding/json" "testing" - "github.com/Factom-Asset-Tokens/fatd/factom" - . "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat103" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var humanReadableZeroAddress = "FA1zT4aFpEvcnPqPCigB3fvGu4Q4mTXY22iiuV69DqE1pNhdF2MC" - -var validIdentityChainIDStr = "88888807e4f3bbb9a2b229645ab6d2f184224190f83e78761674c2362aca4425" - -func validIdentityChainID() factom.Bytes { - return hexToBytes(validIdentityChainIDStr) -} - -func hexToBytes(hexStr string) factom.Bytes { - raw, err := hex.DecodeString(hexStr) - if err != nil { - panic(err) - } - return factom.Bytes(raw) -} +var coinbaseAddressStr = "FA1zT4aFpEvcnPqPCigB3fvGu4Q4mTXY22iiuV69DqE1pNhdF2MC" func TestCoinbase(t *testing.T) { a := Coinbase() - require := require.New(t) - require.Equal(humanReadableZeroAddress, a.String()) + assert.Equal(t, coinbaseAddressStr, a.String()) } var ( - identityChainID = factom.NewBytes32(validIdentityChainID()) + issuerKey = issuerSecret.ID1Key() + issuerSecret = func() factom.SK1Key { + a, _ := factom.GenerateSK1Key() + return a + }() + chainID = factom.Bytes32{1: 1, 2: 2} ) -func TestChainID(t *testing.T) { - assert.Equal(t, "b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb", - ChainID("test", *identityChainID).String()) -} - -var validTokenNameIDsTests = []struct { - Name string - NameIDs []factom.Bytes - Valid bool -}{{ - Name: "valid", - Valid: true, - NameIDs: validTokenNameIDs(), -}, { - Name: "invalid length (short)", - NameIDs: validTokenNameIDs()[0:3], -}, { - Name: "invalid length (long)", - NameIDs: append(validTokenNameIDs(), factom.Bytes{}), -}, { - Name: "invalid ExtID", - NameIDs: invalidTokenNameIDs(0), -}, { - Name: "invalid ExtID", - NameIDs: invalidTokenNameIDs(1), -}, { - Name: "invalid ExtID", - NameIDs: invalidTokenNameIDs(2), -}, { - Name: "invalid ExtID", - NameIDs: invalidTokenNameIDs(3), -}} - -func TestValidTokenNameIDs(t *testing.T) { - for _, test := range validTokenNameIDsTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - valid := ValidTokenNameIDs(test.NameIDs) - if test.Valid { - assert.True(valid) - } else { - assert.False(valid) - } - }) - } -} - -func validTokenNameIDs() []factom.Bytes { - return []factom.Bytes{ - factom.Bytes("token"), - factom.Bytes("valid"), - factom.Bytes("issuer"), - identityChainID[:], - } +type issuanceTest struct { + Name string + Error string + Entry factom.Entry + IssuerKey *factom.ID1Key } -func invalidTokenNameIDs(i int) []factom.Bytes { - n := validTokenNameIDs() - n[i] = factom.Bytes{} - return n -} +var issuanceTests = []issuanceTest{{ + Name: "valid", + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: 100000, + Precision: 3, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), -var issuanceTests = []struct { - Name string - Error string - IssuerKey factom.ID1Key - Issuance -}{{ - Name: "valid", - IssuerKey: issuerKey, - Issuance: validIssuance(), -}, { - Name: "valid (omit symbol)", - IssuerKey: issuerKey, - Issuance: omitFieldIssuance("symbol"), -}, { - Name: "valid (omit name)", - IssuerKey: issuerKey, - Issuance: omitFieldIssuance("name"), -}, { - Name: "valid (omit metadata)", - IssuerKey: issuerKey, - Issuance: omitFieldIssuance("metadata"), -}, { - Name: "invalid JSON (unknown field)", - Error: `*fat.Issuance: unexpected JSON length`, - IssuerKey: issuerKey, - Issuance: setFieldIssuance("invalid", 5), -}, { - Name: "invalid JSON (invalid type)", - Error: `*fat.Issuance: *fat.Type: expected JSON string`, - IssuerKey: issuerKey, - Issuance: invalidIssuance("type"), -}, { - Name: "invalid JSON (invalid supply)", - Error: `*fat.Issuance: json: cannot unmarshal array into Go struct field issuance.supply of type int64`, - IssuerKey: issuerKey, - Issuance: invalidIssuance("supply"), -}, { - Name: "invalid JSON (invalid symbol)", - Error: `*fat.Issuance: json: cannot unmarshal array into Go struct field issuance.symbol of type string`, - IssuerKey: issuerKey, - Issuance: invalidIssuance("symbol"), -}, { - Name: "invalid JSON (nil)", - Error: `unexpected end of JSON input`, - IssuerKey: issuerKey, - Issuance: issuance(nil), -}, { - Name: "invalid data (type)", - Error: `*fat.Issuance: *fat.Type: invalid format`, - IssuerKey: issuerKey, - Issuance: setFieldIssuance("type", "invalid"), -}, { - Name: "invalid data (type omitted)", - Error: `*fat.Issuance: unexpected JSON length`, - IssuerKey: issuerKey, - Issuance: omitFieldIssuance("type"), -}, { - Name: "invalid data (supply: 0)", - Error: `*fat.Issuance: invalid "supply": must be positive or -1`, - IssuerKey: issuerKey, - Issuance: setFieldIssuance("supply", 0), -}, { - Name: "invalid data (supply: -5)", - Error: `*fat.Issuance: invalid "supply": must be positive or -1`, - IssuerKey: issuerKey, - Issuance: setFieldIssuance("supply", -5), -}, { - Name: "invalid data (supply: omitted)", - Error: `*fat.Issuance: invalid "supply": must be positive or -1`, - IssuerKey: issuerKey, - Issuance: omitFieldIssuance("supply"), -}, { - Name: "invalid ExtIDs (timestamp)", - Error: `timestamp salt expired`, - IssuerKey: issuerKey, - Issuance: func() Issuance { - i := validIssuance() - i.ExtIDs[0] = factom.Bytes("10") - return i + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e }(), }, { - Name: "invalid ExtIDs (length)", - Error: `invalid number of ExtIDs`, - IssuerKey: issuerKey, - Issuance: func() Issuance { - i := validIssuance() - i.ExtIDs = append(i.ExtIDs, factom.Bytes{}) - return i + Name: "valid (omit symbol)", + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: 100000, + Precision: 3, + Metadata: json.RawMessage(`"memo"`), + + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e }(), }, { - Name: "invalid RCD hash", - Error: `invalid RCD`, - Issuance: validIssuance(), -}} - -func TestIssuance(t *testing.T) { - for _, test := range issuanceTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - i := test.Issuance - key := test.IssuerKey - err := i.Valid(&key) - if len(test.Error) == 0 { - assert.NoError(err) - } else { - assert.EqualError(err, test.Error) - } - }) - } -} - -func validIssuanceEntryContentMap() map[string]interface{} { - return map[string]interface{}{ - "type": "FAT-0", - "supply": int64(100000), - "symbol": "TEST", - "metadata": []int{0}, - } -} + Name: "valid (omit metadata)", + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: 100000, + Precision: 3, + Symbol: "test", -func validIssuance() Issuance { - return issuance(marshal(validIssuanceEntryContentMap())) -} + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), +}, { + Name: "valid (type 1)", + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT1, + Supply: 100000, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), -var issuerSecret = func() factom.SK1Key { - a, _ := factom.GenerateSK1Key() - return a -}() -var issuerKey = issuerSecret.ID1Key() + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), +}, { + Name: "valid (unlimited)", + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: -1, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), -func issuance(content factom.Bytes) Issuance { - e := factom.Entry{ - ChainID: factom.NewBytes32(nil), - Content: content, - } - i := NewIssuance(e) - i.Sign(issuerSecret) - return i -} + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), +}, { + Name: "invalid JSON (unknown field)", + Error: `*fat.Issuance: unexpected JSON length`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: -1, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), -func invalidIssuance(field string) Issuance { - return setFieldIssuance(field, []int{0}) -} + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + var m map[string]interface{} + if err := json.Unmarshal(e.Content, &m); err != nil { + panic(err) + } + m["newfield"] = 5 + content, err := json.Marshal(m) + if err != nil { + panic(err) + } + e.Content = content + return fat103.Sign(e, issuerSecret) + }(), +}, { + Name: "invalid (supply<-1)", + Error: `invalid "supply": must be positive or -1`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT1, + Supply: -10, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), + Precision: 3, -func omitFieldIssuance(field string) Issuance { - m := validIssuanceEntryContentMap() - delete(m, field) - return issuance(marshal(m)) -} + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), +}, { + Name: "invalid (symbol)", + Error: `invalid "symbol": exceeds 4 characters`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: 100000, + Precision: 3, + Symbol: "testasdfdfsa", + Metadata: json.RawMessage(`"memo"`), -func setFieldIssuance(field string, value interface{}) Issuance { - m := validIssuanceEntryContentMap() - m[field] = value - return issuance(marshal(m)) -} + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), +}, { + Name: "invalid (supply=0)", + Error: `invalid "supply": must be positive or -1`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT1, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), + Precision: 3, -func marshal(v map[string]interface{}) []byte { - data, err := json.Marshal(v) - if err != nil { - panic(err) - } - return data -} + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), +}, { + Name: "invalid (fat1, precision)", + Error: `invalid "precision": not allowed for FAT-1`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT1, + Supply: -1, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), + Precision: 3, -var issuanceMarshalEntryTests = []struct { - Name string - Error string - Issuance -}{{ - Name: "valid", - Issuance: newIssuance(), + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e + }(), }, { - Name: "valid (metadata)", - Issuance: func() Issuance { - i := newIssuance() - i.Metadata = json.RawMessage(`{"memo":"new token"}`) - return i + Name: "invalid (fat0, precision>18 )", + Error: `invalid "precision": out of range [0-18]`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: -1, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), + Precision: 30, + + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e }(), }, { - Name: "invalid data", - Error: `json: error calling MarshalJSON for type *fat.Issuance: invalid "type": FAT-1000`, - Issuance: func() Issuance { - i := newIssuance() - i.Type = 1000 - return i + Name: "invalid (type)", + Error: `invalid "type": FAT-1000000000`, + Entry: func() factom.Entry { + e, err := Issuance{ + Type: 1000000000, + Supply: -1, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), + Precision: 3, + + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e }(), }, { - Name: "invalid metadata JSON", - Error: `json: error calling MarshalJSON for type *fat.Issuance: json: error calling MarshalJSON for type json.RawMessage: invalid character 'a' looking for beginning of object key string`, - Issuance: func() Issuance { - i := newIssuance() - i.Metadata = json.RawMessage("{asdf") - return i + Name: "invalid (issuer key)", + Error: `ExtIDs[1]: unexpected or duplicate RCD Hash`, + IssuerKey: new(factom.ID1Key), + Entry: func() factom.Entry { + e, err := Issuance{ + Type: TypeFAT0, + Supply: 100000, + Precision: 3, + Symbol: "test", + Metadata: json.RawMessage(`"memo"`), + + Entry: factom.Entry{ChainID: &chainID}, + }.Sign(issuerSecret) + if err != nil { + panic(err) + } + return e }(), }} -func TestIssuanceMarshalEntry(t *testing.T) { - for _, test := range issuanceMarshalEntryTests { - t.Run(test.Name, func(t *testing.T) { - assert := assert.New(t) - i := test.Issuance - err := i.MarshalEntry() - if len(test.Error) == 0 { - assert.NoError(err) - } else { - assert.EqualError(err, test.Error) - } - }) +func TestIssuance(t *testing.T) { + for _, test := range issuanceTests { + test := test + t.Run(test.Name, func(t *testing.T) { testIssuance(t, test) }) } } -func newIssuance() Issuance { - return Issuance{ - Type: TypeFAT0, - Supply: 1000000, - Symbol: "TEST", +func testIssuance(t *testing.T, test issuanceTest) { + assert := assert.New(t) + require := require.New(t) + issuerKey := &issuerKey + if test.IssuerKey != nil { + issuerKey = test.IssuerKey + } + i, err := NewIssuance(test.Entry, (*factom.Bytes32)(issuerKey)) + if len(test.Error) == 0 { + require.NoError(err) + assert.NotEqual(Issuance{}, i) + return } + assert.EqualError(err, test.Error) } diff --git a/fat/type.go b/fat/type.go index ccf31aa..8464c91 100644 --- a/fat/type.go +++ b/fat/type.go @@ -27,7 +27,7 @@ import ( "strconv" ) -type Type uint64 +type Type uint const ( TypeFAT0 Type = iota @@ -35,32 +35,33 @@ const ( ) func (t *Type) Set(s string) error { - format := s[0:len(`FAT-`)] - if format != `FAT-` { + return t.UnmarshalText([]byte(s)) +} + +const format = `FAT-` + +func (t *Type) UnmarshalText(text []byte) error { + if string(text[0:len(format)]) != format { return fmt.Errorf("%T: invalid format", t) } - num := s[len(format):] - var err error - if *(*uint64)(t), err = strconv.ParseUint(num, 10, 64); err != nil { - return fmt.Errorf("%T: %v", t, err) + i, err := strconv.ParseUint(string(text[len(format):]), 10, 64) + if err != nil { + return fmt.Errorf("%T: %w", t, err) } + *t = Type(i) return nil } -func (t *Type) UnmarshalJSON(data []byte) error { - if data[0] != '"' || data[len(data)-1] != '"' { - return fmt.Errorf("%T: expected JSON string", t) - } - data = data[1 : len(data)-1] - return t.Set(string(data)) -} - -func (t Type) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("%#v", t.String())), nil +func (t Type) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("%v%v", format, uint(t))), nil } func (t Type) String() string { - return fmt.Sprintf("FAT-%v", uint64(t)) + text, err := t.MarshalText() + if err != nil { + return err.Error() + } + return string(text) } func (t Type) IsValid() bool { diff --git a/fat/fat0/addressamountmap.go b/fat0/addressamountmap.go similarity index 85% rename from fat/fat0/addressamountmap.go rename to fat0/addressamountmap.go index 206924b..b76c983 100644 --- a/fat/fat0/addressamountmap.go +++ b/fat0/addressamountmap.go @@ -26,8 +26,8 @@ import ( "encoding/json" "fmt" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" ) // AddressAmountMap relates a factom.FAAddress to its amount for the Inputs and @@ -37,20 +37,18 @@ type AddressAmountMap map[factom.FAAddress]uint64 // MarshalJSON marshals a list of addresses and amounts used in the inputs or // outputs of a transaction. Addresses with a 0 amount are omitted. func (m AddressAmountMap) MarshalJSON() ([]byte, error) { - if m.Sum() == 0 { + if len(m) == 0 { return nil, fmt.Errorf("empty") } adrStrAmountMap := make(map[string]uint64, len(m)) for adr, amount := range m { - // Omit addresses with 0 amounts. - if amount == 0 { - continue - } adrStrAmountMap[adr.String()] = amount } return json.Marshal(adrStrAmountMap) } +var adrStrLen = len(factom.FAAddress{}.String()) + // UnmarshalJSON unmarshals a list of addresses and amounts used in the inputs // or outputs of a transaction. Duplicate addresses or addresses with a 0 // amount cause an error. @@ -62,16 +60,13 @@ func (m *AddressAmountMap) UnmarshalJSON(data []byte) error { if len(adrStrAmountMap) == 0 { return fmt.Errorf("%T: empty", m) } - adrJSONLen := len(`"":,`) + len(factom.FAAddress{}.String()) - expectedJSONLen := len(`{}`) - len(`,`) + len(adrStrAmountMap)*adrJSONLen + adrJSONLen := len(`"":,`) + adrStrLen + expectedJSONLen := len(`{}`) + len(adrStrAmountMap)*adrJSONLen - len(`,`) *m = make(AddressAmountMap, len(adrStrAmountMap)) var adr factom.FAAddress for adrStr, amount := range adrStrAmountMap { if err := adr.Set(adrStr); err != nil { - return fmt.Errorf("%T: %v", m, err) - } - if amount == 0 { - return fmt.Errorf("%T: invalid amount (0): %v", m, adr) + return fmt.Errorf("%T: %w", m, err) } (*m)[adr] = amount expectedJSONLen += jsonlen.Uint64(amount) @@ -91,7 +86,7 @@ func (m AddressAmountMap) Sum() uint64 { return sum } -func (m AddressAmountMap) NoAddressIntersection(n AddressAmountMap) error { +func (m AddressAmountMap) noAddressIntersection(n AddressAmountMap) error { short, long := m, n if len(short) > len(long) { short, long = long, short diff --git a/fat/fat0/doc.go b/fat0/doc.go similarity index 100% rename from fat/fat0/doc.go rename to fat0/doc.go diff --git a/fat0/transaction.go b/fat0/transaction.go new file mode 100644 index 0000000..3367793 --- /dev/null +++ b/fat0/transaction.go @@ -0,0 +1,134 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package fat0 + +import ( + "encoding/json" + "fmt" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/fat103" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" +) + +const Type = fat.TypeFAT0 + +// Transaction represents a fat0 transaction, which can be a normal account +// transaction or a coinbase transaction depending on the Inputs and the +// RCD/signature pair. +type Transaction struct { + Inputs AddressAmountMap `json:"inputs"` + Outputs AddressAmountMap `json:"outputs"` + + Metadata json.RawMessage `json:"metadata,omitempty"` + + Entry factom.Entry `json:"-"` +} + +func NewTransaction(e factom.Entry, idKey *factom.Bytes32) (Transaction, error) { + var t Transaction + if err := t.UnmarshalJSON(e.Content); err != nil { + return t, err + } + + if t.Inputs.Sum() != t.Outputs.Sum() { + return t, fmt.Errorf("sum(inputs) != sum(outputs)") + } + + var expected map[factom.Bytes32]struct{} + // Coinbase transactions must only have one input. + if t.IsCoinbase() { + if len(t.Inputs) != 1 { + return t, fmt.Errorf("invalid coinbase transaction") + } + + expected = map[factom.Bytes32]struct{}{*idKey: struct{}{}} + } else { + expected = make(map[factom.Bytes32]struct{}, len(t.Inputs)) + for adr := range t.Inputs { + expected[factom.Bytes32(adr)] = struct{}{} + } + } + + if err := fat103.Validate(e, expected); err != nil { + return t, err + } + + t.Entry = e + + return t, nil +} + +func (t *Transaction) UnmarshalJSON(data []byte) error { + data = jsonlen.Compact(data) + var tRaw struct { + Inputs json.RawMessage `json:"inputs"` + Outputs json.RawMessage `json:"outputs"` + Metadata json.RawMessage `json:"metadata,omitempty"` + } + if err := json.Unmarshal(data, &tRaw); err != nil { + return fmt.Errorf("%T: %w", t, err) + } + if err := t.Inputs.UnmarshalJSON(tRaw.Inputs); err != nil { + return fmt.Errorf("%T.Inputs: %w", t, err) + } + if err := t.Outputs.UnmarshalJSON(tRaw.Outputs); err != nil { + return fmt.Errorf("%T.Outputs: %w", t, err) + } + t.Metadata = tRaw.Metadata + + expectedJSONLen := len(`{"inputs":,"outputs":}`) + + len(tRaw.Inputs) + len(tRaw.Outputs) + if tRaw.Metadata != nil { + expectedJSONLen += len(`,"metadata":`) + len(tRaw.Metadata) + } + if expectedJSONLen != len(data) { + return fmt.Errorf("%T: unexpected JSON length", t) + } + + return nil +} + +func (t Transaction) IsCoinbase() bool { + _, ok := t.Inputs[fat.Coinbase()] + return ok +} + +func (t Transaction) String() string { + data, err := json.Marshal(t) + if err != nil { + return err.Error() + } + return string(data) +} + +func (t Transaction) Sign(signingSet ...factom.RCDPrivateKey) (factom.Entry, error) { + e := t.Entry + content, err := json.Marshal(t) + if err != nil { + return e, err + } + e.Content = content + return fat103.Sign(e, signingSet...), nil +} diff --git a/fat/fat0/transaction_test.go b/fat0/transaction_test.go similarity index 84% rename from fat/fat0/transaction_test.go rename to fat0/transaction_test.go index b389909..8fc8769 100644 --- a/fat/fat0/transaction_test.go +++ b/fat0/transaction_test.go @@ -28,9 +28,9 @@ import ( "fmt" "testing" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" "github.com/Factom-Asset-Tokens/fatd/fat" - . "github.com/Factom-Asset-Tokens/fatd/fat/fat0" + . "github.com/Factom-Asset-Tokens/fatd/fat0" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,13 +40,13 @@ var transactionTests = []struct { Error string IssuerKey factom.ID1Key Coinbase bool - Tx Transaction + Tx *Transaction }{{ Name: "valid", Tx: validTx(), }, { Name: "valid (single outputs)", - Tx: func() Transaction { + Tx: func() *Transaction { out := outputs() out[outputAddresses[0].FAAddress().String()] += out[outputAddresses[1].FAAddress().String()] + @@ -82,14 +82,6 @@ var transactionTests = []struct { Name: "invalid JSON (invalid outputs type)", Error: "*fat0.Transaction.Outputs: json: cannot unmarshal array into Go value of type map[string]uint64", Tx: invalidField("outputs"), -}, { - Name: "invalid JSON (invalid inputs, zero amount)", - Error: "*fat0.Transaction.Inputs: *fat0.AddressAmountMap: invalid amount (0): ", - Tx: func() Transaction { - in := inputs() - in[inputAddresses[0].FAAddress().String()] = 0 - return setFieldTransaction("inputs", in) - }(), }, { Name: "invalid JSON (invalid inputs, duplicate)", Error: "*fat0.Transaction.Inputs: *fat0.AddressAmountMap: unexpected JSON length", @@ -125,7 +117,7 @@ var transactionTests = []struct { }, { Name: "invalid data (sum mismatch)", Error: "*fat0.Transaction: sum(inputs) != sum(outputs)", - Tx: func() Transaction { + Tx: func() *Transaction { out := outputs() out[outputAddresses[0].FAAddress().String()]++ return setFieldTransaction("outputs", out) @@ -134,7 +126,7 @@ var transactionTests = []struct { Name: "invalid data (coinbase)", Error: "*fat0.Transaction: invalid coinbase transaction", IssuerKey: issuerKey, - Tx: func() Transaction { + Tx: func() *Transaction { m := validCoinbaseTxEntryContentMap() in := coinbaseInputs() in[inputAddresses[0].FAAddress().String()] = 1 @@ -144,36 +136,10 @@ var transactionTests = []struct { m["outputs"] = out return transaction(marshal(m)) }(), -}, { - Name: "invalid data (coinbase, coinbase outputs)", - Error: "*fat0.Transaction: duplicate address: ", - IssuerKey: issuerKey, - Tx: func() Transaction { - m := validCoinbaseTxEntryContentMap() - in := coinbaseInputs() - out := coinbaseOutputs() - in[fat.Coinbase().String()]++ - out[fat.Coinbase().String()]++ - m["inputs"] = in - m["outputs"] = out - return transaction(marshal(m)) - }(), -}, { - Name: "invalid data (inputs outputs overlap)", - Error: "*fat0.Transaction: duplicate address: ", - Tx: func() Transaction { - m := validTxEntryContentMap() - in := inputs() - in[outputAddresses[0].FAAddress().String()] = - in[inputAddresses[0].FAAddress().String()] - delete(in, inputAddresses[0].FAAddress().String()) - m["inputs"] = in - return transaction(marshal(m)) - }(), }, { Name: "invalid ExtIDs (timestamp)", Error: "timestamp salt expired", - Tx: func() Transaction { + Tx: func() *Transaction { t := validTx() t.ExtIDs[0] = factom.Bytes("100") return t @@ -181,7 +147,7 @@ var transactionTests = []struct { }, { Name: "invalid ExtIDs (length)", Error: "invalid number of ExtIDs", - Tx: func() Transaction { + Tx: func() *Transaction { t := validTx() t.ExtIDs = append(t.ExtIDs, factom.Bytes{}) return t @@ -193,7 +159,7 @@ var transactionTests = []struct { }, { Name: "RCD input mismatch", Error: "invalid RCDs", - Tx: func() Transaction { + Tx: func() *Transaction { t := validTx() adrs := twoAddresses() t.Sign(adrs[0], adrs[1]) @@ -207,7 +173,7 @@ func TestTransaction(t *testing.T) { assert := assert.New(t) tx := test.Tx key := test.IssuerKey - err := tx.Valid(&key) + err := tx.Validate(&key) if len(test.Error) != 0 { assert.Contains(err.Error(), test.Error) return @@ -233,31 +199,31 @@ var ( coinbaseInputAmounts = []uint64{110} coinbaseOutputAmounts = []uint64{90, 20} - tokenChainID = fat.ChainID("test", *identityChainID) + tokenChainID = fat.ComputeChainID("test", identityChainID) identityChainID = factom.NewBytes32(validIdentityChainID()) ) // Transactions -func omitFieldTransaction(field string) Transaction { +func omitFieldTransaction(field string) *Transaction { m := validTxEntryContentMap() delete(m, field) return transaction(marshal(m)) } -func setFieldTransaction(field string, value interface{}) Transaction { +func setFieldTransaction(field string, value interface{}) *Transaction { m := validTxEntryContentMap() m[field] = value return transaction(marshal(m)) } -func validTx() Transaction { +func validTx() *Transaction { return transaction(marshal(validTxEntryContentMap())) } -func coinbaseTx() Transaction { +func coinbaseTx() *Transaction { t := transaction(marshal(validCoinbaseTxEntryContentMap())) t.Sign(issuerSecret) return t } -func transaction(content factom.Bytes) Transaction { +func transaction(content factom.Bytes) *Transaction { e := factom.Entry{ ChainID: &tokenChainID, Content: content, @@ -270,7 +236,7 @@ func transaction(content factom.Bytes) Transaction { t.Sign(adrs...) return t } -func invalidField(field string) Transaction { +func invalidField(field string) *Transaction { m := validTxEntryContentMap() m[field] = []int{0} return transaction(marshal(m)) @@ -294,21 +260,21 @@ func validCoinbaseTxEntryContentMap() map[string]interface{} { // inputs/outputs func inputs() map[string]uint64 { - inputs := map[string]uint64{} + inputs := make(map[string]uint64) for i := range inputAddresses { inputs[inputAddresses[i].FAAddress().String()] = inputAmounts[i] } return inputs } func outputs() map[string]uint64 { - outputs := map[string]uint64{} + outputs := make(map[string]uint64) for i := range outputAddresses { outputs[outputAddresses[i].FAAddress().String()] = outputAmounts[i] } return outputs } func coinbaseInputs() map[string]uint64 { - inputs := map[string]uint64{} + inputs := make(map[string]uint64) for i := range coinbaseInputAddresses { inputs[coinbaseInputAddresses[i].FAAddress().String()] = coinbaseInputAmounts[i] @@ -316,7 +282,7 @@ func coinbaseInputs() map[string]uint64 { return inputs } func coinbaseOutputs() map[string]uint64 { - outputs := map[string]uint64{} + outputs := make(map[string]uint64) for i := range coinbaseOutputAddresses { outputs[coinbaseOutputAddresses[i].FAAddress().String()] = coinbaseOutputAmounts[i] @@ -327,20 +293,20 @@ func coinbaseOutputs() map[string]uint64 { var transactionMarshalEntryTests = []struct { Name string Error string - Tx Transaction + Tx *Transaction }{{ Name: "valid", Tx: newTransaction(), }, { Name: "valid (omit zero balances)", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Inputs[fat.Coinbase()] = 0 return t }(), }, { Name: "valid (metadata)", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Metadata = json.RawMessage(`{"memo":"Rent for Dec 2018"}`) return t @@ -348,7 +314,7 @@ var transactionMarshalEntryTests = []struct { }, { Name: "invalid data", Error: "json: error calling MarshalJSON for type *fat0.Transaction: sum(inputs) != sum(outputs)", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Inputs[inputAddresses[0].FAAddress()]++ return t @@ -356,7 +322,7 @@ var transactionMarshalEntryTests = []struct { }, { Name: "invalid data", Error: "json: error calling MarshalJSON for type *fat0.Transaction: json: error calling MarshalJSON for type fat0.AddressAmountMap: empty", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Inputs = make(AddressAmountMap) t.Outputs = make(AddressAmountMap) @@ -365,7 +331,7 @@ var transactionMarshalEntryTests = []struct { }, { Name: "invalid metadata JSON", Error: "json: error calling MarshalJSON for type *fat0.Transaction: json: error calling MarshalJSON for type json.RawMessage: invalid character 'a' looking for beginning of object key string", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Metadata = json.RawMessage("{asdf") return t @@ -387,8 +353,8 @@ func TestTransactionMarshalEntry(t *testing.T) { } } -func newTransaction() Transaction { - return Transaction{ +func newTransaction() *Transaction { + return &Transaction{ Inputs: inputAddressAmountMap(), Outputs: outputAddressAmountMap(), } @@ -402,7 +368,7 @@ func outputAddressAmountMap() AddressAmountMap { func addressAmountMap(aas map[string]uint64) AddressAmountMap { m := make(AddressAmountMap) for addressStr, amount := range aas { - a := factom.FAAddress{} + var a factom.FAAddress if err := json.Unmarshal( []byte(fmt.Sprintf("%#v", addressStr)), &a); err != nil { panic(err) diff --git a/fat/fat1/addressnftokensmap.go b/fat1/addressnftokensmap.go similarity index 53% rename from fat/fat1/addressnftokensmap.go rename to fat1/addressnftokensmap.go index 2794a3c..9ef16c9 100644 --- a/fat/fat1/addressnftokensmap.go +++ b/fat1/addressnftokensmap.go @@ -26,60 +26,49 @@ import ( "encoding/json" "fmt" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" ) // AddressTokenMap relates the RCDHash of an address to its NFTokenIDs. type AddressNFTokensMap map[factom.FAAddress]NFTokens func (m AddressNFTokensMap) MarshalJSON() ([]byte, error) { - if m.NumNFTokenIDs() == 0 { - return nil, fmt.Errorf("empty") - } - if err := m.NoInternalNFTokensIntersection(); err != nil { - return nil, err - } adrStrTknsMap := make(map[string]NFTokens, len(m)) for adr, tkns := range m { - // Omit addresses with empty NFTokens. - if len(tkns) == 0 { - continue - } adrStrTknsMap[adr.String()] = tkns } return json.Marshal(adrStrTknsMap) } +var adrStrLen = len(factom.FAAddress{}.String()) + func (m *AddressNFTokensMap) UnmarshalJSON(data []byte) error { var adrStrDataMap map[string]json.RawMessage if err := json.Unmarshal(data, &adrStrDataMap); err != nil { - return fmt.Errorf("%T: %v", m, err) + return fmt.Errorf("%T: %w", m, err) } if len(adrStrDataMap) == 0 { return fmt.Errorf("%T: empty", m) } - adrJSONLen := len(`"":,`) + len(factom.FAAddress{}.String()) + adrJSONLen := len(`"":,`) + adrStrLen expectedJSONLen := len(`{}`) - len(`,`) + len(adrStrDataMap)*adrJSONLen *m = make(AddressNFTokensMap, len(adrStrDataMap)) - var adr factom.FAAddress - var tkns NFTokens var numTkns int for adrStr, data := range adrStrDataMap { - if err := adr.Set(adrStr); err != nil { - return fmt.Errorf("%T: %#v: %v", m, adrStr, err) + adr, err := factom.NewFAAddress(adrStr) + if err != nil { + return fmt.Errorf("%T: %#v: %w", m, adrStr, err) } + var tkns NFTokens if err := tkns.UnmarshalJSON(data); err != nil { - return fmt.Errorf("%T: %v: %v", m, err, adr) + return fmt.Errorf("%T: %v: %w", m, err, adr) } numTkns += len(tkns) if numTkns > maxCapacity { return fmt.Errorf("%T(len:%v): %T(len:%v): %v", m, numTkns-len(tkns), tkns, len(tkns), ErrorCapacity) } - if err := m.NoNFTokensIntersection(tkns); err != nil { - return fmt.Errorf("%T: %v and %v", m, err, adr) - } (*m)[adr] = tkns expectedJSONLen += len(jsonlen.Compact(data)) } @@ -89,81 +78,40 @@ func (m *AddressNFTokensMap) UnmarshalJSON(data []byte) error { return nil } -func (m AddressNFTokensMap) NoNFTokensIntersection(newTkns NFTokens) error { - for adr, existingTkns := range m { - if err := existingTkns.NoIntersection(newTkns); err != nil { - return fmt.Errorf("%v: %v", err, adr) - } - } - return nil -} - -func (m AddressNFTokensMap) NoAddressIntersection(n AddressNFTokensMap) error { - short, long := m, n - if len(short) > len(long) { - short, long = long, short - } - for rcdHash, tkns := range short { - if len(tkns) == 0 { - continue - } - if tkns := long[rcdHash]; len(tkns) != 0 { - return fmt.Errorf("duplicate address: %v", rcdHash) - } - } - return nil -} - -func (m AddressNFTokensMap) NFTokenIDsConserved(n AddressNFTokensMap) error { - numTknIDs := m.NumNFTokenIDs() - if numTknIDs != n.NumNFTokenIDs() { +func (m AddressNFTokensMap) nfTokenIDsConserved(n AddressNFTokensMap) error { + if m.Sum() != n.Sum() { return fmt.Errorf("number of NFTokenIDs differ") } - allTkns := m.AllNFTokens() + allTkns, err := m.AllNFTokens() + if err != nil { + return err + } for _, tkns := range n { - for tknID := range tkns { - if _, ok := allTkns[tknID]; !ok { - return fmt.Errorf("missing NFTokenID: %v", tknID) - } + if err := allTkns.ContainsAll(tkns); err != nil { + return err } } return nil } -func (m AddressNFTokensMap) AllNFTokens() NFTokens { - allTkns := make(NFTokens, len(m)) +func (m AddressNFTokensMap) AllNFTokens() (NFTokens, error) { + allTkns := make(NFTokens, m.Sum()) for _, tkns := range m { for tknID := range tkns { - allTkns[tknID] = struct{}{} + if err := allTkns.set(tknID); err != nil { + return nil, err + } } } - return allTkns + return allTkns, nil } -func (m AddressNFTokensMap) NumNFTokenIDs() int { - var numTknIDs int +func (m AddressNFTokensMap) Sum() uint64 { + var sum uint64 for _, tkns := range m { - numTknIDs += len(tkns) + sum += uint64(len(tkns)) } - return numTknIDs -} - -func (m AddressNFTokensMap) NoInternalNFTokensIntersection() error { - allTkns := make(NFTokens, m.NumNFTokenIDs()) - for rcdHash, tkns := range m { - if err := allTkns.Append(tkns); err != nil { - // We found an intersection. To identify the other - // RCDHash that owns tknID, we temporarily remove - // rcdHash from m and restore it after we return. - tknID := NFTokenID(err.(ErrorNFTokenIDIntersection)) - delete(m, rcdHash) - otherRCDHash := m.Owner(tknID) - m[rcdHash] = tkns - return fmt.Errorf("%v: %v and %v", err, rcdHash, otherRCDHash) - - } - } - return nil + return sum } func (m AddressNFTokensMap) Owner(tknID NFTokenID) factom.FAAddress { diff --git a/fat/fat1/addressnftokensmap_test.go b/fat1/addressnftokensmap_test.go similarity index 98% rename from fat/fat1/addressnftokensmap_test.go rename to fat1/addressnftokensmap_test.go index 4fae647..8bda966 100644 --- a/fat/fat1/addressnftokensmap_test.go +++ b/fat1/addressnftokensmap_test.go @@ -26,7 +26,7 @@ import ( "encoding/json" "testing" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" "github.com/stretchr/testify/assert" ) @@ -153,7 +153,7 @@ func TestAddressNFTokensMapUnmarshal(t *testing.T) { for _, test := range AddressNFTokensMapUnmarshalTests { t.Run(test.Name, func(t *testing.T) { assert := assert.New(t) - adrNFTkns := AddressNFTokensMap{} + var adrNFTkns AddressNFTokensMap err := adrNFTkns.UnmarshalJSON([]byte(test.JSON)) if len(test.Error) > 0 { assert.Contains(err.Error(), test.Error) diff --git a/fat/fat1/disjointnftokens_test.go b/fat1/disjointnftokens_test.go similarity index 100% rename from fat/fat1/disjointnftokens_test.go rename to fat1/disjointnftokens_test.go diff --git a/fat/fat1/nftokenid.go b/fat1/nftokenid.go similarity index 85% rename from fat/fat1/nftokenid.go rename to fat1/nftokenid.go index f319b0a..1da8c1a 100644 --- a/fat/fat1/nftokenid.go +++ b/fat1/nftokenid.go @@ -25,15 +25,14 @@ package fat1 import ( "fmt" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" ) // NFTokenID is a Non-Fungible Token ID. type NFTokenID uint64 -// Set id in nfTkns and return an error if it is already set. -func (id NFTokenID) Set(tkns NFTokens) error { - if len(tkns)+id.Len() > maxCapacity { +func (id NFTokenID) setInto(tkns NFTokens) error { + if len(tkns)+1 > maxCapacity { return fmt.Errorf("%T(len:%v): %T(%v): %v", tkns, len(tkns), id, id, ErrorCapacity) } @@ -44,11 +43,6 @@ func (id NFTokenID) Set(tkns NFTokens) error { return nil } -// Len returns 1. -func (id NFTokenID) Len() int { - return 1 -} - func (id NFTokenID) jsonLen() int { return jsonlen.Uint64(uint64(id)) } diff --git a/fat/fat1/nftokenidrange.go b/fat1/nftokenidrange.go similarity index 62% rename from fat/fat1/nftokenidrange.go rename to fat1/nftokenidrange.go index 124c27e..50440a4 100644 --- a/fat/fat1/nftokenidrange.go +++ b/fat1/nftokenidrange.go @@ -25,17 +25,18 @@ package fat1 import ( "encoding/json" "fmt" + "strconv" + "strings" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" ) -// NFTokenIDRange represents a contiguous range of NFTokenIDs. -type NFTokenIDRange struct { +type nfTokenIDRange struct { Min NFTokenID `json:"min"` Max NFTokenID `json:"max"` } -func NewNFTokenIDRange(minMax ...NFTokenID) NFTokenIDRange { +func newNFTokenIDRange(minMax ...NFTokenID) nfTokenIDRange { var min, max NFTokenID if len(minMax) >= 2 { min, max = minMax[0], minMax[1] @@ -45,43 +46,53 @@ func NewNFTokenIDRange(minMax ...NFTokenID) NFTokenIDRange { } else if len(minMax) == 1 { min, max = minMax[0], minMax[0] } - return NFTokenIDRange{Min: min, Max: max} + return nfTokenIDRange{Min: min, Max: max} } -func (idRange NFTokenIDRange) IsJSONEfficient() bool { +func (idRange nfTokenIDRange) IsJSONEfficient() bool { var expandedLen int for id := idRange.Min; id <= idRange.Max; id++ { expandedLen += id.jsonLen() + len(`,`) } return idRange.jsonLen() <= expandedLen } +func (idRange nfTokenIDRange) jsonLen() int { + return len(`{"min":`) + + idRange.Min.jsonLen() + + len(`,"max":`) + + idRange.Max.jsonLen() + + len(`}`) +} -func (idRange NFTokenIDRange) IsStringEfficient() bool { +func (idRange nfTokenIDRange) IsStringEfficient() bool { var expandedLen int for id := idRange.Min; id <= idRange.Max; id++ { expandedLen += id.jsonLen() + len(`,`) } return idRange.strLen() <= expandedLen } +func (idRange nfTokenIDRange) strLen() int { + return idRange.Min.jsonLen() + len(`-`) + idRange.Max.jsonLen() +} -func (idRange NFTokenIDRange) Len() int { +func (idRange nfTokenIDRange) Len() int { return int(idRange.Max - idRange.Min + 1) } -func (idRange NFTokenIDRange) Set(tkns NFTokens) error { +func (idRange nfTokenIDRange) setInto(tkns NFTokens) error { if len(tkns)+idRange.Len() > maxCapacity { return fmt.Errorf("%T(len:%v): %T(%v): %v", tkns, len(tkns), idRange, idRange, ErrorCapacity) } for id := idRange.Min; id <= idRange.Max; id++ { - if err := id.Set(tkns); err != nil { + if err := id.setInto(tkns); err != nil { return err } } return nil } -func (idRange NFTokenIDRange) Valid() error { +func (idRange nfTokenIDRange) Valid() error { if idRange.Len() > maxCapacity { return ErrorCapacity } @@ -91,9 +102,7 @@ func (idRange NFTokenIDRange) Valid() error { return nil } -type nfTokenIDRange NFTokenIDRange - -func (idRange NFTokenIDRange) String() string { +func (idRange nfTokenIDRange) String() string { if !idRange.IsStringEfficient() { ids := idRange.Slice() return fmt.Sprintf("%v", ids) @@ -101,7 +110,7 @@ func (idRange NFTokenIDRange) String() string { return fmt.Sprintf("%v-%v", idRange.Min, idRange.Max) } -func (idRange NFTokenIDRange) MarshalJSON() ([]byte, error) { +func (idRange nfTokenIDRange) MarshalJSON() ([]byte, error) { if err := idRange.Valid(); err != nil { return nil, err } @@ -109,11 +118,22 @@ func (idRange NFTokenIDRange) MarshalJSON() ([]byte, error) { ids := idRange.Slice() return json.Marshal(ids) } - return json.Marshal(nfTokenIDRange(idRange)) + type n nfTokenIDRange + return json.Marshal(n(idRange)) +} +func (idRange nfTokenIDRange) MarshalText() ([]byte, error) { + if err := idRange.Valid(); err != nil { + return nil, err + } + if !idRange.IsStringEfficient() { + ids := idRange.Slice() + return json.Marshal(ids) + } + return []byte(fmt.Sprintf("%v-%v", idRange.Min, idRange.Max)), nil } // Slice returns a sorted slice of tkns' NFTokenIDs. -func (idRange NFTokenIDRange) Slice() []NFTokenID { +func (idRange nfTokenIDRange) Slice() []NFTokenID { ids := make([]NFTokenID, idRange.Len()) for i := range ids { ids[i] = NFTokenID(i) + idRange.Min @@ -121,26 +141,33 @@ func (idRange NFTokenIDRange) Slice() []NFTokenID { return ids } -func (idRange *NFTokenIDRange) UnmarshalJSON(data []byte) error { - if err := json.Unmarshal(data, (*nfTokenIDRange)(idRange)); err != nil { - return fmt.Errorf("%T: %v", idRange, err) +func (idRange *nfTokenIDRange) UnmarshalJSON(data []byte) error { + type n nfTokenIDRange + if err := json.Unmarshal(data, (*n)(idRange)); err != nil { + return fmt.Errorf("%T: %w", idRange, err) } if err := idRange.Valid(); err != nil { - return fmt.Errorf("%T: %v", idRange, err) + return fmt.Errorf("%T: %w", idRange, err) } if len(jsonlen.Compact(data)) != idRange.jsonLen() { return fmt.Errorf("%T: unexpected JSON length", idRange) } return nil } -func (idRange NFTokenIDRange) jsonLen() int { - return len(`{"min":`) + - idRange.Min.jsonLen() + - len(`,"max":`) + - idRange.Max.jsonLen() + - len(`}`) -} -func (idRange NFTokenIDRange) strLen() int { - return idRange.Min.jsonLen() + len(`-`) + idRange.Max.jsonLen() +func (idRange *nfTokenIDRange) UnmarshalText(text []byte) error { + texts := strings.SplitN(string(text), "-", 2) + if len(texts) != 2 { + return fmt.Errorf("invalid range format") + } + min, err := strconv.ParseUint(texts[0], 10, 64) + if err != nil { + return fmt.Errorf("could not parse min: %w", err) + } + max, err := strconv.ParseUint(texts[1], 10, 64) + if err != nil { + return fmt.Errorf("could not parse max: %w", err) + } + idRange.Min, idRange.Max = NFTokenID(min), NFTokenID(max) + return nil } diff --git a/fat/fat1/nftokenmetadata.go b/fat1/nftokenmetadata.go similarity index 89% rename from fat/fat1/nftokenmetadata.go rename to fat1/nftokenmetadata.go index 5302f48..9df25f1 100644 --- a/fat/fat1/nftokenmetadata.go +++ b/fat1/nftokenmetadata.go @@ -26,12 +26,12 @@ import ( "encoding/json" "fmt" - "github.com/Factom-Asset-Tokens/fatd/fat/jsonlen" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" ) type NFTokenIDMetadataMap map[NFTokenID]json.RawMessage -type NFTokenMetadata struct { +type nfTokenMetadata struct { Tokens NFTokens `json:"ids"` Metadata json.RawMessage `json:"metadata,omitempty"` } @@ -42,7 +42,7 @@ func (m *NFTokenIDMetadataMap) UnmarshalJSON(data []byte) error { Metadata json.RawMessage `json:"metadata"` } if err := json.Unmarshal(data, &tknMs); err != nil { - return fmt.Errorf("%T: %v", m, err) + return fmt.Errorf("%T: %w", m, err) } *m = make(NFTokenIDMetadataMap, len(tknMs)) var expectedJSONLen int @@ -55,7 +55,7 @@ func (m *NFTokenIDMetadataMap) UnmarshalJSON(data []byte) error { } var tkns NFTokens if err := tkns.UnmarshalJSON(tknM.Tokens); err != nil { - return fmt.Errorf("%T: %v", m, err) + return fmt.Errorf("%T: %w", m, err) } metadata := jsonlen.Compact(tknM.Metadata) expectedJSONLen += len(metadata) + len(jsonlen.Compact(tknM.Tokens)) @@ -84,13 +84,13 @@ func (m NFTokenIDMetadataMap) MarshalJSON() ([]byte, error) { tkns = make(NFTokens) metadataNFTokens[string(metadata)] = tkns } - if err := tknID.Set(tkns); err != nil { + if err := tkns.set(tknID); err != nil { return nil, err } } var i int - tknMs := make([]NFTokenMetadata, len(metadataNFTokens)) + tknMs := make([]nfTokenMetadata, len(metadataNFTokens)) for metadata, tkns := range metadataNFTokens { tknMs[i].Tokens = tkns tknMs[i].Metadata = json.RawMessage(metadata) @@ -100,7 +100,7 @@ func (m NFTokenIDMetadataMap) MarshalJSON() ([]byte, error) { return json.Marshal(tknMs) } -func (m NFTokenIDMetadataMap) IsSubsetOf(tkns NFTokens) error { +func (m NFTokenIDMetadataMap) isSubsetOf(tkns NFTokens) error { if len(m) > len(tkns) { return fmt.Errorf("too many NFTokenIDs") } @@ -113,7 +113,7 @@ func (m NFTokenIDMetadataMap) IsSubsetOf(tkns NFTokens) error { return nil } -func (m NFTokenIDMetadataMap) Set(md NFTokenMetadata) { +func (m NFTokenIDMetadataMap) set(md nfTokenMetadata) { for tknID := range md.Tokens { m[tknID] = md.Metadata } diff --git a/fat/fat1/nftokens.go b/fat1/nftokens.go similarity index 51% rename from fat/fat1/nftokens.go rename to fat1/nftokens.go index c10894c..2aa9129 100644 --- a/fat/fat1/nftokens.go +++ b/fat1/nftokens.go @@ -26,6 +26,8 @@ import ( "encoding/json" "fmt" "sort" + "strconv" + "strings" ) const MaxCapacity = 4e5 @@ -38,76 +40,41 @@ var ErrorCapacity = fmt.Errorf("NFTokenID max capacity (%v) exceeded", maxCapaci // guarantee uniqueness of NFTokenIDs. type NFTokens map[NFTokenID]struct{} -// NFTokensSetter is an interface implemented by types that can set the -// NFTokenIDs they represent in a given NFTokens. -type NFTokensSetter interface { - // Set the NFTokenIDs in tkns. Return an error if tkns already - // contains one of the NFTokenIDs. - Set(tkns NFTokens) error - // Len returns number of NFTokenIDs that will be set. - Len() int +type nfTokensSetter interface { + setInto(tkns NFTokens) error } -// NewNFTokens returns an NFTokens initialized with ids. If ids contains any -// duplicate NFTokenIDs. -func NewNFTokens(ids ...NFTokensSetter) (NFTokens, error) { - var capacity int - for _, id := range ids { - capacity += id.Len() - } - tkns := make(NFTokens, capacity) - if err := tkns.Set(ids...); err != nil { - return nil, err - } - return tkns, nil -} - -func (tkns NFTokens) Append(newTkns NFTokens) error { - if len(tkns)+len(newTkns) > maxCapacity { +func (tkns NFTokens) setInto(to NFTokens) error { + if len(tkns)+len(to) > maxCapacity { return ErrorCapacity } - if err := tkns.NoIntersection(newTkns); err != nil { - return err - } - for tknID := range newTkns { - tkns[tknID] = struct{}{} + for tknID := range tkns { + if _, ok := to[tknID]; ok { + return errorNFTokenIDIntersection(tknID) + } + to[tknID] = struct{}{} } return nil } -// Set all ids in tkns. Return an error if ids contains any duplicate or -// previously set NFTokenIDs. -func (tkns NFTokens) Set(ids ...NFTokensSetter) error { +func (tkns NFTokens) set(ids ...nfTokensSetter) error { for _, id := range ids { - if err := id.Set(tkns); err != nil { + if err := id.setInto(tkns); err != nil { return err } } return nil } -type ErrorNFTokenIDIntersection NFTokenID +type errorNFTokenIDIntersection NFTokenID -func (id ErrorNFTokenIDIntersection) Error() string { +func (id errorNFTokenIDIntersection) Error() string { return fmt.Sprintf("duplicate NFTokenID: %v", NFTokenID(id)) } -func (tkns NFTokens) NoIntersection(tknsCmp NFTokens) error { - small, large := tkns, tknsCmp - if len(small) > len(large) { - small, large = large, small - } - for tknID := range small { - if _, ok := large[tknID]; ok { - return ErrorNFTokenIDIntersection(tknID) - } - } - return nil -} - -type ErrorMissingNFTokenID NFTokenID +type errorMissingNFTokenID NFTokenID -func (id ErrorMissingNFTokenID) Error() string { +func (id errorMissingNFTokenID) Error() string { return fmt.Sprintf("missing NFTokenID: %v", NFTokenID(id)) } @@ -117,7 +84,7 @@ func (tkns NFTokens) ContainsAll(tknsSub NFTokens) error { } for tknID := range tknsSub { if _, ok := tkns[tknID]; !ok { - return ErrorMissingNFTokenID(tknID) + return errorMissingNFTokenID(tknID) } } return nil @@ -137,46 +104,13 @@ func (tkns NFTokens) Slice() []NFTokenID { return tknsAry } -// MarshalJSON implements the json.Marshaler interface. MarshalJSON will always -// produce the most efficient representation of tkns using NFTokenIDRanges -// over individual NFTokenIDs where appropriate. MarshalJSON will return an -// error if tkns is empty. func (tkns NFTokens) MarshalJSON() ([]byte, error) { if len(tkns) == 0 { - return nil, fmt.Errorf("%T: empty", tkns) + return []byte(`[]`), nil } + tknsAry := tkns.compress(true) - tknsFullAry := tkns.Slice() - - // Compress the tknsAry by replacing contiguous id ranges with an - // NFTokenIDRange. - tknsAry := make([]interface{}, len(tkns)) - idRange := NewNFTokenIDRange(tknsFullAry[0]) - i := 0 - for _, id := range append(tknsFullAry[1:], 0) { - // If this id is contiguous with idRange, expand the range to - // include this id and check the next id. - if id == idRange.Max+1 { - idRange.Max = id - continue - } - // Otherwise, the id is not contiguous with the range, so - // append the idRange and set up a new idRange to start at id. - - // Use the most efficient JSON representation for the idRange. - if idRange.IsJSONEfficient() { - tknsAry[i] = idRange - i++ - } else { - for id := idRange.Min; id <= idRange.Max; id++ { - tknsAry[i] = id - i++ - } - } - idRange = NewNFTokenIDRange(id) - } - - return json.Marshal(tknsAry[:i]) + return json.Marshal(tknsAry) } func (tkns NFTokens) String() string { @@ -184,13 +118,26 @@ func (tkns NFTokens) String() string { return "[]" } - tknsFullAry := tkns.Slice() + tknsAry := tkns.compress(false) + str := "[" + for _, tkn := range tknsAry { + str += fmt.Sprintf("%v,", tkn) + } + return str[:len(str)-1] + "]" +} + +func (tkns NFTokens) compress(forJSON bool) []interface{} { + tknsFullAry := tkns.Slice() // Compress the tknsAry by replacing contiguous id ranges with an - // NFTokenIDRange. + // nfTokenIDRange. tknsAry := make([]interface{}, len(tkns)) - idRange := NewNFTokenIDRange(tknsFullAry[0]) - i := 0 + firstID := tknsFullAry[0] + idRange := nfTokenIDRange{Min: firstID, Max: firstID} + i := 0 // index into tknsAry + // The first id will be placed when the idRange is inserted either as a + // single NFTokenID or as a range. The last id does not get included, + // so append 0. for _, id := range append(tknsFullAry[1:], 0) { // If this id is contiguous with idRange, expand the range to // include this id and check the next id. @@ -201,8 +148,10 @@ func (tkns NFTokens) String() string { // Otherwise, the id is not contiguous with the range, so // append the idRange and set up a new idRange to start at id. - // Use the most efficient JSON representation for the idRange. - if idRange.IsStringEfficient() { + // Use the most efficient JSON or String representation for the + // idRange. + if (forJSON && idRange.IsJSONEfficient()) || + (!forJSON && idRange.IsStringEfficient()) { tknsAry[i] = idRange i++ } else { @@ -211,43 +160,74 @@ func (tkns NFTokens) String() string { i++ } } - idRange = NewNFTokenIDRange(id) + idRange = nfTokenIDRange{Min: id, Max: id} } - str := "[" - for _, tkn := range tknsAry[:i] { - str += fmt.Sprintf("%v,", tkn) - } - return str[:len(str)-1] + "]" + return tknsAry[:i] } func (tkns *NFTokens) UnmarshalJSON(data []byte) error { var tknsJSONAry []json.RawMessage if err := json.Unmarshal(data, &tknsJSONAry); err != nil { - return fmt.Errorf("%T: %v", tkns, err) + return fmt.Errorf("%T: %w", tkns, err) } - if len(tknsJSONAry) == 0 { - return fmt.Errorf("%T: empty", tkns) + if *tkns == nil { + *tkns = make(NFTokens, len(tknsJSONAry)) } - *tkns = make(NFTokens, len(tknsJSONAry)) for _, data := range tknsJSONAry { - var ids NFTokensSetter + var ids nfTokensSetter if data[0] == '{' { - var idRange NFTokenIDRange + var idRange nfTokenIDRange if err := idRange.UnmarshalJSON(data); err != nil { - return fmt.Errorf("%T: %v", tkns, err) + return fmt.Errorf("%T: %w", tkns, err) } ids = idRange } else { var id NFTokenID if err := json.Unmarshal(data, &id); err != nil { - return fmt.Errorf("%T: %v", tkns, err) + return fmt.Errorf("%T: %w", tkns, err) } ids = id } - if err := ids.Set(*tkns); err != nil { - return fmt.Errorf("%T: %v", tkns, err) + if err := tkns.set(ids); err != nil { + return fmt.Errorf("%T: %w", tkns, err) } } return nil } + +func (tkns *NFTokens) UnmarshalText(text []byte) error { + if len(text) < 2 || text[0] != '[' || text[len(text)-1] != ']' { + return fmt.Errorf("invalid format") + } + text = []byte(strings.Trim(string(text), "[]")) + + texts := strings.Split(string(text), ",") + + if *tkns == nil { + *tkns = make(NFTokens, len(texts)) + } + + for _, text := range texts { + var ids nfTokensSetter + var idRange nfTokenIDRange + err := idRange.UnmarshalText([]byte(text)) + if err == nil { + if err := idRange.Valid(); err != nil { + return err + } + ids = idRange + } else { + + tknID, err := strconv.ParseInt(texts[0], 10, 64) + if err != nil { + return err + } + ids = NFTokenID(tknID) + } + if err := tkns.set(ids); err != nil { + return err + } + } + return nil +} diff --git a/fat/fat1/nftokens_test.go b/fat1/nftokens_test.go similarity index 100% rename from fat/fat1/nftokens_test.go rename to fat1/nftokens_test.go diff --git a/fat1/transaction.go b/fat1/transaction.go new file mode 100644 index 0000000..95c134a --- /dev/null +++ b/fat1/transaction.go @@ -0,0 +1,155 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package fat1 + +import ( + "encoding/json" + "fmt" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/fat103" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" +) + +const Type = fat.TypeFAT1 + +// Transaction represents a fat1 transaction, which can be a normal account +// transaction or a coinbase transaction depending on the Inputs and the +// RCD/signature pair. +type Transaction struct { + Inputs AddressNFTokensMap `json:"inputs"` + Outputs AddressNFTokensMap `json:"outputs"` + + TokenMetadata NFTokenIDMetadataMap `json:"tokenmetadata,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` + + Entry factom.Entry `json:"-"` +} + +func NewTransaction(e factom.Entry, idKey *factom.Bytes32) (Transaction, error) { + var t Transaction + if err := t.UnmarshalJSON(e.Content); err != nil { + return t, err + } + + if err := t.Inputs.nfTokenIDsConserved(t.Outputs); err != nil { + return t, fmt.Errorf("Inputs and Outputs mismatch: %w", err) + } + + var expected map[factom.Bytes32]struct{} + // Coinbase transactions must only have one input. + if t.IsCoinbase() { + if len(t.Inputs) != 1 { + return t, fmt.Errorf("invalid coinbase transaction") + } + if err := t.TokenMetadata.isSubsetOf( + t.Inputs[fat.Coinbase()]); err != nil { + return t, fmt.Errorf("%T.TokenMetadata: %w", t, err) + } + + expected = map[factom.Bytes32]struct{}{*idKey: struct{}{}} + } else { + if len(t.TokenMetadata) > 0 { + return t, fmt.Errorf( + `non-coinbase transaction with "tokenmetadata"`) + } + + expected = make(map[factom.Bytes32]struct{}, len(t.Inputs)) + for adr := range t.Inputs { + expected[factom.Bytes32(adr)] = struct{}{} + } + } + + if err := fat103.Validate(e, expected); err != nil { + return t, err + } + + t.Entry = e + + return t, nil +} + +func (t *Transaction) UnmarshalJSON(data []byte) error { + tRaw := struct { + Inputs json.RawMessage `json:"inputs"` + Outputs json.RawMessage `json:"outputs"` + TokenMetadata json.RawMessage `json:"tokenmetadata"` + Metadata json.RawMessage `json:"metadata,omitempty"` + }{} + if err := json.Unmarshal(data, &tRaw); err != nil { + return fmt.Errorf("%T: %w", t, err) + } + if err := t.Inputs.UnmarshalJSON(tRaw.Inputs); err != nil { + return fmt.Errorf("%T.Inputs: %w", t, err) + } + if err := t.Outputs.UnmarshalJSON(tRaw.Outputs); err != nil { + return fmt.Errorf("%T.Outputs: %w", t, err) + } + + expectedJSONLen := len(`{"inputs":,"outputs":}`) + + len(jsonlen.Compact(tRaw.Inputs)) + len(jsonlen.Compact(tRaw.Outputs)) + if tRaw.Metadata != nil { + expectedJSONLen += len(`,"metadata":`) + len(tRaw.Metadata) + t.Metadata = tRaw.Metadata + } + + if len(tRaw.TokenMetadata) > 0 { + if err := t.TokenMetadata.UnmarshalJSON(tRaw.TokenMetadata); err != nil { + return fmt.Errorf("%T.TokenMetadata: %w", t, err) + + } + + expectedJSONLen += len(`,"tokenmetadata":`) + + len(jsonlen.Compact(tRaw.TokenMetadata)) + } + + if expectedJSONLen != len(jsonlen.Compact(data)) { + return fmt.Errorf("%T: unexpected JSON length", t) + } + + return nil +} + +func (t Transaction) IsCoinbase() bool { + _, ok := t.Inputs[fat.Coinbase()] + return ok +} + +func (t Transaction) String() string { + data, err := json.Marshal(t) + if err != nil { + return err.Error() + } + return string(data) +} + +func (t Transaction) Sign(signingSet ...factom.RCDPrivateKey) (factom.Entry, error) { + e := t.Entry + content, err := json.Marshal(t) + if err != nil { + return e, err + } + e.Content = content + return fat103.Sign(e, signingSet...), nil +} diff --git a/fat/fat1/transaction_test.go b/fat1/transaction_test.go similarity index 92% rename from fat/fat1/transaction_test.go rename to fat1/transaction_test.go index 5e450d4..ed8895f 100644 --- a/fat/fat1/transaction_test.go +++ b/fat1/transaction_test.go @@ -28,9 +28,9 @@ import ( "fmt" "testing" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" "github.com/Factom-Asset-Tokens/fatd/fat" - . "github.com/Factom-Asset-Tokens/fatd/fat/fat1" + . "github.com/Factom-Asset-Tokens/fatd/fat1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,13 +40,13 @@ var transactionTests = []struct { Error string IssuerKey factom.ID1Key Coinbase bool - Tx Transaction + Tx *Transaction }{{ Name: "valid", Tx: validTx(), }, { Name: "valid (single outputs)", - Tx: func() Transaction { + Tx: func() *Transaction { out := outputs() out[outputAddresses[0].FAAddress().String()]. Append(out[outputAddresses[1].FAAddress().String()]) @@ -110,7 +110,7 @@ var transactionTests = []struct { }, { Name: "invalid data (Input Output mismatch)", Error: "*fat1.Transaction: Inputs and Outputs mismatch: number of NFTokenIDs differ", - Tx: func() Transaction { + Tx: func() *Transaction { out := outputs() NFTokenID(1000).Set(out[outputAddresses[0].FAAddress().String()]) return setFieldTransaction("outputs", out) @@ -118,7 +118,7 @@ var transactionTests = []struct { }, { Name: "invalid data (Input Output mismatch)", Error: "*fat1.Transaction: Inputs and Outputs mismatch: missing NFTokenID: 1000", - Tx: func() Transaction { + Tx: func() *Transaction { in := inputs() NFTokenID(1001).Set(in[inputAddresses[0].FAAddress().String()]) out := outputs() @@ -132,7 +132,7 @@ var transactionTests = []struct { Name: "invalid data (coinbase)", Error: "*fat1.Transaction: invalid coinbase transaction", IssuerKey: issuerKey, - Tx: func() Transaction { + Tx: func() *Transaction { m := validCoinbaseTxEntryContentMap() in := coinbaseInputs() in[inputAddresses[0].FAAddress().String()] = newNFTokens(NFTokenID(1000)) @@ -146,7 +146,7 @@ var transactionTests = []struct { Name: "invalid data (coinbase, coinbase outputs)", Error: "*fat1.Transaction: Inputs and Outputs intersect: duplicate address: ", IssuerKey: issuerKey, - Tx: func() Transaction { + Tx: func() *Transaction { m := validCoinbaseTxEntryContentMap() in := coinbaseInputs() out := coinbaseOutputs() @@ -161,7 +161,7 @@ var transactionTests = []struct { Name: "invalid data (coinbase, tokenmetadata)", Error: "*fat1.Transaction.TokenMetadata: too many NFTokenIDs", IssuerKey: issuerKey, - Tx: func() Transaction { + Tx: func() *Transaction { m := validCoinbaseTxEntryContentMap() in := coinbaseInputs() delete(in[fat.Coinbase().String()], NFTokenID(0)) @@ -175,7 +175,7 @@ var transactionTests = []struct { }, { Name: "invalid data (inputs outputs overlap)", Error: "*fat1.Transaction: Inputs and Outputs intersect: duplicate address: ", - Tx: func() Transaction { + Tx: func() *Transaction { m := validTxEntryContentMap() in := inputs() in[outputAddresses[0].FAAddress().String()] = @@ -187,7 +187,7 @@ var transactionTests = []struct { }, { Name: "invalid ExtIDs (timestamp)", Error: "timestamp salt expired", - Tx: func() Transaction { + Tx: func() *Transaction { t := validTx() t.ExtIDs[0] = factom.Bytes("100") return t @@ -195,7 +195,7 @@ var transactionTests = []struct { }, { Name: "invalid ExtIDs (length)", Error: "invalid number of ExtIDs", - Tx: func() Transaction { + Tx: func() *Transaction { t := validTx() t.ExtIDs = append(t.ExtIDs, factom.Bytes{}) return t @@ -207,7 +207,7 @@ var transactionTests = []struct { }, { Name: "RCD input mismatch", Error: "invalid RCDs", - Tx: func() Transaction { + Tx: func() *Transaction { t := validTx() adrs := twoAddresses() t.Sign(adrs[0], adrs[1]) @@ -221,7 +221,7 @@ func TestTransaction(t *testing.T) { assert := assert.New(t) tx := test.Tx key := test.IssuerKey - err := tx.Valid(&key) + err := tx.Validate(&key) if len(test.Error) != 0 { assert.Contains(err.Error(), test.Error) return @@ -252,7 +252,7 @@ var ( newNFTokens(NewNFTokenIDRange(6, 11))} identityChainID = factom.NewBytes32(validIdentityChainID()) - tokenChainID = fat.ChainID("test", *identityChainID) + tokenChainID = fat.ComputeChainID("test", identityChainID) ) func newNFTokens(ids ...NFTokensSetter) NFTokens { @@ -264,25 +264,25 @@ func newNFTokens(ids ...NFTokensSetter) NFTokens { } // Transactions -func omitFieldTransaction(field string) Transaction { +func omitFieldTransaction(field string) *Transaction { m := validTxEntryContentMap() delete(m, field) return transaction(marshal(m)) } -func setFieldTransaction(field string, value interface{}) Transaction { +func setFieldTransaction(field string, value interface{}) *Transaction { m := validTxEntryContentMap() m[field] = value return transaction(marshal(m)) } -func validTx() Transaction { +func validTx() *Transaction { return transaction(marshal(validTxEntryContentMap())) } -func coinbaseTx() Transaction { +func coinbaseTx() *Transaction { t := transaction(marshal(validCoinbaseTxEntryContentMap())) t.Sign(issuerSecret) return t } -func transaction(content factom.Bytes) Transaction { +func transaction(content factom.Bytes) *Transaction { e := factom.Entry{ ChainID: &tokenChainID, Content: content, @@ -295,7 +295,7 @@ func transaction(content factom.Bytes) Transaction { t.Sign(adrs...) return t } -func invalidField(field string) Transaction { +func invalidField(field string) *Transaction { m := validTxEntryContentMap() m[field] = []int{0} return transaction(marshal(m)) @@ -320,7 +320,7 @@ func validCoinbaseTxEntryContentMap() map[string]interface{} { // inputs/outputs func inputs() map[string]NFTokens { - inputs := map[string]NFTokens{} + inputs := make(map[string]NFTokens) for i := range inputAddresses { tkns := newNFTokens() tkns.Append(inputNFTokens[i]) @@ -329,7 +329,7 @@ func inputs() map[string]NFTokens { return inputs } func outputs() map[string]NFTokens { - outputs := map[string]NFTokens{} + outputs := make(map[string]NFTokens) for i := range outputAddresses { tkns := newNFTokens() tkns.Append(outputNFTokens[i]) @@ -338,7 +338,7 @@ func outputs() map[string]NFTokens { return outputs } func coinbaseInputs() map[string]NFTokens { - inputs := map[string]NFTokens{} + inputs := make(map[string]NFTokens) for i := range coinbaseInputAddresses { tkns := newNFTokens() tkns.Append(coinbaseInputNFTokens[i]) @@ -347,7 +347,7 @@ func coinbaseInputs() map[string]NFTokens { return inputs } func coinbaseOutputs() map[string]NFTokens { - outputs := map[string]NFTokens{} + outputs := make(map[string]NFTokens) for i := range coinbaseOutputAddresses { tkns := newNFTokens() tkns.Append(coinbaseOutputNFTokens[i]) @@ -359,20 +359,20 @@ func coinbaseOutputs() map[string]NFTokens { var transactionMarshalEntryTests = []struct { Name string Error string - Tx Transaction + Tx *Transaction }{{ Name: "valid", Tx: newTransaction(), }, { Name: "valid (omit zero balances)", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Inputs[fat.Coinbase()], _ = NewNFTokens() return t }(), }, { Name: "valid (metadata)", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Metadata = json.RawMessage(`{"memo":"Rent for Dec 2018"}`) return t @@ -380,7 +380,7 @@ var transactionMarshalEntryTests = []struct { }, { Name: "invalid data", Error: "json: error calling MarshalJSON for type *fat1.Transaction: Inputs and Outputs mismatch: number of NFTokenIDs differ", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Inputs[inputAddresses[0].FAAddress()].Set(NFTokenID(12345)) return t @@ -388,7 +388,7 @@ var transactionMarshalEntryTests = []struct { }, { Name: "invalid metadata JSON", Error: "json: error calling MarshalJSON for type *fat1.Transaction: json: error calling MarshalJSON for type json.RawMessage: invalid character 'a' looking for beginning of object key string", - Tx: func() Transaction { + Tx: func() *Transaction { t := newTransaction() t.Metadata = json.RawMessage("{asdf") return t @@ -410,8 +410,8 @@ func TestTransactionMarshalEntry(t *testing.T) { } } -func newTransaction() Transaction { - return Transaction{ +func newTransaction() *Transaction { + return &Transaction{ Inputs: inputAddressNFTokensMap(), Outputs: outputAddressNFTokensMap(), } @@ -425,7 +425,7 @@ func outputAddressNFTokensMap() AddressNFTokensMap { func addressNFTokensMap(aas map[string]NFTokens) AddressNFTokensMap { m := make(AddressNFTokensMap) for adrStr, amount := range aas { - a := factom.FAAddress{} + var a factom.FAAddress if err := a.Set(adrStr); err != nil { panic(err.Error() + " " + adrStr) } diff --git a/fat103/sign.go b/fat103/sign.go new file mode 100644 index 0000000..3f76796 --- /dev/null +++ b/fat103/sign.go @@ -0,0 +1,50 @@ +package fat103 + +import ( + "crypto/ed25519" + "crypto/sha512" + "math/rand" + "strconv" + "time" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" +) + +// Sign the RCD/Sig ID Salt + Timestamp Salt + Chain ID Salt + Content of the +// factom.Entry and add the RCD + signature pairs for the given addresses to +// the ExtIDs. This clears any existing ExtIDs. +func Sign(e factom.Entry, signingSet ...factom.RCDPrivateKey) factom.Entry { + // Set the Entry's timestamp so that the signatures will verify against + // this time salt. + timeSalt := newTimestampSalt() + e.Timestamp = time.Now() + + // Compose the signed message data using exactly allocated bytes. + maxRcdSigIDSaltStrLen := jsonlen.Uint64(uint64(len(signingSet))) + maxMsgLen := maxRcdSigIDSaltStrLen + len(timeSalt) + len(e.ChainID) + len(e.Content) + msg := make(factom.Bytes, maxMsgLen) + i := maxRcdSigIDSaltStrLen + i += copy(msg[i:], timeSalt[:]) + i += copy(msg[i:], e.ChainID[:]) + copy(msg[i:], e.Content) + + // Generate the ExtIDs for each address in the signing set. + e.ExtIDs = make([]factom.Bytes, 1, len(signingSet)*2+1) + e.ExtIDs[0] = timeSalt + for rcdSigID, a := range signingSet { + // Compose the RcdSigID salt and prepend it to the message. + rcdSigIDSalt := strconv.FormatUint(uint64(rcdSigID), 10) + start := maxRcdSigIDSaltStrLen - len(rcdSigIDSalt) + copy(msg[start:], rcdSigIDSalt) + + msgHash := sha512.Sum512(msg[start:]) + sig := ed25519.Sign(a.PrivateKey(), msgHash[:]) + e.ExtIDs = append(e.ExtIDs, a.RCD(), sig) + } + return e +} +func newTimestampSalt() []byte { + timestamp := time.Now().Add(time.Duration(-rand.Int63n(int64(1 * time.Hour)))) + return []byte(strconv.FormatInt(timestamp.Unix(), 10)) +} diff --git a/fat103/validate.go b/fat103/validate.go new file mode 100644 index 0000000..a6a181b --- /dev/null +++ b/fat103/validate.go @@ -0,0 +1,114 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package fat103 + +import ( + "crypto/sha256" + "crypto/sha512" + "fmt" + "strconv" + "time" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/internal/jsonlen" + + "crypto/ed25519" +) + +// Validate validates the structure of the ExtIDs of the factom.Entry to make +// sure that it has a valid timestamp salt and a valid set of RCD/signature +// pairs. +func Validate(e factom.Entry, expected map[factom.Bytes32]struct{}) error { + if len(expected) == 0 || len(e.ExtIDs) != 2*len(expected)+1 { + return fmt.Errorf("invalid number of ExtIDs") + } + + // Validate Timestamp Salt + timestampSalt := string(e.ExtIDs[0]) + sec, err := strconv.ParseInt(timestampSalt, 10, 64) + if err != nil { + return fmt.Errorf("ExtIDs[0]: timestamp salt: %w", err) + } + ts := time.Unix(sec, 0) + diff := e.Timestamp.Sub(ts) + if -12*time.Hour > diff || diff > 12*time.Hour { + return fmt.Errorf("ExtIDs[0]: timestamp salt: expired") + } + + // Compose the signed message data using exactly allocated bytes. + numRcdSigPairs := len(e.ExtIDs) / 2 + maxRcdSigIDSalt := numRcdSigPairs - 1 + maxRcdSigIDSaltStrLen := jsonlen.Uint64(uint64(maxRcdSigIDSalt)) + timeSalt := e.ExtIDs[0] + maxMsgLen := maxRcdSigIDSaltStrLen + + len(timeSalt) + + len(e.ChainID) + + len(e.Content) + msg := make([]byte, maxMsgLen) + i := maxRcdSigIDSaltStrLen + i += copy(msg[i:], timeSalt) + i += copy(msg[i:], e.ChainID[:]) + copy(msg[i:], e.Content) + + rcdSigs := e.ExtIDs[1:] + for i := 0; i < len(rcdSigs); i += 2 { + rcd := rcdSigs[i] + if len(rcd) != factom.RCDSize { + return fmt.Errorf("ExtIDs[%v]: invalid RCD size", i+1) + } + if rcd[0] != factom.RCDType { + return fmt.Errorf("ExtIDs[%v]: invalid RCD type", i+1) + } + rcdHash := sha256d(rcd) + if _, ok := expected[rcdHash]; !ok { + return fmt.Errorf( + "ExtIDs[%v]: unexpected or duplicate RCD Hash", i+1) + } + delete(expected, rcdHash) + + sig := rcdSigs[i+1] + if len(sig) != factom.SignatureSize { + return fmt.Errorf("ExtIDs[%v]: invalid signature size", i+1) + } + + rcdSigID := i / 2 + // Prepend the RCD Sig ID Salt to the message data + rcdSigIDSalt := strconv.FormatUint(uint64(rcdSigID), 10) + start := maxRcdSigIDSaltStrLen - len(rcdSigIDSalt) + copy(msg[start:], rcdSigIDSalt) + + msgHash := sha512.Sum512(msg[start:]) + pubKey := []byte(rcd[1:]) // Omit RCD Type byte + if !ed25519.Verify(pubKey, msgHash[:], sig) { + return fmt.Errorf("ExtIDs[%v]: invalid signature", i+1+1) + } + } + + return nil +} + +// sha256d computes two rounds of the sha256 hash. +func sha256d(data []byte) factom.Bytes32 { + hash := sha256.Sum256(data) + return sha256.Sum256(hash[:]) +} diff --git a/fat103/validate_test.go b/fat103/validate_test.go new file mode 100644 index 0000000..6f22aff --- /dev/null +++ b/fat103/validate_test.go @@ -0,0 +1,218 @@ +package fat103 + +import ( + "math/rand" + "strconv" + "testing" + "time" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + for _, test := range validateTests { + test := test + t.Run(test.Name, func(t *testing.T) { testValidate(t, test) }) + } +} + +func testValidate(t *testing.T, test validateTest) { + assert := assert.New(t) + err := Validate(test.Entry, rcdHashes(test.Expected)) + if len(test.Error) == 0 { + assert.NoError(err) + return + } + assert.EqualError(err, test.Error) +} + +type validateTest struct { + Name string + factom.Entry + Expected []factom.RCDPrivateKey + Error string +} + +var validateTests = []validateTest{ + func() validateTest { + e, adrs := validEntry(2) + return validateTest{ + Name: "valid", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(100) + return validateTest{ + Name: "valid (large signing set)", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs = nil + return validateTest{ + Name: "nil ExtIDs", + Error: "invalid number of ExtIDs", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs = append(e.ExtIDs, factom.Bytes{}) + return validateTest{ + Name: "extra ExtIDs", + Error: "invalid number of ExtIDs", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs[0] = []byte("xxxx") + return validateTest{ + Name: "invalid timestamp (format)", + Error: "ExtIDs[0]: timestamp salt: strconv.ParseInt: parsing \"xxxx\": invalid syntax", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.Timestamp = time.Now().Add(-48 * time.Hour) + return validateTest{ + Name: "invalid timestamp (expired)", + Error: "ExtIDs[0]: timestamp salt: expired", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.Timestamp = time.Now().Add(48 * time.Hour) + return validateTest{ + Name: "invalid timestamp (expired)", + Error: "ExtIDs[0]: timestamp salt: expired", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs[1] = append(e.ExtIDs[1], 0x00) + return validateTest{ + Name: "invalid RCD size", + Error: "ExtIDs[1]: invalid RCD size", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs[1][0]++ + return validateTest{ + Name: "invalid RCD type", + Error: "ExtIDs[1]: invalid RCD type", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs[2] = append(e.ExtIDs[2], 0x00) + return validateTest{ + Name: "invalid signature size", + Error: "ExtIDs[1]: invalid signature size", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ExtIDs[2][0]++ + return validateTest{ + Name: "invalid signatures", + Error: "ExtIDs[2]: invalid signature", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + rcdSig := e.ExtIDs[1:3] + e.ExtIDs[1] = e.ExtIDs[3] + e.ExtIDs[2] = e.ExtIDs[4] + e.ExtIDs[3] = rcdSig[0] + e.ExtIDs[4] = rcdSig[1] + return validateTest{ + Name: "invalid signatures (transpose)", + Error: "ExtIDs[2]: invalid signature", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + ts := time.Now().Add(time.Duration( + -rand.Int63n(int64(12 * time.Hour)))) + timeSalt := []byte(strconv.FormatInt(ts.Unix(), 10)) + e.ExtIDs[0] = timeSalt + return validateTest{ + Name: "invalid signatures (timestamp)", + Error: "ExtIDs[2]: invalid signature", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, adrs := validEntry(2) + e.ChainID = new(factom.Bytes32) + e.ChainID[0] = 0x01 + e.ChainID[2] = 0x02 + return validateTest{ + Name: "invalid signatures (chain ID)", + Error: "ExtIDs[2]: invalid signature", + Entry: e, + Expected: adrs, + } + }(), func() validateTest { + e, _ := validEntry(2) + return validateTest{ + Name: "unexpected RCD", + Error: "ExtIDs[0]: unexpected or duplicate RCD Hash", + Entry: e, + Expected: genAddresses(2), + } + }(), func() validateTest { + e, adrs := validEntry(3) + e.ExtIDs = append(e.ExtIDs[:5], e.ExtIDs[1:3]...) + return validateTest{ + Name: "unexpected RCD (duplicate)", + Error: "ExtIDs[4]: unexpected or duplicate RCD Hash", + Entry: e, + Expected: adrs, + } + }(), +} + +func validEntry(n int) (factom.Entry, []factom.RCDPrivateKey) { + var e factom.Entry + e.Content = factom.Bytes("some data to sign") + e.ChainID = new(factom.Bytes32) + *e.ChainID = factom.ComputeChainID([]factom.Bytes{factom.Bytes("test chain ID")}) + // Generate valid signatures with blank Addresses. + adrs := genAddresses(n) + e = Sign(e, adrs...) + return e, adrs +} + +func genAddresses(n int) []factom.RCDPrivateKey { + adrs := make([]factom.RCDPrivateKey, n) + for i := range adrs { + adr, err := factom.GenerateFsAddress() + if err != nil { + panic(err) + } + adrs[i] = adr + } + return adrs +} + +func rcdHashes(adrs []factom.RCDPrivateKey) map[factom.Bytes32]struct{} { + rcdHashes := make(map[factom.Bytes32]struct{}, len(adrs)) + for _, adr := range adrs { + rcdHashes[sha256d(adr.RCD())] = struct{}{} + } + return rcdHashes +} diff --git a/fatd@.service b/fatd@.service new file mode 100644 index 0000000..d2a0d96 --- /dev/null +++ b/fatd@.service @@ -0,0 +1,16 @@ +[Unit] +Description=Run the Factom Asset Tokens Daemon on %i +Wants=network-online.target +After=network-online.target + +[Service] +User=fatd +Group=fatd + +Environment=FATD_DB_PATH=/var/lib/fatd +EnvironmentFile=-/etc/default/fatd +EnvironmentFile=-/etc/default/fatd@%i +ExecStart=/usr/bin/fatd $FATD_START_OPTS -networkid=%i + +[Install] +WantedBy=default.target diff --git a/go.mod b/go.mod index 1745b39..b940379 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,32 @@ module github.com/Factom-Asset-Tokens/fatd -go 1.12 +go 1.13 require ( - github.com/AdamSLevy/go-merkle v0.0.0-20190611101253-ca33344a884d - github.com/AdamSLevy/jsonrpc2/v11 v11.3.2 - github.com/Factom-Asset-Tokens/base58 v0.0.0-20181227014902-61655c4dd885 - github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect - github.com/gocraft/dbr v0.0.0-20190131145710-48a049970bd2 - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/jinzhu/gorm v1.9.4 - github.com/jinzhu/now v1.0.0 // indirect - github.com/kr/pretty v0.1.0 // indirect + crawshaw.io/sqlite v0.1.3-0.20190520153332-66f853b01dfb + github.com/AdamSLevy/jsonrpc2/v12 v12.0.2-0.20191015223217-9181d6ac9347 + github.com/AdamSLevy/sqlitechangeset v0.0.0-20190925183646-3ddb70fb709d + github.com/Factom-Asset-Tokens/factom v0.0.0-20191025014600-db7c22b6c31a + github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/mitchellh/go-homedir v1.1.0 + github.com/nightlyone/lockfile v0.0.0-20180618180623-0ad87eef1443 github.com/posener/complete v1.2.1 - github.com/rs/cors v1.6.0 - github.com/sirupsen/logrus v1.4.1 - github.com/spf13/cobra v0.0.3 + github.com/rs/cors v1.7.0 + github.com/sirupsen/logrus v1.4.2 + github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.3 - github.com/spf13/viper v1.3.2 - github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 - golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 + github.com/spf13/viper v1.4.0 + github.com/stretchr/testify v1.4.0 + golang.org/x/sys v0.0.0-20190911201528-7ad0cfa0b7b5 // indirect ) -replace github.com/gocraft/dbr => github.com/AdamSLevy/dbr v0.0.0-20190429075658-5db28ac75cea - replace github.com/spf13/pflag v1.0.3 => github.com/AdamSLevy/pflag v1.0.4 + +replace crawshaw.io/sqlite => github.com/AdamSLevy/sqlite v0.1.3-0.20191014215059-b98bb18889de + +//replace github.com/Factom-Asset-Tokens/factom => ../factom + +//replace crawshaw.io/sqlite => /home/aslevy/repos/go-modules/AdamSLevy/sqlite + +//replace github.com/AdamSLevy/jsonrpc2/v12 => /home/aslevy/repos/go-modules/AdamSLevy/jsonrpc2 diff --git a/go.sum b/go.sum index e9a79fd..18b7073 100644 --- a/go.sum +++ b/go.sum @@ -1,280 +1,202 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.2 h1:4y4L7BdHenTfZL0HervofNTHh9Ad6mNX72cQvl+5eH0= -cloud.google.com/go v0.37.2/go.mod h1:H8IAquKe2L30IxoupDgqTaQvKSwF/c8prYHynGIWQbA= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/AdamSLevy/dbr v0.0.0-20190429075658-5db28ac75cea h1:8jg7gBfU9VSu5i12RTCgR4HzPt9ZZCLz+bz9/qFcRtw= -github.com/AdamSLevy/dbr v0.0.0-20190429075658-5db28ac75cea/go.mod h1:TWT34di9Z4gHhQ/LjVuBGMXKsQgkGhdwNh/ZvFmJ6DU= +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797 h1:yDf7ARQc637HoxDho7xjqdvO5ZA2Yb+xzv/fOnnvZzw= +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= github.com/AdamSLevy/go-merkle v0.0.0-20190611101253-ca33344a884d h1:FWutTJGVqBnL4rLgeNaspUYnmnvkXcmDA3QO3rHBGgU= github.com/AdamSLevy/go-merkle v0.0.0-20190611101253-ca33344a884d/go.mod h1:Nw3sh5L40Xs1wno7ndbD/dYWg+vARpBvpX9Zz1YSxbo= -github.com/AdamSLevy/jsonrpc2/v11 v11.3.2 h1:McSW/pP7K0/Ucjig6AJwW7Khph/XOMYhSB8v3YxMBl4= -github.com/AdamSLevy/jsonrpc2/v11 v11.3.2/go.mod h1:7fNjH6BXM0KVswWqj+K/mnOS8wiSke0sE8X46hS+nsc= +github.com/AdamSLevy/jsonrpc2/v12 v12.0.2-0.20191005213732-3b0dfc9c5f77 h1:+UJmKY1f0AITEyfhLaH9zpqIwLgVIKLE+BBCY2B2gys= +github.com/AdamSLevy/jsonrpc2/v12 v12.0.2-0.20191005213732-3b0dfc9c5f77/go.mod h1:UUmIu8A7Sjw+yI0tIc/iGQHoSu/7loifZ7E/AbjFFcI= +github.com/AdamSLevy/jsonrpc2/v12 v12.0.2-0.20191015223217-9181d6ac9347 h1:bnHpux+c+kROwUL+2nscrUODAa33JQWlViGCBD2W5bg= +github.com/AdamSLevy/jsonrpc2/v12 v12.0.2-0.20191015223217-9181d6ac9347/go.mod h1:UUmIu8A7Sjw+yI0tIc/iGQHoSu/7loifZ7E/AbjFFcI= github.com/AdamSLevy/pflag v1.0.4 h1:oykgyxDWo391JRRNOLr892UzaK2SmLiAvza8OBLX24U= github.com/AdamSLevy/pflag v1.0.4/go.mod h1:UeCxQpw/gAoTUVBfjr8n7qcGZZNmECgohVFOAdlRMtA= +github.com/AdamSLevy/sqlite v0.1.3-0.20191014215059-b98bb18889de h1:ehn7bnzDZt3ZTXLR5gn0p0NUUl50wZazoi41y7qNrGY= +github.com/AdamSLevy/sqlite v0.1.3-0.20191014215059-b98bb18889de/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +github.com/AdamSLevy/sqlitechangeset v0.0.0-20190925183646-3ddb70fb709d h1:yPm4An70OhM4k4WUq7M9sWaVlFas2+hJB+I3Fsgw38A= +github.com/AdamSLevy/sqlitechangeset v0.0.0-20190925183646-3ddb70fb709d/go.mod h1:kQNmmf+2gf3uGKHt0LS4guxdp4Ay44SXA4+Is8/Gxm8= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Factom-Asset-Tokens/base58 v0.0.0-20181227014902-61655c4dd885 h1:rfy2fwMrOZPqQgjsH7VCreGMSPzcvqQrfjeSs8nf+sY= github.com/Factom-Asset-Tokens/base58 v0.0.0-20181227014902-61655c4dd885/go.mod h1:RVXsRSp6VzXw5l1uiGazuf3qo23Qk0h1HzMcQk+X4LE= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/Factom-Asset-Tokens/factom v0.0.0-20191025014600-db7c22b6c31a h1:jDjY4eIs6/A+rR5Mr28dbPXs0xmT+FrB9pze7PYwgkA= +github.com/Factom-Asset-Tokens/factom v0.0.0-20191025014600-db7c22b6c31a/go.mod h1:HfWlWphJ30PlWXD9FTiAboLc58dQU/2A9HxBGnKyDpg= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 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/denisenkom/go-mssqldb v0.0.0-20190401154936-ce35bd87d4b3 h1:3mNLx0iFqaq/Ssxqkjte26072KMu96uz1VBlbiZhQU4= -github.com/denisenkom/go-mssqldb v0.0.0-20190401154936-ce35bd87d4b3/go.mod h1:EcO5fNtMZHCMjAvj8LE6T+5bphSdR6LQ75n+m1TtsFI= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d h1:lBXNCxVENCipq4D1Is42JVOP4eQjlB8TQ6H69Yx5J9Q= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= -github.com/jinzhu/gorm v1.9.4 h1:3KDoUjMEfH58nweXdD5Dng222YiwOVUNFShENhehJyQ= -github.com/jinzhu/gorm v1.9.4/go.mod h1:7ZYqlk/T0SqZip7ZOIL1aC/sjDj+dJo6sN98WljHFXY= -github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= -github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.0 h1:6WV8LvwPpDhKjo5U9O6b4+xdG/jTXNPwlDme/MTo8Ns= -github.com/jinzhu/now v1.0.0/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= -github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 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.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 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/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/nightlyone/lockfile v0.0.0-20180618180623-0ad87eef1443 h1:+2OJrU8cmOstEoh0uQvYemRGVH1O6xtO2oANUWHFnP0= +github.com/nightlyone/lockfile v0.0.0-20180618180623-0ad87eef1443/go.mod h1:JbxfV1Iifij2yhRjXai0oFrbpxszXHRx1E5RuM26o4Y= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/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/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI= github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= -github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= -github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= -go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= -go.opencensus.io v0.19.2/go.mod h1:NO/8qkisMZLZ1FCsKNqtJPwc8/TaclWyY0B6wcYNg9M= -go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= -golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5aV1gMSwgiKVHwu/JncqDpuRr7lS4= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190911201528-7ad0cfa0b7b5 h1:SW/0nsKCUaozCUtZTakri5laocGx/5bkDSSLrFUsa5s= +golang.org/x/sys v0.0.0-20190911201528-7ad0cfa0b7b5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= -google.golang.org/api v0.3.0/go.mod h1:IuvZyQh8jgscv8qWfQ4ABd8m7hEudgBFM/EdhA3BnXw= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0 h1:S0iUepdCWODXRvtE+gcRDd15L+k+k1AiHlMiMjefH24= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 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= -grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/db/addresses/addresses.go b/internal/db/addresses/addresses.go new file mode 100644 index 0000000..787d146 --- /dev/null +++ b/internal/db/addresses/addresses.go @@ -0,0 +1,132 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +// Package addresses provides functions and SQL framents for working with the +// "addresses" table, which stores factom.FAAddress with its balance. +package addresses + +import ( + "fmt" + + "crawshaw.io/sqlite" + "crawshaw.io/sqlite/sqlitex" + "github.com/Factom-Asset-Tokens/factom" +) + +// CreateTable is a SQL string that creates the "addresses" table. +const CreateTable = `CREATE TABLE "addresses" ( + "id" INTEGER PRIMARY KEY, + "address" BLOB NOT NULL UNIQUE, + "balance" INTEGER NOT NULL + CONSTRAINT "insufficient balance" CHECK ("balance" >= 0) +); +` + +// Add adds add to the balance of adr, creating a new row in "addresses" if it +// does not exist. If successful, the row id of adr is returned. +func Add(conn *sqlite.Conn, adr *factom.FAAddress, add uint64) (int64, error) { + stmt := conn.Prep(`INSERT INTO "addresses" + ("address", "balance") VALUES (?, ?) + ON CONFLICT("address") DO + UPDATE SET "balance" = "balance" + "excluded"."balance";`) + stmt.BindBytes(1, adr[:]) + stmt.BindInt64(2, int64(add)) + _, err := stmt.Step() + if err != nil { + return -1, err + } + return SelectID(conn, adr) +} + +const sqlitexNoResultsErr = "sqlite: statement has no results" + +// Sub subtracts sub from the balance of adr creating the row in "addresses" if +// it does not exist and sub is 0. If successful, the row id of adr is +// returned. If subtracting sub would result in a negative balance, txErr is +// not nil and starts with "insufficient balance". +func Sub(conn *sqlite.Conn, + adr *factom.FAAddress, sub uint64) (id int64, txErr, err error) { + if sub == 0 { + // Allow tx's with zeros to result in an INSERT. + id, err = Add(conn, adr, 0) + return id, nil, err + } + id, err = SelectID(conn, adr) + if err != nil { + if err.Error() == sqlitexNoResultsErr { + return id, fmt.Errorf("insufficient balance: %v", adr), nil + } + return id, nil, err + } + if id < 0 { + return id, fmt.Errorf("insufficient balance: %v", adr), nil + } + stmt := conn.Prep( + `UPDATE addresses SET balance = balance - ? WHERE rowid = ?;`) + stmt.BindInt64(1, int64(sub)) + stmt.BindInt64(2, id) + if _, err := stmt.Step(); err != nil { + if sqlite.ErrCode(err) == sqlite.SQLITE_CONSTRAINT_CHECK { + return id, fmt.Errorf("insufficient balance: %v", adr), nil + } + return id, nil, err + } + if conn.Changes() == 0 { + panic("no balances updated") + } + return id, nil, nil +} + +// SelectIDBalance returns the row id and balance for the given adr. +func SelectIDBalance(conn *sqlite.Conn, + adr *factom.FAAddress) (adrID int64, bal uint64, err error) { + adrID = -1 + stmt := conn.Prep(`SELECT "id", "balance" FROM "addresses" WHERE "address" = ?;`) + defer stmt.Reset() + stmt.BindBytes(1, adr[:]) + hasRow, err := stmt.Step() + if err != nil { + return + } + if !hasRow { + return + } + adrID = stmt.ColumnInt64(0) + bal = uint64(stmt.ColumnInt64(1)) + return +} + +// SelectID returns the row id for the given adr. +func SelectID(conn *sqlite.Conn, adr *factom.FAAddress) (int64, error) { + stmt := conn.Prep(`SELECT "id" FROM "addresses" WHERE "address" = ?;`) + stmt.BindBytes(1, adr[:]) + return sqlitex.ResultInt64(stmt) +} + +// SelectCount returns the number of rows in "addresses". If nonZeroOnly is +// true, then only count the addresses with a non zero balance. +func SelectCount(conn *sqlite.Conn, nonZeroOnly bool) (int64, error) { + stmt := conn.Prep(`SELECT count(*) FROM "addresses" WHERE "id" != 1 + AND (? OR "balance" > 0);`) + stmt.BindBool(1, !nonZeroOnly) + return sqlitex.ResultInt64(stmt) +} diff --git a/internal/db/addresses/txrelation.go b/internal/db/addresses/txrelation.go new file mode 100644 index 0000000..e0b5c7e --- /dev/null +++ b/internal/db/addresses/txrelation.go @@ -0,0 +1,41 @@ +package addresses + +import "crawshaw.io/sqlite" + +// CreateTableTransactions is a SQL string that creates the +// "address_transactions" table. +// +// The "address_transactions" table has a foreign key reference to the +// "addresses" and "entries" tables, which must exist first. +const CreateTableTransactions = `CREATE TABLE "address_transactions" ( + "entry_id" INTEGER NOT NULL, + "address_id" INTEGER NOT NULL, + "to" BOOL NOT NULL, + + PRIMARY KEY("entry_id", "address_id", "to"), + + FOREIGN KEY("entry_id") REFERENCES "entries", + FOREIGN KEY("address_id") REFERENCES "addresses" +); +CREATE INDEX "idx_address_transactions_address_id" ON "address_transactions"("address_id"); +CREATE INDEX "idx_address_transactions_entry_id" ON "address_transactions"("entry_id"); +` + +// InsertTransactionRelation inserts a row into "address_transactions" relating +// the adrID with the entryID with the given transaction direction, to. If +// successful, the row id for the new row in "address_transactions" is +// returned. +func InsertTransactionRelation(conn *sqlite.Conn, + adrID int64, entryID int64, to bool) (int64, error) { + stmt := conn.Prep(`INSERT INTO "address_transactions" + ("address_id", "entry_id", "to") VALUES + (?, ?, ?)`) + stmt.BindInt64(1, adrID) + stmt.BindInt64(2, entryID) + stmt.BindBool(3, to) + _, err := stmt.Step() + if err != nil { + return -1, err + } + return conn.LastInsertRowID(), nil +} diff --git a/internal/db/apply.go b/internal/db/apply.go new file mode 100644 index 0000000..b61e98e --- /dev/null +++ b/internal/db/apply.go @@ -0,0 +1,345 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package db + +import ( + "fmt" + + "crawshaw.io/sqlite/sqlitex" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/fat0" + "github.com/Factom-Asset-Tokens/fatd/fat1" + "github.com/Factom-Asset-Tokens/fatd/internal/db/addresses" + "github.com/Factom-Asset-Tokens/fatd/internal/db/eblocks" + "github.com/Factom-Asset-Tokens/fatd/internal/db/entries" + "github.com/Factom-Asset-Tokens/fatd/internal/db/metadata" + "github.com/Factom-Asset-Tokens/fatd/internal/db/nftokens" +) + +type applyFunc func(*Chain, int64, factom.Entry) (txErr, err error) + +func (chain *Chain) Apply(dbKeyMR *factom.Bytes32, eb factom.EBlock) (err error) { + // Ensure entire EBlock is applied atomically. + defer sqlitex.Save(chain.Conn)(&err) + defer func(chainCopy Chain) { + if err != nil { + // Reset chain on error + *chain = chainCopy + } + }(*chain) + + chain.Head = eb + + // Insert latest EBlock. + if err = eblocks.Insert(chain.Conn, eb, dbKeyMR); err != nil { + return + } + + // Insert each entry and attempt to apply it... + for _, e := range eb.Entries { + if _, err = chain.ApplyEntry(e); err != nil { + return + } + } + return +} + +func (chain *Chain) ApplyEntry(e factom.Entry) (txErr, err error) { + ei, err := entries.Insert(chain.Conn, e, chain.Head.Sequence) + if err != nil { + return + } + return chain.apply(chain, ei, e) +} + +var alwaysRollbackErr = fmt.Errorf("always rollback") + +func (chain *Chain) applyIssuance(ei int64, e factom.Entry) (issueErr, err error) { + // The Identity must exist prior to issuance. + if !chain.Identity.IsPopulated() || e.Timestamp.Before(chain.Identity.Timestamp) { + issueErr = fmt.Errorf("Identity not set up prior to this Entry") + return + } + issuance, issueErr := fat.NewIssuance(e, (*factom.Bytes32)(chain.Identity.ID1Key)) + if issueErr != nil { + return + } + rollback := sqlitex.Save(chain.Conn) + chainCopy := *chain + defer func() { + if err != nil || issueErr != nil { + rollback(&alwaysRollbackErr) + // Reset chain on error + *chain = chainCopy + if err != nil { + return + } + //chain.Log.Debugf("Entry{%v}: invalid Issuance: %v", + // e.Hash, issueErr) + return + } + rollback(&err) // commit + //chain.Log.Debugf("Valid Issuance Entry: %v %+v", e.Hash, issuance) + }() + if err = metadata.SetInitEntryID(chain.Conn, ei); err != nil { + return + } + chain.Issuance = issuance + chain.setApplyFunc() + return +} + +func (chain *Chain) setApplyFunc() { + if !chain.Issuance.Entry.IsPopulated() { + chain.apply = func(chain *Chain, ei int64, e factom.Entry) ( + txErr, err error) { + txErr, err = chain.applyIssuance(ei, e) + return + } + return + } + // Adapt to match ApplyFunc. + switch chain.Issuance.Type { + case fat0.Type: + chain.apply = func(chain *Chain, ei int64, e factom.Entry) ( + txErr, err error) { + _, txErr, err = chain.ApplyFAT0Tx(ei, e) + return + } + case fat1.Type: + chain.apply = func(chain *Chain, ei int64, e factom.Entry) ( + txErr, err error) { + _, txErr, err = chain.ApplyFAT1Tx(ei, e) + return + } + default: + panic("invalid FAT type") + } +} + +func (chain *Chain) Save() func(txErr, err *error) { + rollback := sqlitex.Save(chain.Conn) + chainCopy := *chain + return func(txErr, err *error) { + //e := tx.FactomEntry() + if *err != nil || *txErr != nil { + rollback(&alwaysRollbackErr) + // Reset chain on error + *chain = chainCopy + if *err != nil { + return + } + return + } + rollback(err) + } +} + +func (chain *Chain) ApplyFAT0Tx(ei int64, e factom.Entry) (tx fat0.Transaction, + txErr, err error) { + + valid, err := entries.CheckUniquelyValid(chain.Conn, ei, e.Hash) + if err != nil { + return + } + if !valid { + txErr = fmt.Errorf("replay: hash previously marked valid") + return + } + + tx, txErr = fat0.NewTransaction(e, (*factom.Bytes32)(chain.Identity.ID1Key)) + if txErr != nil { + return + } + + defer chain.Save()(&txErr, &err) + + if err = entries.SetValid(chain.Conn, ei); err != nil { + return + } + + if tx.IsCoinbase() { + addIssued := tx.Inputs[fat.Coinbase()] + if chain.Issuance.Supply > 0 && + int64(chain.NumIssued+addIssued) > chain.Issuance.Supply { + txErr = fmt.Errorf("coinbase exceeds max supply") + return + } + if err = chain.addNumIssued(addIssued); err != nil { + return + } + if _, err = addresses.InsertTransactionRelation( + chain.Conn, 1, ei, false); err != nil { + return + } + } else { + for adr, amount := range tx.Inputs { + var ai int64 + ai, txErr, err = addresses.Sub(chain.Conn, &adr, amount) + if err != nil || txErr != nil { + return + } + if _, err = addresses.InsertTransactionRelation( + chain.Conn, ai, ei, false); err != nil { + return + } + } + } + + for adr, amount := range tx.Outputs { + var ai int64 + ai, err = addresses.Add(chain.Conn, &adr, amount) + if err != nil { + return + } + if _, err = addresses.InsertTransactionRelation( + chain.Conn, ai, ei, true); err != nil { + return + } + } + + return +} + +func (chain *Chain) ApplyFAT1Tx(ei int64, e factom.Entry) (tx fat1.Transaction, + txErr, err error) { + + valid, err := entries.CheckUniquelyValid(chain.Conn, ei, e.Hash) + if err != nil { + return + } + if !valid { + txErr = fmt.Errorf("replay: hash previously marked valid") + return + } + + tx, txErr = fat1.NewTransaction(e, (*factom.Bytes32)(chain.Identity.ID1Key)) + if txErr != nil { + return + } + + defer chain.Save()(&txErr, &err) + + if err = entries.SetValid(chain.Conn, ei); err != nil { + return + } + + if tx.IsCoinbase() { + nfTkns := tx.Inputs[fat.Coinbase()] + addIssued := uint64(len(nfTkns)) + if chain.Issuance.Supply > 0 && + int64(chain.NumIssued+addIssued) > chain.Issuance.Supply { + txErr = fmt.Errorf("coinbase exceeds max supply") + return + } + if err = chain.addNumIssued(addIssued); err != nil { + return + } + var adrTxID int64 + adrTxID, err = addresses.InsertTransactionRelation(chain.Conn, 1, ei, false) + if err != nil { + return + } + for nfID := range nfTkns { + // Insert the NFToken with the coinbase address as a + // placeholder for the owner. + txErr, err = nftokens.Insert(chain.Conn, nfID, 1, ei) + if err != nil || txErr != nil { + return + } + if err = nftokens.InsertTransactionRelation( + chain.Conn, nfID, adrTxID); err != nil { + return + } + metadata := tx.TokenMetadata[nfID] + if len(metadata) == 0 { + continue + } + if err = nftokens.SetMetadata( + chain.Conn, nfID, metadata); err != nil { + return + } + } + } else { + for adr, nfTkns := range tx.Inputs { + var ai int64 + ai, txErr, err = addresses.Sub( + chain.Conn, &adr, uint64(len(nfTkns))) + if err != nil || txErr != nil { + return + } + var adrTxID int64 + adrTxID, err = addresses.InsertTransactionRelation( + chain.Conn, ai, ei, false) + if err != nil { + return + } + for nfTkn := range nfTkns { + var ownerID int64 + ownerID, err = nftokens.SelectOwnerID(chain.Conn, nfTkn) + if err != nil { + return + } + if ownerID == -1 { + txErr = fmt.Errorf("no such NFToken{%v}", nfTkn) + return + } + if ownerID != ai { + txErr = fmt.Errorf("NFToken{%v} not owned by %v", + nfTkn, adr) + return + } + if err = nftokens.InsertTransactionRelation( + chain.Conn, nfTkn, adrTxID); err != nil { + return + } + } + } + } + + for adr, nfTkns := range tx.Outputs { + var ai int64 + ai, err = addresses.Add(chain.Conn, &adr, uint64(len(nfTkns))) + if err != nil { + return + } + var adrTxID int64 + adrTxID, err = addresses.InsertTransactionRelation( + chain.Conn, ai, ei, true) + if err != nil { + return + } + for nfID := range nfTkns { + if err = nftokens.SetOwner(chain.Conn, nfID, ai); err != nil { + return + } + if err = nftokens.InsertTransactionRelation( + chain.Conn, nfID, adrTxID); err != nil { + return + } + } + } + + return +} diff --git a/internal/db/chain.go b/internal/db/chain.go new file mode 100644 index 0000000..b177648 --- /dev/null +++ b/internal/db/chain.go @@ -0,0 +1,371 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package db + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + + "crawshaw.io/sqlite" + "crawshaw.io/sqlite/sqlitex" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/internal/db/addresses" + "github.com/Factom-Asset-Tokens/fatd/internal/db/eblocks" + "github.com/Factom-Asset-Tokens/fatd/internal/db/entries" + "github.com/Factom-Asset-Tokens/fatd/internal/db/metadata" + _log "github.com/Factom-Asset-Tokens/fatd/internal/log" +) + +var ( + log _log.Log +) + +const ( + dbDriver = "sqlite3" + dbFileExtension = ".sqlite3" + dbFileNameLen = len(factom.Bytes32{})*2 + len(dbFileExtension) + + PoolSize = 10 +) + +type Chain struct { + // General Factom Blockchain Data + ID *factom.Bytes32 + Head factom.EBlock + HeadDBKeyMR *factom.Bytes32 + NetworkID factom.NetworkID + SyncHeight uint32 + SyncDBKeyMR *factom.Bytes32 + + // FAT Specific Data + TokenID string + IssuerChainID *factom.Bytes32 + Identity factom.Identity + Issuance fat.Issuance + NumIssued uint64 + + DBFile string + Conn *sqlite.Conn // Read/Write + Pool *sqlitex.Pool // Read Only Pool + Log _log.Log + + apply applyFunc +} + +// dbPath must be path ending in os.Separator +func OpenNew(ctx context.Context, dbPath string, + dbKeyMR *factom.Bytes32, eb factom.EBlock, networkID factom.NetworkID, + identity factom.Identity) (chain Chain, err error) { + + fname := eb.ChainID.String() + dbFileExtension + path := dbPath + fname + + nameIDs := eb.Entries[0].ExtIDs + if !fat.ValidNameIDs(nameIDs) { + err = fmt.Errorf("invalid token chain Name IDs") + return + } + + // Ensure that the database file doesn't already exist. + _, err = os.Stat(path) + if err == nil { + err = fmt.Errorf("already exists: %v", path) + return + } + if !os.IsNotExist(err) { // Any other error is unexpected. + return + } + + chain.Conn, chain.Pool, err = OpenConnPool(ctx, dbPath+fname) + if err != nil { + return + } + defer func() { + if err != nil { + chain.Close() + if err := os.Remove(path); err != nil { + chain.Log.Errorf("os.Remove(): %w", err) + } + } + }() + chain.Log = _log.New("chain", strings.TrimRight(fname, dbFileExtension)) + chain.DBFile = fname + chain.ID = eb.ChainID + chain.IssuerChainID = new(factom.Bytes32) + chain.TokenID, *chain.IssuerChainID = fat.ParseTokenIssuer(nameIDs) + chain.HeadDBKeyMR = dbKeyMR + chain.Identity = identity + chain.SyncHeight = eb.Height + chain.SyncDBKeyMR = dbKeyMR + chain.NetworkID = networkID + + if err = metadata.Insert(chain.Conn, chain.SyncHeight, chain.SyncDBKeyMR, + chain.NetworkID, chain.Identity); err != nil { + return + } + + // Ensure that the coinbase address has rowid = 1. + coinbase := fat.Coinbase() + if _, err = addresses.Add(chain.Conn, &coinbase, 0); err != nil { + return + } + + chain.setApplyFunc() + if err = chain.Apply(dbKeyMR, eb); err != nil { + return + } + + return +} + +func Open(ctx context.Context, dbPath, fname string) (chain Chain, err error) { + chain.Log = _log.New("chain", strings.TrimRight(fname, dbFileExtension)) + chain.Log.Info("Opening...") + chain.Conn, chain.Pool, err = OpenConnPool(ctx, dbPath+fname) + if err != nil { + return + } + defer func() { + if err != nil { + chain.Close() + } + }() + chain.DBFile = fname + + err = chain.loadMetadata() + return +} + +func OpenAll(ctx context.Context, dbPath string) (chains []Chain, err error) { + log = _log.New("pkg", "db") + defer func() { + if err != nil { + for _, chain := range chains { + chain.Close() + } + chains = nil + } + }() + + // Scan through all files within the database directory. Ignore invalid + // file names. + files, err := ioutil.ReadDir(dbPath) + if err != nil { + return nil, fmt.Errorf("ioutil.ReadDir(%q): %w", dbPath, err) + } + chains = make([]Chain, 0, len(files)) + for _, f := range files { + fname := f.Name() + chainID, err := fnameToChainID(fname) + if err != nil { + continue + } + chain, err := Open(ctx, dbPath, fname) + if err != nil { + return nil, err + } + chains = append(chains, chain) + if *chainID != *chain.ID { + return nil, fmt.Errorf( + "filename %v does not match database Chain ID %v", + fname, chain.ID) + } + } + return chains, nil +} +func fnameToChainID(fname string) (*factom.Bytes32, error) { + invalidFName := fmt.Errorf("invalid filename: %v", fname) + if len(fname) != dbFileNameLen || + fname[dbFileNameLen-len(dbFileExtension):dbFileNameLen] != + dbFileExtension { + return nil, invalidFName + } + chainID := factom.NewBytes32(fname[0:64]) + if chainID.IsZero() { + return nil, invalidFName + } + return &chainID, nil +} + +func OpenConnPool(ctx context.Context, dbURI string) ( + conn *sqlite.Conn, pool *sqlitex.Pool, err error) { + + const baseFlags = sqlite.SQLITE_OPEN_WAL | + sqlite.SQLITE_OPEN_URI | + sqlite.SQLITE_OPEN_NOMUTEX + flags := baseFlags | sqlite.SQLITE_OPEN_READWRITE | sqlite.SQLITE_OPEN_CREATE + if conn, err = sqlite.OpenConn(dbURI, flags); err != nil { + err = fmt.Errorf("sqlite.OpenConn(%q, %x): %w", dbURI, flags, err) + return + } + defer func() { + if err != nil { + if err := conn.Close(); err != nil { + log.Error(err) + } + } + }() + conn.SetInterrupt(ctx.Done()) + if err = validateOrApplySchema(conn, chainDBSchema); err != nil { + return + } + if err = sqlitex.ExecScript(conn, `PRAGMA foreign_keys = ON;`); err != nil { + return + } + + // Snapshots are unreliable if auto checkpointing is enabled. So we + // manually checkpoint in the engine every new EBlock and in Close. + if err = sqlitex.ExecScript(conn, + `PRAGMA wal_autocheckpoint = 0;`); err != nil { + return + } + + // Ensure WAL file is created and ready for snapshots by ensuring at + // least one transaction exists in the WAL. + var uv int + if err = sqlitex.Exec(conn, `PRAGMA user_version;`, + func(stmt *sqlite.Stmt) error { + uv = stmt.ColumnInt(0) + return nil + }); err != nil { + return + } + if err = sqlitex.ExecScript(conn, + fmt.Sprintf(`PRAGMA user_version = %v;`, uv)); err != nil { + return + } + + // Open pool + flags = baseFlags | sqlite.SQLITE_OPEN_READONLY + if pool, err = sqlitex.Open(dbURI, flags, PoolSize); err != nil { + err = fmt.Errorf("sqlitex.Open(%q, %x, %v): %w", + dbURI, flags, PoolSize, err) + return + } + defer func() { + if err != nil { + if err := pool.Close(); err != nil { + log.Error(err) + } + } + }() + + // Prime pool for snapshot reads. + for i := 0; i < PoolSize; i++ { + c := pool.Get(nil) + defer pool.Put(c) + if err = sqlitex.ExecScript(c, "PRAGMA application_id;"); err != nil { + return + } + } + return +} + +// Close all database connections. Log any errors. +func (chain *Chain) Close() { + if err := chain.Pool.Close(); err != nil { + chain.Log.Errorf("chain.Pool.Close(): %v", err) + } + chain.Conn.SetInterrupt(nil) + if err := sqlitex.ExecScript(chain.Conn, + `PRAGMA wal_checkpoint;`); err != nil { + chain.Log.Error(err) + } + // Close this last so that the wal and shm files are removed. + if err := chain.Conn.Close(); err != nil { + chain.Log.Errorf("chain.Conn.Close(): %v", err) + } +} + +func (chain *Chain) LatestEntryTimestamp() time.Time { + entries := chain.Head.Entries + lastID := len(entries) - 1 + return entries[lastID].Timestamp +} + +func (chain *Chain) SetSync(height uint32, dbKeyMR *factom.Bytes32) error { + if height <= chain.SyncHeight { + return nil + } + if err := metadata.SetSync(chain.Conn, height, dbKeyMR); err != nil { + return err + } + chain.SyncHeight = height + chain.SyncDBKeyMR = dbKeyMR + return nil +} + +func (chain *Chain) addNumIssued(add uint64) error { + if err := metadata.AddNumIssued(chain.Conn, add); err != nil { + return err + } + chain.NumIssued += add + return nil +} + +func (chain *Chain) loadMetadata() error { + defer chain.setApplyFunc() + // Load NameIDs + first, err := entries.SelectByID(chain.Conn, 1) + if err != nil { + return err + } + if !first.IsPopulated() { + return fmt.Errorf("no first entry") + } + + nameIDs := first.ExtIDs + if !fat.ValidNameIDs(nameIDs) { + return fmt.Errorf("invalid token chain Name IDs") + } + chain.IssuerChainID = new(factom.Bytes32) + chain.TokenID, *chain.IssuerChainID = fat.ParseTokenIssuer(nameIDs) + + // Load Chain Head + eb, dbKeyMR, err := eblocks.SelectLatest(chain.Conn) + if err != nil { + return err + } + if !eb.IsPopulated() { + // A database must always have at least one EBlock. + return fmt.Errorf("no eblock in database") + } + chain.Head = eb + chain.HeadDBKeyMR = &dbKeyMR + chain.ID = eb.ChainID + + chain.SyncHeight, chain.NumIssued, chain.SyncDBKeyMR, + chain.NetworkID, chain.Identity, + chain.Issuance, err = metadata.Select(chain.Conn) + if err != nil { + return err + } + + return err +} diff --git a/factom/heights_test.go b/internal/db/chain_test.go similarity index 69% rename from factom/heights_test.go rename to internal/db/chain_test.go index 8a2e43b..32891aa 100644 --- a/factom/heights_test.go +++ b/internal/db/chain_test.go @@ -20,23 +20,27 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package factom +package db import ( + "context" "testing" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestHeights(t *testing.T) { - var h Heights - assert := assert.New(t) - c := NewClient() - err := h.Get(c) - assert.NoError(err) - zero := uint64(0) - assert.NotEqual(zero, h.DirectoryBlock) - assert.NotEqual(zero, h.Leader) - assert.NotEqual(zero, h.EntryBlock) - assert.NotEqual(zero, h.Entry) +func TestChainValidate(t *testing.T) { + require := require.New(t) + flag.LogDebug = true + chains, err := OpenAll(context.Background(), "./test-fatd.db/") + require.NoError(err, "OpenAll()") + require.NotEmptyf(chains, "Test database is empty: %v", flag.DBPath) + + for _, chain := range chains { + chain := chain + defer chain.Close() + assert.NoErrorf(t, chain.Validate(), "Chain{%v}.Validate()", chain.ID) + } } diff --git a/internal/db/eblocks/eblocks.go b/internal/db/eblocks/eblocks.go new file mode 100644 index 0000000..a240d44 --- /dev/null +++ b/internal/db/eblocks/eblocks.go @@ -0,0 +1,186 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +// Package eblocks provides functions and SQL framents for working with the +// "eblocks" table, which stores factom.EBlock. +package eblocks + +import ( + "fmt" + "time" + + "crawshaw.io/sqlite" + "github.com/Factom-Asset-Tokens/factom" +) + +// CreateTable is a SQL string that creates the "eblocks" table. +const CreateTable = `CREATE TABLE "eblocks" ( + "seq" INTEGER PRIMARY KEY, + "key_mr" BLOB NOT NULL UNIQUE, + "db_height" INTEGER NOT NULL UNIQUE, + "db_key_mr" BLOB NOT NULL UNIQUE, + "timestamp" INTEGER NOT NULL, + "data" BLOB NOT NULL +); +` + +// Insert eb into the "eblocks" table with dbKeyMR. +func Insert(conn *sqlite.Conn, eb factom.EBlock, dbKeyMR *factom.Bytes32) error { + // Ensure that this is the next EBlock. + prevKeyMR, err := SelectKeyMR(conn, eb.Sequence-1) + if *eb.PrevKeyMR != prevKeyMR { + return fmt.Errorf("invalid EBlock.PrevKeyMR") + } + + var data []byte + data, err = eb.MarshalBinary() + if err != nil { + panic(fmt.Errorf("factom.EBlock.MarshalBinary(): %w", err)) + } + stmt := conn.Prep(`INSERT INTO "eblocks" + ("seq", "key_mr", "db_height", "db_key_mr", "timestamp", "data") + VALUES (?, ?, ?, ?, ?, ?);`) + stmt.BindInt64(1, int64(eb.Sequence)) + stmt.BindBytes(2, eb.KeyMR[:]) + stmt.BindInt64(3, int64(eb.Height)) + stmt.BindBytes(4, dbKeyMR[:]) + stmt.BindInt64(5, eb.Timestamp.Unix()) + stmt.BindBytes(6, data) + + _, err = stmt.Step() + return err +} + +// SelectWhere is a SQL fragment for retrieving rows from the "eblocks" table +// with Select(). +const SelectWhere = `SELECT "key_mr", "data", "timestamp" FROM "eblocks" WHERE ` + +// Select the next factom.EBlock from the given prepared Stmt. +// +// The Stmt must be created with a SQL string starting with SelectWhere. +func Select(stmt *sqlite.Stmt) (factom.EBlock, error) { + var eb factom.EBlock + hasRow, err := stmt.Step() + if err != nil { + return eb, err + } + if !hasRow { + return eb, nil + } + + eb.KeyMR = new(factom.Bytes32) + if stmt.ColumnBytes(0, eb.KeyMR[:]) != len(eb.KeyMR) { + panic("invalid key_mr length") + } + + // Load timestamp so that entries have correct timestamps. + eb.Timestamp = time.Unix(stmt.ColumnInt64(2), 0) + + data := make([]byte, stmt.ColumnLen(1)) + stmt.ColumnBytes(1, data) + if err := eb.UnmarshalBinary(data); err != nil { + panic(fmt.Errorf("factom.EBlock.UnmarshalBinary(%v): %w", + factom.Bytes(data), err)) + } + + return eb, nil +} + +// SelectByHeight returns the factom.EBlock with the given height. +func SelectByHeight(conn *sqlite.Conn, height uint32) (factom.EBlock, error) { + stmt := conn.Prep(SelectWhere + `"db_height" = ?;`) + stmt.BindInt64(1, int64(height)) + defer stmt.Reset() + return Select(stmt) +} + +// SelectBySequence returns the factom.EBlock with sequence seq. +func SelectBySequence(conn *sqlite.Conn, seq uint32) (factom.EBlock, error) { + stmt := conn.Prep(SelectWhere + `"seq" = ?;`) + stmt.BindInt64(1, int64(seq)) + defer stmt.Reset() + return Select(stmt) +} + +// SelectKeyMR returns the KeyMR for the EBlock with sequence seq. +func SelectKeyMR(conn *sqlite.Conn, seq uint32) (factom.Bytes32, error) { + var keyMR factom.Bytes32 + stmt := conn.Prep(`SELECT "key_mr" FROM "eblocks" WHERE "seq" = ?;`) + stmt.BindInt64(1, int64(int32(seq))) // Preserve uint32(-1) as -1 + hasRow, err := stmt.Step() + defer stmt.Reset() + if err != nil { + return keyMR, err + } + if !hasRow { + return keyMR, fmt.Errorf("no such EBlock{Sequence: %v}", seq) + } + + if stmt.ColumnBytes(0, keyMR[:]) != len(keyMR) { + panic("invalid key_mr length") + } + + return keyMR, nil +} + +// SelectDBKeyMR returns the DBKeyMR for the EBlock with sequence seq. +func SelectDBKeyMR(conn *sqlite.Conn, seq uint32) (factom.Bytes32, error) { + var dbKeyMR factom.Bytes32 + stmt := conn.Prep(`SELECT "db_key_mr" FROM "eblocks" WHERE "seq" = ?;`) + stmt.BindInt64(1, int64(int32(seq))) // Preserve uint32(-1) as -1 + hasRow, err := stmt.Step() + defer stmt.Reset() + if err != nil { + return dbKeyMR, err + } + if !hasRow { + return dbKeyMR, fmt.Errorf("no such EBlock{Sequence: %v}", seq) + } + + if stmt.ColumnBytes(0, dbKeyMR[:]) != len(dbKeyMR) { + panic("invalid key_mr length") + } + + return dbKeyMR, nil +} + +// SelectLatest returns the most recent factom.EBlock. +func SelectLatest(conn *sqlite.Conn) (factom.EBlock, factom.Bytes32, error) { + var dbKeyMR factom.Bytes32 + stmt := conn.Prep( + `SELECT "key_mr", "data", "timestamp", "db_key_mr" FROM "eblocks" + WHERE "seq" = (SELECT max("seq") FROM "eblocks");`) + eb, err := Select(stmt) + defer stmt.Reset() + if err != nil { + return eb, dbKeyMR, err + } + if !eb.IsPopulated() { + panic("no EBlocks") + } + + if stmt.ColumnBytes(3, dbKeyMR[:]) != len(dbKeyMR) { + panic("invalid db_key_mr length") + } + + return eb, dbKeyMR, nil +} diff --git a/internal/db/entries/entries.go b/internal/db/entries/entries.go new file mode 100644 index 0000000..16dcca9 --- /dev/null +++ b/internal/db/entries/entries.go @@ -0,0 +1,291 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +// Package entries provides functions and SQL framents for working with the +// "entries" table, which stores factom.Entry with a valid flag. +package entries + +import ( + "fmt" + "strings" + "time" + + "crawshaw.io/sqlite" + "crawshaw.io/sqlite/sqlitex" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat1" + "github.com/Factom-Asset-Tokens/fatd/internal/db/sqlbuilder" +) + +// CreateTable is a SQL string that creates the "entries" table. +// +// The "entries" table has a foreign key reference to the "eblocks" table, +// which must exist first. +const CreateTable = `CREATE TABLE "entries" ( + "id" INTEGER PRIMARY KEY, + "eb_seq" INTEGER NOT NULL, + "timestamp" INTEGER NOT NULL, + "valid" BOOL NOT NULL DEFAULT FALSE, + "hash" BLOB NOT NULL, + "data" BLOB NOT NULL, + + FOREIGN KEY("eb_seq") REFERENCES "eblocks" +); +CREATE INDEX "idx_entries_eb_seq" ON "entries"("eb_seq"); +CREATE INDEX "idx_entries_hash" ON "entries"("hash"); +` + +// Insert e into the "entries" table with the EBlock reference ebSeq. If +// successful, the new row id of e is returned. +func Insert(conn *sqlite.Conn, e factom.Entry, ebSeq uint32) (int64, error) { + data, err := e.MarshalBinary() + if err != nil { + panic(fmt.Errorf("factom.Entry.MarshalBinary(): %w", err)) + } + + stmt := conn.Prep(`INSERT INTO "entries" + ("eb_seq", "timestamp", "hash", "data") + VALUES (?, ?, ?, ?);`) + stmt.BindInt64(1, int64(int32(ebSeq))) // Preserve uint32(-1) as -1 + stmt.BindInt64(2, int64(e.Timestamp.Unix())) + stmt.BindBytes(3, e.Hash[:]) + stmt.BindBytes(4, data) + + if _, err := stmt.Step(); err != nil { + return -1, err + } + return conn.LastInsertRowID(), nil +} + +// SetValid marks the entry valid at the id'th row of the "entries" table. +func SetValid(conn *sqlite.Conn, id int64) error { + stmt := conn.Prep(`UPDATE "entries" SET "valid" = 1 WHERE "id" = ?;`) + stmt.BindInt64(1, id) + _, err := stmt.Step() + if err != nil { + return err + } + if conn.Changes() == 0 { + panic("no entries updated") + } + return nil +} + +// SelectWhere is a SQL fragment for retrieving rows from the "entries" table +// with Select(). +const SelectWhere = `SELECT "hash", "data", "timestamp" FROM "entries" WHERE ` + +// Select the next factom.Entry from the given prepared Stmt. +// +// The Stmt must be created with a SQL string starting with SelectWhere. +func Select(stmt *sqlite.Stmt) (factom.Entry, error) { + var e factom.Entry + hasRow, err := stmt.Step() + if err != nil { + return e, err + } + if !hasRow { + return e, nil + } + + e.Hash = new(factom.Bytes32) + if stmt.ColumnBytes(0, e.Hash[:]) != len(e.Hash) { + panic("invalid hash length") + } + + data := make([]byte, stmt.ColumnLen(1)) + stmt.ColumnBytes(1, data) + if err := e.UnmarshalBinary(data); err != nil { + panic(fmt.Errorf("factom.Entry.UnmarshalBinary(%v): %w", + factom.Bytes(data), err)) + } + + e.Timestamp = time.Unix(stmt.ColumnInt64(2), 0) + + return e, nil +} + +// SelectByID returns the factom.Entry at row id. +func SelectByID(conn *sqlite.Conn, id int64) (factom.Entry, error) { + stmt := conn.Prep(SelectWhere + `"id" = ?;`) + stmt.BindInt64(1, id) + defer stmt.Reset() + return Select(stmt) +} + +// SelectByHash returns the first factom.Entry with hash. +func SelectByHash(conn *sqlite.Conn, hash *factom.Bytes32) (factom.Entry, error) { + stmt := conn.Prep(SelectWhere + `"hash" = ?;`) + stmt.BindBytes(1, hash[:]) + defer stmt.Reset() + return Select(stmt) +} + +// SelectValidByHash returns the first valid factom.Entry with hash. +func SelectValidByHash(conn *sqlite.Conn, hash *factom.Bytes32) (factom.Entry, error) { + stmt := conn.Prep(SelectWhere + `"hash" = ? AND "valid" = true;`) + stmt.BindBytes(1, hash[:]) + defer stmt.Reset() + return Select(stmt) +} + +// SelectCount returns the total number of rows in the "entries" table. If +// validOnly is true, only the rows where "valid" = true are counted. +func SelectCount(conn *sqlite.Conn, validOnly bool) (int64, error) { + stmt := conn.Prep(`SELECT count(*) FROM "entries" WHERE (? OR "valid" = true);`) + stmt.BindBool(1, !validOnly) + return sqlitex.ResultInt64(stmt) +} + +// SelectByAddress returns all the factom.Entry where adrs and nfTkns were +// involved in the valid transaction, for the given pagination range. +// +// Pages start at 1. +// +// TODO: This should probably be moved out of the entries package and into a db +// package that is more specific to FAT0 and FAT1. +func SelectByAddress(conn *sqlite.Conn, startHash *factom.Bytes32, + adrs []factom.FAAddress, nfTkns fat1.NFTokens, + toFrom, order string, + page, limit uint) ([]factom.Entry, error) { + if page == 0 { + return nil, fmt.Errorf("invalid page") + } + var sql sqlbuilder.SQLBuilder + sql.Append(SelectWhere + `"valid" = true`) + if startHash != nil { + sql.Append(` AND "id" >= (SELECT "id" FROM "entries" WHERE "hash" = ?)`, + func(s *sqlite.Stmt, p int) int { + s.BindBytes(p, startHash[:]) + return 1 + }) + } + var to bool + switch strings.ToLower(toFrom) { + case "to": + to = true + case "from", "": + default: + panic(fmt.Errorf("invalid toFrom: %v", toFrom)) + } + if len(nfTkns) > 0 { + sql.WriteString(` AND "id" IN ( + SELECT "entry_id" FROM "nf_token_address_transactions" + WHERE "nf_tkn_id" IN (`) // 2 open ( + sql.BindNParams(len(nfTkns), func(s *sqlite.Stmt, p int) int { + i := 0 + for nfTkn := range nfTkns { + s.BindInt64(p+i, int64(nfTkn)) + i++ + } + return len(nfTkns) + }) + sql.WriteString(`)`) // 1 open ( + if len(adrs) > 0 { + sql.WriteString(` AND "address_id" IN ( + SELECT "id" FROM "addresses" + WHERE "address" IN (`) // 3 open ( + sql.BindNParams(len(adrs), func(s *sqlite.Stmt, p int) int { + for i, adr := range adrs { + s.BindBytes(p+i, adr[:]) + } + return len(adrs) + }) + sql.WriteString(`))`) // 1 open ( + } + if len(toFrom) > 0 { + sql.Append(` AND "to" = ?`, func(s *sqlite.Stmt, p int) int { + s.BindBool(p, to) + return 1 + }) + } + sql.WriteString(`)`) // 0 open { + } else if len(adrs) > 0 { + sql.WriteString(` AND "id" IN ( + SELECT "entry_id" FROM "address_transactions" + WHERE "address_id" IN ( + SELECT "id" FROM "addresses" + WHERE "address" IN (`) // 3 open ( + + sql.BindNParams(len(adrs), func(s *sqlite.Stmt, p int) int { + for i, adr := range adrs { + s.BindBytes(p+i, adr[:]) + } + return len(adrs) + }) + sql.WriteString(`))`) // 1 open ( + if len(toFrom) > 0 { + sql.Append(` AND "to" = ?`, func(s *sqlite.Stmt, p int) int { + s.BindBool(p, to) + return 1 + }) + } + sql.WriteString(`)`) // 0 open ( + } + + sql.OrderByPaginate("id", order, page, limit) + + stmt := sql.Prep(conn) + defer stmt.Reset() + + var entries []factom.Entry + for { + e, err := Select(stmt) + if err != nil { + return nil, err + } + if !e.IsPopulated() { + break + } + entries = append(entries, e) + } + + return entries, nil +} + +// CheckUniquelyValid returns true if there are no valid entries earlier than +// id that have the same hash. If id is 0, then all entries are checked. +func CheckUniquelyValid(conn *sqlite.Conn, + id int64, hash *factom.Bytes32) (bool, error) { + stmt := conn.Prep(`SELECT count(*) FROM "entries" WHERE + "valid" = true AND (? OR "id" < ?) AND "hash" = ?;`) + stmt.BindBool(1, id > 0) + stmt.BindInt64(2, id) + stmt.BindBytes(3, hash[:]) + val, err := sqlitex.ResultInt(stmt) + if err != nil { + return false, err + } + return val == 0, nil +} + +// SelectLatestValid returns the most recent valid factom.Entry. +func SelectLatestValid(conn *sqlite.Conn) (factom.Entry, error) { + stmt := conn.Prep(SelectWhere + + `"id" = (SELECT max("id") FROM "entries" WHERE "valid" = true);`) + e, err := Select(stmt) + defer stmt.Reset() + if err != nil { + return e, err + } + return e, nil +} diff --git a/factom/gen.go b/internal/db/gen.go similarity index 83% rename from factom/gen.go rename to internal/db/gen.go index a34a7ec..ea6b241 100644 --- a/factom/gen.go +++ b/internal/db/gen.go @@ -20,8 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package factom +package db -//go:generate go run ./genmain.go -//go:generate goimports -w ./idkey_gen.go ./idkey_gen_test.go -//go:generate gofmt -s -w ./idkey_gen.go ./idkey_gen_test.go +//go:generate go run ./gentestdb.go -chainid b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb +//go:generate go run ./gentestdb.go -chainid 0692c0f9c3171575cf53d6d8067139bad3f56169f94966341018b1950542f3dd diff --git a/internal/db/gentestdb.go b/internal/db/gentestdb.go new file mode 100644 index 0000000..e38a53d --- /dev/null +++ b/internal/db/gentestdb.go @@ -0,0 +1,119 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +// +build ignore + +package main + +import ( + "context" + "flag" + "fmt" + "log" + "os" + + . "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/internal/db" + fflag "github.com/Factom-Asset-Tokens/fatd/internal/flag" +) + +func init() { + log.SetFlags(log.Lshortfile) + fflag.DBPath = "./test-fatd.db/" + fflag.LogDebug = true +} + +func main() { + if err := os.Mkdir(fflag.DBPath, 0755); err != nil { + if !os.IsExist(err) { + log.Fatalf("os.Mkdir(%#v): %v", fflag.DBPath, err) + } + } + c := NewClient() + chainID := NewBytes32FromString( + "b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb") + flag.Var(chainID, "chainid", "Chain ID to use for the test database") + flag.StringVar(&c.FactomdServer, "factomd", c.FactomdServer, "factomd endpoint") + flag.Parse() + + log.SetPrefix(fmt.Sprintf("ChainID: %v ", chainID.String())) + + eblocks, err := EBlock{ChainID: chainID}.GetPrevAll(context.Background(), c) + if err != nil { + log.Fatal(err) + } + + first := eblocks[len(eblocks)-1] + var dblock DBlock + dblock.Height = first.Height + if err := dblock.Get(context.Background(), c); err != nil { + log.Fatal(err) + } + timestamp := dblock.Timestamp + for i := range first.Entries { + e := &first.Entries[i] + if err := e.Get(context.Background(), c); err != nil { + log.Fatal(err) + } + e.Timestamp = timestamp.Add(e.Timestamp.Sub(first.Timestamp)) + } + first.Timestamp = timestamp + + nameIDs := first.Entries[0].ExtIDs + + if !fat.ValidTokenNameIDs(nameIDs) { + log.Fatalf("invalid token chain") + } + _, identityChainID := fat.TokenIssuer(nameIDs) + identity := NewIdentity(&identityChainID) + if err := identity.Get(context.Background(), c); err != nil { + log.Fatal(err) + } + + // We don't need the actual dbKeyMR + chain, err := db.OpenNew(context.Background(), + fflag.DBPath, dblock.KeyMR, first, MainnetID(), identity) + if err != nil { + log.Println(err) + return + } + defer chain.Close() + + eblocks = eblocks[:len(eblocks)-1] // skip first eblock + for i := range eblocks { + eb := eblocks[len(eblocks)-i-1] + if err := eb.GetEntries(context.Background(), c); err != nil { + log.Fatal(err) + } + var dblock DBlock + dblock.Height = eb.Height + if err := dblock.Get(context.Background(), c); err != nil { + log.Fatal(err) + } + eb.SetTimestamp(dblock.Timestamp) + + if err := chain.Apply(dblock.KeyMR, eb); err != nil { + log.Fatal(err) + } + } +} diff --git a/internal/db/metadata/metadata.go b/internal/db/metadata/metadata.go new file mode 100644 index 0000000..02d2290 --- /dev/null +++ b/internal/db/metadata/metadata.go @@ -0,0 +1,184 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +// Package metadata provides functions and SQL framents for working with the +// "metadata" table, which stores the sync height, sync DBKeyMR, +// factom.NetworkID, and factom.Identity. +package metadata + +import ( + "fmt" + + "crawshaw.io/sqlite" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/internal/db/entries" +) + +// CreateTable is a SQL string that creates the "metadata" table. +// +// The "metadata" table has a foreign key reference to the "entries" table, +// which must exist first. +const CreateTable = `CREATE TABLE "metadata" ( + "id" INTEGER PRIMARY KEY, + "sync_height" INTEGER NOT NULL, + "sync_db_key_mr" BLOB NOT NULL, + "network_id" BLOB NOT NULL, + "id_key_entry" BLOB, + "id_key_height" INTEGER, + + "init_entry_id" INTEGER, + "num_issued" INTEGER, + + FOREIGN KEY("init_entry_id") REFERENCES "entries" +); +` + +// Insert the syncHeight, syncDBKeyMR, networkID, and if populated, the +// identity into the first row of the "metadata" table. This may only ever be +// called once for a given database. +func Insert(conn *sqlite.Conn, syncHeight uint32, syncDBKeyMR *factom.Bytes32, + networkID factom.NetworkID, identity factom.Identity) error { + stmt := conn.Prep(`INSERT INTO "metadata" + ("id", "sync_height", "sync_db_key_mr", + "network_id", "id_key_entry", "id_key_height") + VALUES (0, ?, ?, ?, ?, ?);`) + stmt.BindInt64(1, int64(syncHeight)) + stmt.BindBytes(2, syncDBKeyMR[:]) + stmt.BindBytes(3, networkID[:]) + if identity.IsPopulated() { + data, err := identity.MarshalBinary() + if err != nil { + return err + } + stmt.BindBytes(4, data) + stmt.BindInt64(5, int64(identity.Height)) + } else { + stmt.BindNull(4) + stmt.BindNull(5) + } + _, err := stmt.Step() + return err +} + +// SetSync updates the "sync_height" and "sync_db_key_mr". +func SetSync(conn *sqlite.Conn, height uint32, dbKeyMR *factom.Bytes32) error { + stmt := conn.Prep(`UPDATE "metadata" SET + ("sync_height", "sync_db_key_mr") = (?, ?);`) + stmt.BindInt64(1, int64(height)) + stmt.BindBytes(2, dbKeyMR[:]) + _, err := stmt.Step() + if err != nil && conn.Changes() != 1 { + panic(fmt.Errorf("expected exactly 1 change but got %v", + conn.Changes())) + } + return err +} + +// SetInitEntryID updates the "init_entry_id" +func SetInitEntryID(conn *sqlite.Conn, id int64) error { + stmt := conn.Prep(`UPDATE "metadata" SET + ("init_entry_id", "num_issued") = (?, 0);`) + stmt.BindInt64(1, id) + _, err := stmt.Step() + if err != nil && conn.Changes() != 1 { + panic(fmt.Errorf("expected exactly 1 change but got %v", + conn.Changes())) + } + return err +} + +func AddNumIssued(conn *sqlite.Conn, add uint64) error { + stmt := conn.Prep(`UPDATE "metadata" SET + "num_issued" = "num_issued" + ?;`) + stmt.BindInt64(1, int64(add)) + _, err := stmt.Step() + if err != nil && conn.Changes() != 1 { + panic(fmt.Errorf("expected exactly 1 change but got %v", + conn.Changes())) + } + return err +} + +func Select(conn *sqlite.Conn) (syncHeight uint32, numIssued uint64, + syncDBKeyMR *factom.Bytes32, + networkID factom.NetworkID, + identity factom.Identity, + issuance fat.Issuance, + err error) { + stmt := conn.Prep(`SELECT "sync_height", "sync_db_key_mr", "network_id", + "id_key_entry", "id_key_height", "init_entry_id", "num_issued" + FROM "metadata";`) + hasRow, err := stmt.Step() + defer stmt.Reset() + if err != nil { + return + } + if !hasRow { + err = fmt.Errorf("no saved metadata") + return + } + + syncHeight = uint32(stmt.ColumnInt64(0)) + + syncDBKeyMR = new(factom.Bytes32) + if stmt.ColumnBytes(1, syncDBKeyMR[:]) != len(syncDBKeyMR) { + panic("invalid sync_db_key_mr length") + } + + if stmt.ColumnBytes(2, networkID[:]) != len(networkID) { + panic("invalid network_id length") + } + + // Load chain.Identity... + if stmt.ColumnType(3) == sqlite.SQLITE_NULL { + // No Identity, therefore no Issuance. + return + } + idKeyEntryData := make(factom.Bytes, stmt.ColumnLen(3)) + stmt.ColumnBytes(3, idKeyEntryData) + if err = identity.UnmarshalBinary(idKeyEntryData); err != nil { + err = fmt.Errorf("identity.UnmarshalBinary(): %w", err) + return + } + identity.Height = uint32(stmt.ColumnInt64(4)) + + // Load chain.Issuance... + if stmt.ColumnType(5) == sqlite.SQLITE_NULL { + // No issuance entry so far... + return + } + initEntryID := stmt.ColumnInt64(5) + e, err := entries.SelectByID(conn, initEntryID) + if err != nil { + return + } + + issuance, err = fat.NewIssuance(e, (*factom.Bytes32)(identity.ID1Key)) + if err != nil { + return + } + + numIssued = uint64(stmt.ColumnInt64(6)) + + return +} diff --git a/internal/db/nftokens/nftokens.go b/internal/db/nftokens/nftokens.go new file mode 100644 index 0000000..eca5c25 --- /dev/null +++ b/internal/db/nftokens/nftokens.go @@ -0,0 +1,246 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +// Package nftokens provides functions and SQL framents for working with the +// "nf_tokens" table, which stores fat.NFToken with owner, creation id, and +// metadata. +package nftokens + +import ( + "encoding/json" + "fmt" + + "crawshaw.io/sqlite" + "crawshaw.io/sqlite/sqlitex" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat1" + "github.com/Factom-Asset-Tokens/fatd/internal/db/sqlbuilder" +) + +// CreateTable is a SQL string that creates the "nf_tokens" table. +// +// The "nf_tokens" table has foreign key references to the "entries" and +// "addresses" tables, which must exist first. +const CreateTable = `CREATE TABLE "nf_tokens" ( + "id" INTEGER PRIMARY KEY, + "metadata" BLOB, + "creation_entry_id" INTEGER NOT NULL, + "owner_id" INTEGER NOT NULL, + + FOREIGN KEY("creation_entry_id") REFERENCES "entries", + FOREIGN KEY("owner_id") REFERENCES "addresses" +); +CREATE INDEX "idx_nf_tokens_metadata" ON nf_tokens("metadata"); +CREATE INDEX "idx_nf_tokens_owner_id" ON nf_tokens("owner_id"); +CREATE VIEW "nf_tokens_addresses" AS + SELECT "nf_tokens"."id" AS "id", + "metadata", + "hash" AS "creation_hash", + "address" AS "owner" FROM + "nf_tokens", "addresses", "entries" ON + "owner_id" = "addresses"."id" AND + "creation_entry_id" = "entries"."id"; +` + +// Insert a new NFToken with "owner_id" set to the "addresses" foreign key +// adrID and the "creation_entry_id" set to the "entries" foreign key entryID. +func Insert(conn *sqlite.Conn, nfID fat1.NFTokenID, adrID, entryID int64) (error, error) { + stmt := conn.Prep(`INSERT INTO "nf_tokens" + ("id", "owner_id", "creation_entry_id") VALUES (?, ?, ?);`) + stmt.BindInt64(1, int64(nfID)) + stmt.BindInt64(2, adrID) + stmt.BindInt64(3, entryID) + if _, err := stmt.Step(); err != nil { + if sqlite.ErrCode(err) == sqlite.SQLITE_CONSTRAINT_PRIMARYKEY { + return fmt.Errorf("NFTokenID{%v} already exists", nfID), nil + } + return nil, err + } + return nil, nil +} + +// SetOwner updates the "owner_id" of the given nfID to the given adrID. +// +// If the given adrID does not exist, a foreign key constraint error will be +// returned. If the nfID does not exist, this will panic. +// +// TODO: consider that the use of panic is inconsistent here. This function +// should never be called on an adrID that does not exist. Should it also panic +// on that constraint error too? Both reflect program integrity issues. +func SetOwner(conn *sqlite.Conn, nfID fat1.NFTokenID, adrID int64) error { + stmt := conn.Prep(`UPDATE "nf_tokens" SET "owner_id" = ? WHERE "id" = ?;`) + stmt.BindInt64(1, adrID) + stmt.BindInt64(2, int64(nfID)) + _, err := stmt.Step() + if conn.Changes() == 0 { + panic("no NFTokenID updated") + } + return err +} + +// SetMetadata updates the "metadata" to metadata for a given nfID. +// +// If the nfID does not exist, this will panic. +func SetMetadata(conn *sqlite.Conn, nfID fat1.NFTokenID, metadata json.RawMessage) error { + stmt := conn.Prep(`UPDATE "nf_tokens" SET "metadata" = ? WHERE "id" = ?;`) + stmt.BindBytes(1, metadata) + stmt.BindInt64(2, int64(nfID)) + _, err := stmt.Step() + if conn.Changes() == 0 { + // This must only be called after the nfID has been inserted. + panic("no NFTokenID updated") + } + return err +} + +// SelectOwnerID returns the "owner_id" for the given nfID. +// +// If the nfID does not yet exist, (-1, nil) is returned. +func SelectOwnerID(conn *sqlite.Conn, nfID fat1.NFTokenID) (int64, error) { + stmt := conn.Prep(`SELECT "owner_id" FROM "nf_tokens" WHERE "id" = ?;`) + stmt.BindInt64(1, int64(nfID)) + ownerID, err := sqlitex.ResultInt64(stmt) + if err != nil && err.Error() == "sqlite: statement has no results" { + return -1, nil + } + if err != nil { + return -1, err + } + return ownerID, nil +} + +// SelectData returns the owner address, the creation entry hash, and the +// NFToken metadata for the given nfID +// +// If the nfID doesn't exist, all zero values are returned. Namely, check +// IsZero on the returned creation entry hash. +func SelectData(conn *sqlite.Conn, nfID fat1.NFTokenID) ( + factom.FAAddress, factom.Bytes32, []byte, error) { + + var owner factom.FAAddress + var creationHash factom.Bytes32 + stmt := conn.Prep(`SELECT "owner", "metadata", "creation_hash" + FROM "nf_tokens_addresses" WHERE "id" = ?;`) + stmt.BindInt64(1, int64(nfID)) + hasRow, err := stmt.Step() + defer stmt.Reset() + if err != nil { + return owner, creationHash, nil, err + } + if !hasRow { + return owner, creationHash, nil, nil + } + if stmt.ColumnBytes(0, owner[:]) != len(owner) { + panic("invalid address length") + } + metadata := make([]byte, stmt.ColumnLen(1)) + stmt.ColumnBytes(1, metadata) + + if stmt.ColumnBytes(2, creationHash[:]) != len(creationHash) { + panic("invalid hash length") + } + return owner, creationHash, metadata, nil +} + +// SelectDataAll returns the nfIDs, owner addresses, creation entry hashes, and +// the NFToken metadata for the given pagination range of NFTokens. +// +// Pages start at 1. +func SelectDataAll(conn *sqlite.Conn, order string, page, limit uint) ( + []fat1.NFTokenID, []factom.FAAddress, []factom.Bytes32, [][]byte, error) { + if page == 0 { + return nil, nil, nil, nil, fmt.Errorf("invalid page") + } + stmt := conn.Prep(`SELECT "id", "owner", "creation_hash", "metadata" + FROM "nf_tokens_addresses";`) + defer stmt.Reset() + + var tkns []fat1.NFTokenID + var owners []factom.FAAddress + var creationHashes []factom.Bytes32 + var metadata [][]byte + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, nil, nil, nil, err + } + if !hasRow { + break + } + tkns = append(tkns, fat1.NFTokenID(stmt.ColumnInt64(0))) + + var owner factom.FAAddress + if stmt.ColumnBytes(1, owner[:]) != len(owner) { + panic("invalid address length") + } + owners = append(owners, owner) + + var creationHash factom.Bytes32 + if stmt.ColumnBytes(2, creationHash[:]) != len(creationHash) { + panic("invalid hash length") + } + creationHashes = append(creationHashes, creationHash) + + data := make([]byte, stmt.ColumnLen(3)) + stmt.ColumnBytes(3, data) + metadata = append(metadata, data) + } + return tkns, owners, creationHashes, metadata, nil +} + +// SelectByOwner returns the fat1.NFTokens owned by the given adr for the given +// pagination range. +// +// Pages start at 1. +func SelectByOwner(conn *sqlite.Conn, adr *factom.FAAddress, + page, limit uint, order string) (fat1.NFTokens, error) { + if page == 0 { + return nil, fmt.Errorf("invalid page") + } + var sql sqlbuilder.SQLBuilder + sql.Append(`SELECT "id" FROM "nf_tokens" WHERE "owner_id" = ( + SELECT "id" FROM "addresses" WHERE "address" = ?)`, + func(s *sqlite.Stmt, c int) int { + s.BindBytes(c, adr[:]) + return 1 + }) + sql.OrderByPaginate("id", order, page, limit) + + stmt := sql.Prep(conn) + defer stmt.Reset() + nfTkns := make(fat1.NFTokens) + for { + hasRow, err := stmt.Step() + if err != nil { + return nil, err + } + if !hasRow { + break + } + colVal := stmt.ColumnInt64(0) + if colVal < 0 { + panic("negative NFTokenID") + } + nfTkns[fat1.NFTokenID(colVal)] = struct{}{} + } + return nfTkns, nil +} diff --git a/internal/db/nftokens/txrelation.go b/internal/db/nftokens/txrelation.go new file mode 100644 index 0000000..d1a45b5 --- /dev/null +++ b/internal/db/nftokens/txrelation.go @@ -0,0 +1,45 @@ +package nftokens + +import ( + "crawshaw.io/sqlite" + "github.com/Factom-Asset-Tokens/fatd/fat1" +) + +// CreateTableTransactions is a SQL string that creates the +// "nf_token_transactions" table. +// +// The "nf_token_transactions" table has a foreign key reference to the +// "address_transactions" and "nf_tokens" tables, which must exist first. +const CreateTableTransactions = `CREATE TABLE "nf_token_transactions" ( + "adr_tx_id" INTEGER NOT NULL, + "nf_tkn_id" INTEGER NOT NULL, + + PRIMARY KEY("adr_tx_id", "nf_tkn_id"), + + FOREIGN KEY("nf_tkn_id") REFERENCES "nf_tokens", + FOREIGN KEY("adr_tx_id") REFERENCES "address_transactions" +); +CREATE INDEX "idx_nf_token_transactions_adr_tx_id" ON + "nf_token_transactions"("adr_tx_id"); +CREATE INDEX "idx_nf_token_transactions_nf_tkn_id" ON + "nf_token_transactions"("nf_tkn_id"); +CREATE VIEW "nf_token_address_transactions" AS + SELECT "entry_id", "address_id", "nf_tkn_id", "to" FROM + "address_transactions" AS "adr_tx", + "nf_token_transactions" AS "tkn_tx" + ON "adr_tx"."rowid" = "tkn_tx"."adr_tx_id"; +` + +// InsertTransactionRelation inserts a row into "nf_token_transactions" +// relating the given adrTxID, a foreign row id from the "address_transactions" +// table, with the given nfID. +func InsertTransactionRelation(conn *sqlite.Conn, + nfID fat1.NFTokenID, adrTxID int64) error { + stmt := conn.Prep(`INSERT INTO "nf_token_transactions" + ("nf_tkn_id", "adr_tx_id") VALUES (?, ?);`) + stmt.BindInt64(1, int64(nfID)) + stmt.BindInt64(2, adrTxID) + + _, err := stmt.Step() + return err +} diff --git a/internal/db/schema.go b/internal/db/schema.go new file mode 100644 index 0000000..eed04e1 --- /dev/null +++ b/internal/db/schema.go @@ -0,0 +1,84 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package db + +import ( + "fmt" + + "crawshaw.io/sqlite" + "crawshaw.io/sqlite/sqlitex" + "github.com/Factom-Asset-Tokens/fatd/internal/db/addresses" + "github.com/Factom-Asset-Tokens/fatd/internal/db/eblocks" + "github.com/Factom-Asset-Tokens/fatd/internal/db/entries" + "github.com/Factom-Asset-Tokens/fatd/internal/db/metadata" + "github.com/Factom-Asset-Tokens/fatd/internal/db/nftokens" +) + +const ( + // For the sake of simplicity, all chain DBs use the exact same schema, + // regardless of whether they actually make use of the NFTokens tables. + chainDBSchema = eblocks.CreateTable + + entries.CreateTable + + addresses.CreateTable + + addresses.CreateTableTransactions + + nftokens.CreateTable + + nftokens.CreateTableTransactions + + metadata.CreateTable +) + +// validateOrApplySchema compares schema with the database connected to by +// conn. If the database has no schema, the schema is applied. Otherwise an +// error will be returned if the schema is not an exact match. +func validateOrApplySchema(conn *sqlite.Conn, schema string) error { + fullSchema, err := getFullSchema(conn) + if err != nil { + return err + } + if len(fullSchema) == 0 { + if err := sqlitex.ExecScript(conn, schema); err != nil { + return fmt.Errorf("failed to apply schema: %w", err) + } + return nil + } + if fullSchema != schema { + return fmt.Errorf("invalid schema: %v\n expected: %v", + fullSchema, schema) + } + return nil +} +func getFullSchema(conn *sqlite.Conn) (string, error) { + const selectSchema = `SELECT "sql" FROM "sqlite_master";` + var schema string + err := sqlitex.ExecTransient(conn, selectSchema, + func(stmt *sqlite.Stmt) error { + // Concatenate all non-empty table schemas. + if tableSchema := stmt.ColumnText(0); len(tableSchema) > 0 { + schema += tableSchema + ";\n" + } + return nil + }) + if err != nil { + return "", err + } + return schema, nil +} diff --git a/internal/db/sqlbuilder/sqlbuilder.go b/internal/db/sqlbuilder/sqlbuilder.go new file mode 100644 index 0000000..f4782cf --- /dev/null +++ b/internal/db/sqlbuilder/sqlbuilder.go @@ -0,0 +1,127 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package sqlbuilder + +import ( + "fmt" + "strings" + + "crawshaw.io/sqlite" +) + +// BindFunc is a function that binds one or more values to a sqlite.Stmt. A +// valid BindFunc must use startParam as the first param index for any binds +// and must return the total number of parameters bound. +type BindFunc func(stmt *sqlite.Stmt, startParam int) int + +// SQLBuilder prepares a sqlite.Stmt that has variable numbers of Binds. +type SQLBuilder struct { + strings.Builder + Binds []BindFunc + + LimitMax uint // Max limit allowed by Paginate() +} + +// Prep prepares a sqlite.Stmt on conn using s.String() with a trailing `;` as +// the SQL, and then sequentially calls all s.Binds using the Stmt and the +// appropriate startParam. The Stmt is returned ready for its first Step(). +// +// If the total number of binds reported by the Binds differs from the total +// number of params reported by the Stmt, then Prep panics. +func (s *SQLBuilder) Prep(conn *sqlite.Conn) *sqlite.Stmt { + s.WriteString(`;`) + stmt := conn.Prep(s.String()) + param := 1 + for _, bind := range s.Binds { + param += bind(stmt, param) + } + if param-1 != stmt.BindParamCount() { + panic(fmt.Errorf( + "reported bind count (%v) does not match bind param count (%v)"+ + "\nSQL:%q", + s.String()+`;`, param-1, stmt.BindParamCount())) + } + + return stmt +} + +// Append sql and any associated binds. +// +// Do not include a `;` in sql. +// +// The sum of the binds return values must equal the number of params (e.g. +// "?") in sql or else s.Prep will panic. +func (s *SQLBuilder) Append(sql string, binds ...BindFunc) { + s.WriteString(sql) + s.Binds = append(s.Binds, binds...) +} + +// BindNParams appends n comma separated params placeholders (e.g. "?, ?, ... , +// ?") and append the binds. +// +// Do not include a `;` in sql. +// +// The sum of the binds return values must equal n or else s.Prep will panic. +func (s *SQLBuilder) BindNParams(n int, binds ...BindFunc) { + str := strings.TrimRight(strings.Repeat("?, ", n), ", ") + s.Append(str, binds...) +} + +// LimitMaxDefault is used in SQLBuilder.Paginate() if SQLBuilder.LimitMax +// equals 0. +var LimitMaxDefault uint = 600 + +// Paginate appends ` LIMIT ?, ?` and the appropriate page and limit binds. +func (s *SQLBuilder) Paginate(page, limit uint) { + if s.LimitMax == 0 { + s.LimitMax = LimitMaxDefault + } + if limit == 0 || limit > s.LimitMax { + limit = s.LimitMax + } + s.Append(` LIMIT ?, ?`, func(s *sqlite.Stmt, p int) int { + s.BindInt64(p, int64((page-1)*limit)) + s.BindInt64(p+1, int64(limit)) + return 2 + }) +} + +// OrderBy append fmt.Sprintf(` ORDER BY %q %s`, col, order). No binds are +// added. +func (s *SQLBuilder) OrderBy(col, ascDesc string) { + ascDesc = strings.ToUpper(ascDesc) + switch ascDesc { + case "ASC", "DESC": + case "": + ascDesc = "ASC" + default: + panic(fmt.Errorf("invalid order: %v", ascDesc)) + } + s.WriteString(fmt.Sprintf(` ORDER BY %q %v`, col, ascDesc)) +} + +// OrderByPaginate calls s.OrderBy() and then s.Paginate(). +func (s *SQLBuilder) OrderByPaginate(col, order string, page, limit uint) { + s.OrderBy(col, order) + s.Paginate(page, limit) +} diff --git a/internal/db/test-fatd.db/0692c0f9c3171575cf53d6d8067139bad3f56169f94966341018b1950542f3dd.sqlite3 b/internal/db/test-fatd.db/0692c0f9c3171575cf53d6d8067139bad3f56169f94966341018b1950542f3dd.sqlite3 new file mode 100644 index 0000000..1ff7aaa Binary files /dev/null and b/internal/db/test-fatd.db/0692c0f9c3171575cf53d6d8067139bad3f56169f94966341018b1950542f3dd.sqlite3 differ diff --git a/internal/db/test-fatd.db/b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb.sqlite3 b/internal/db/test-fatd.db/b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb.sqlite3 new file mode 100644 index 0000000..caeda62 Binary files /dev/null and b/internal/db/test-fatd.db/b54c4310530dc4dd361101644fa55cb10aec561e7874a7b786ea3b66f2c6fdfb.sqlite3 differ diff --git a/internal/db/validate.go b/internal/db/validate.go new file mode 100644 index 0000000..6d58bd7 --- /dev/null +++ b/internal/db/validate.go @@ -0,0 +1,197 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package db + +import ( + "fmt" + "os" + "time" + + "crawshaw.io/sqlite/sqlitex" + "github.com/AdamSLevy/sqlitechangeset" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/internal/db/eblocks" + "github.com/Factom-Asset-Tokens/fatd/internal/db/entries" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" +) + +func init() { + sqlitechangeset.AlwaysUseBlob = true +} + +// Validate all Entry Hashes and EBlock KeyMRs, as well as the continuity of +// all stored EBlocks and Entries. +// +// This does not validate the validity of the saved DBlock KeyMRs. +func (chain Chain) Validate() (err error) { + // Validate ChainID... + chain.Log.Info("Validating...") + read := chain.Pool.Get(nil) + defer chain.Pool.Put(read) + write := chain.Conn + first, err := entries.SelectByID(read, 1) + if err != nil { + return err + } + if !first.IsPopulated() { + return fmt.Errorf("no entries") + } + if *chain.ID != factom.ComputeChainID(first.ExtIDs) { + return fmt.Errorf("invalid NameIDs") + } + + // We will use a session to determine if recomputing state results in + // any changes. If the state is uncorrupted, the session should have an + // empty patchset. + sess, err := write.CreateSession("") + if err != nil { + return err + } + sess.Attach("eblocks") + sess.Attach("entries") + sess.Attach("addresses") + sess.Attach("nf_tokens") + sess.Attach("metadata") + defer sess.Delete() + + // In case there are any changes, we want to roll back everything. We + // don't fix corrupted databases, at least not yet. + defer sqlitex.Save(write)(&err) + + // Completely clear the state, while preserving all chain data. + sqlitex.ExecScript(write, ` + UPDATE "addresses" SET "balance" = 0; + DELETE FROM "address_transactions"; + DELETE FROM "nf_tokens"; + DELETE FROM "nf_token_transactions"; + DELETE FROM "eblocks"; + DELETE FROM "entries"; + UPDATE "metadata" SET ("init_entry_id", "num_issued") = (NULL, NULL); + `) + chain.NumIssued = 0 + chain.Issuance = fat.Issuance{} + chain.setApplyFunc() + + eBlockStmt := read.Prep(eblocks.SelectWhere + `true;`) // SELECT all EBlocks. + defer eBlockStmt.Reset() + entryStmt := read.Prep(entries.SelectWhere + `true;`) // SELECT all Entries. + defer entryStmt.Reset() + + var eID int = 1 // Entry ID + var sequence uint32 // EBlock Sequence + var prevKeyMR, prevFullHash *factom.Bytes32 + for { + eb, err := eblocks.Select(eBlockStmt) + if err != nil { + return err + } + if !eb.IsPopulated() { + // No more EBlocks. + break + } + + if sequence != eb.Sequence { + return fmt.Errorf("invalid EBlock{%v, %v}: invalid Sequence", + eb.Sequence, eb.KeyMR) + } + sequence++ + + if (prevKeyMR != nil && *eb.PrevKeyMR != *prevKeyMR) || + (prevKeyMR == nil && !eb.PrevKeyMR.IsZero()) { + return fmt.Errorf("invalid EBlock{%v, %v}: broken PrevKeyMR link", + eb.Sequence, eb.KeyMR) + } + prevKeyMR = eb.KeyMR + + if (prevFullHash != nil && *eb.PrevFullHash != *prevFullHash) || + (prevFullHash == nil && !eb.PrevFullHash.IsZero()) { + return fmt.Errorf("invalid EBlock{%v, %v}: broken FullHash link", + eb.Sequence, eb.KeyMR) + } + prevFullHash = eb.FullHash + + for i, ebe := range eb.Entries { + e, err := entries.Select(entryStmt) + if err != nil { + return err + } + + if *e.Hash != *ebe.Hash { + return fmt.Errorf("invalid Entry{%v}: broken EBlock link", + e.Hash) + } + + if *e.ChainID != *chain.ID { + return fmt.Errorf("invalid Entry{%v}: invalid ChainID", + e.Hash) + } + + if e.Timestamp != ebe.Timestamp { + return fmt.Errorf("invalid Entry{%v}: invalid Timestamp", + e.Hash) + } + + eb.Entries[i] = e + eID++ + } + dbKeyMR, err := eblocks.SelectDBKeyMR(read, eb.Sequence) + if err != nil { + return err + } + if err := chain.Apply(&dbKeyMR, eb); err != nil { + return err + } + } + if sequence == 0 { + return fmt.Errorf("no eblocks") + } + + changesetSQL, err := sqlitechangeset.SessionToSQL(chain.Conn, sess) + if err != nil { + chain.Log.Debugf("sqlitechangeset.SessionToSQL(): %v", err) + return + } + if len(changesetSQL) > 0 { + defer func() { + chain.Log.Warnf("invalid state changeset: %v", changesetSQL) + // Write the changeset to a file for later analysis... + path := fmt.Sprintf("%v/%v-corrupt-%v.changeset", + flag.DBPath, chain.ID.String(), time.Now().Unix()) + chain.Log.Warnf("writing corrupted state changeset to %v", path) + f, err := os.Create(path) + if err != nil { + chain.Log.Debug(err) + return + } + if _, err := f.WriteString(changesetSQL); err != nil { + chain.Log.Debug(err) + } + if err := f.Close(); err != nil { + chain.Log.Debug(err) + } + }() + return fmt.Errorf("could not recompute saved state") + } + return nil +} diff --git a/internal/engine/chain.go b/internal/engine/chain.go new file mode 100644 index 0000000..fbe2b44 --- /dev/null +++ b/internal/engine/chain.go @@ -0,0 +1,205 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package engine + +import ( + "context" + "fmt" + + "crawshaw.io/sqlite" + + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/internal/db" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" + _log "github.com/Factom-Asset-Tokens/fatd/internal/log" +) + +type Chain struct { + ChainStatus + db.Chain + + Pending Pending +} + +type Pending struct { + OfficialSnapshot *sqlite.Snapshot + + Session *sqlite.Session + OfficialChain db.Chain + + Entries map[factom.Bytes32]factom.Entry +} + +func (chain Chain) String() string { + return fmt.Sprintf("{ChainStatus:%v, ID:%v, "+ + "fat.Identity:%+v, fat.Issuance:%+v}", + chain.ChainStatus, chain.ID, + chain.Identity, chain.Issuance) +} + +func OpenNew(ctx context.Context, c *factom.Client, + dbKeyMR *factom.Bytes32, eb factom.EBlock) (chain Chain, err error) { + var identity factom.Identity + identity.ChainID = new(factom.Bytes32) + _, *identity.ChainID = fat.ParseTokenIssuer(eb.Entries[0].ExtIDs) + if err = identity.Get(ctx, c); err != nil { + // A jsonrpc2.Error indicates that the identity chain + // doesn't yet exist, which we tolerate. + if _, ok := err.(jsonrpc2.Error); !ok { + return + } + } + + if err := eb.GetEntries(ctx, c); err != nil { + return chain, fmt.Errorf("%#v.GetEntries(): %w", eb, err) + } + + chain.Chain, err = db.OpenNew(ctx, + flag.DBPath, dbKeyMR, eb, flag.NetworkID, identity) + if err != nil { + return chain, fmt.Errorf("db.OpenNew(): %w", err) + } + if chain.Issuance.Entry.IsPopulated() { + chain.ChainStatus = ChainStatusIssued + } else { + chain.ChainStatus = ChainStatusTracked + } + return +} + +func OpenNewByChainID(ctx context.Context, + c *factom.Client, chainID *factom.Bytes32) (chain Chain, err error) { + + log := _log.New("chain", chainID) + log.Infof("Syncing new chain...") + + eblocks, err := factom.EBlock{ChainID: chainID}.GetPrevAll(ctx, c) + if err != nil { + err = fmt.Errorf("factom.EBlock.GetPrevAll(): %w", err) + return + } + + firstEB := eblocks[len(eblocks)-1] + // Get DBlock Timestamp and KeyMR + var dblock factom.DBlock + dblock.Height = firstEB.Height + if err = dblock.Get(ctx, c); err != nil { + err = fmt.Errorf("factom.DBlock.Get(): %w", err) + return + } + firstEB.SetTimestamp(dblock.Timestamp) + if err = firstEB.Get(ctx, c); err != nil { + err = fmt.Errorf("%#v.Get(): %w", firstEB, err) + return + } + // Load first entry of new chain. + first := &firstEB.Entries[0] + if err = first.Get(ctx, c); err != nil { + err = fmt.Errorf("%#v.Get(): %w", first, err) + return + } + + nameIDs := first.ExtIDs + if !fat.ValidNameIDs(nameIDs) { + err = fmt.Errorf("not a valid FAT chain: %v", chainID) + return + } + + chain, err = OpenNew(ctx, c, dblock.KeyMR, firstEB) + if err != nil { + return + } + defer func() { + if err != nil { + chain.Close() + } + }() + + // We already applied the first EBlock. Sync the remaining. + err = chain.SyncEBlocks(ctx, c, eblocks[:len(eblocks)-1]) + return +} + +func (chain *Chain) Sync(ctx context.Context, c *factom.Client) error { + chain.Log.Infof("Syncing chain...") + eblocks, err := factom.EBlock{ChainID: chain.ID}. + GetPrevUpTo(ctx, c, *chain.Head.KeyMR) + if err != nil { + return fmt.Errorf("factom.EBlock.GetPrevUpTo(): %w", err) + } + return chain.SyncEBlocks(ctx, c, eblocks) +} + +func (chain *Chain) SyncEBlocks( + ctx context.Context, c *factom.Client, ebs []factom.EBlock) error { + for i := range ebs { + eb := ebs[len(ebs)-1-i] // Earliest EBlock first. + + // Get DBlock Timestamp and KeyMR + var dblock factom.DBlock + dblock.Height = eb.Height + if err := dblock.Get(ctx, c); err != nil { + return fmt.Errorf("factom.DBlock.Get(): %w", err) + } + eb.SetTimestamp(dblock.Timestamp) + + if err := chain.Apply(ctx, c, dblock.KeyMR, eb); err != nil { + return err + } + } + chain.Log.Infof("Chain synced.") + return nil +} + +func (chain *Chain) Apply(ctx context.Context, c *factom.Client, + dbKeyMR *factom.Bytes32, eb factom.EBlock) error { + // Get Identity each time in case it wasn't populated before. + if err := chain.Identity.Get(ctx, c); err != nil { + // A jsonrpc2.Error indicates that the identity chain doesn't yet + // exist, which we tolerate. + if _, ok := err.(jsonrpc2.Error); !ok { + return err + } + } + // Get all entry data. + if err := eb.GetEntries(ctx, c); err != nil { + return err + } + if err := chain.Chain.Apply(dbKeyMR, eb); err != nil { + return err + } + // Update ChainStatus + if !chain.IsIssued() && chain.Issuance.Entry.IsPopulated() { + chain.ChainStatus = ChainStatusIssued + } + return nil +} + +func (chain *Chain) conflictFn( + cType sqlite.ConflictType, _ sqlite.ChangesetIter) sqlite.ConflictAction { + chain.Log.Errorf("ChangesetApply Conflict: %v", cType) + return sqlite.SQLITE_CHANGESET_ABORT +} diff --git a/internal/engine/chainmap.go b/internal/engine/chainmap.go new file mode 100644 index 0000000..2005a91 --- /dev/null +++ b/internal/engine/chainmap.go @@ -0,0 +1,224 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package engine + +import ( + "context" + "fmt" + "math" + "sync" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/internal/db" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" +) + +var ( + Chains = ChainMap{m: map[factom.Bytes32]Chain{ + factom.Bytes32{31: 0x0a}: Chain{ChainStatus: ChainStatusIgnored}, + factom.Bytes32{31: 0x0c}: Chain{ChainStatus: ChainStatusIgnored}, + factom.Bytes32{31: 0x0f}: Chain{ChainStatus: ChainStatusIgnored}, + }, RWMutex: new(sync.RWMutex)} +) + +type ChainMap struct { + m map[factom.Bytes32]Chain + issuedIDs []*factom.Bytes32 + trackedIDs []*factom.Bytes32 + *sync.RWMutex +} + +func (cm *ChainMap) set(id *factom.Bytes32, chain Chain, prevStatus ChainStatus) { + cm.Lock() + defer cm.Unlock() + cm.m[*id] = chain + if chain.ChainStatus != prevStatus { + switch chain.ChainStatus { + case ChainStatusIssued: + cm.issuedIDs = append(cm.issuedIDs, id) + fallthrough + case ChainStatusTracked: + if prevStatus.IsUnknown() { + cm.trackedIDs = append(cm.trackedIDs, id) + } + } + } +} + +func (cm *ChainMap) ignore(id *factom.Bytes32) { + cm.set(id, Chain{ChainStatus: ChainStatusIgnored}, ChainStatusIgnored) +} + +func (cm *ChainMap) get(id *factom.Bytes32) Chain { + cm.RLock() + defer cm.RUnlock() + return cm.m[*id] +} + +func (cm *ChainMap) GetIssued() []*factom.Bytes32 { + cm.RLock() + defer cm.RUnlock() + return cm.issuedIDs +} + +func (cm *ChainMap) GetTracked() []*factom.Bytes32 { + cm.RLock() + defer cm.RUnlock() + return cm.trackedIDs +} + +func (cm *ChainMap) setSync(height uint32, dbKeyMR *factom.Bytes32) error { + cm.Lock() + defer cm.Unlock() + for _, chain := range cm.m { + if !chain.IsTracked() { + continue + } + if err := chain.SetSync(height, dbKeyMR); err != nil { + return fmt.Errorf("chain{%v}.SetSync(): %w", chain.ID, err) + } + cm.m[*chain.ID] = chain + } + return nil +} + +func (cm *ChainMap) Close() { + cm.Lock() + defer cm.Unlock() + for _, chain := range cm.m { + if chain.IsTracked() { + // Rollback any pending entries on the chain. + if chain.Pending.Entries != nil { + // Always clean up. + if err := chain.revertPending(); err != nil { + log.Error(err) + } + } + chain.Close() + } + } +} + +// loadChains loads all chains from the database that are not blacklisted, and +// syncs them. Any whitelisted chains that are not previously tracked are +// synced. The lowest sync height among all chain databases is returned. +func loadChains(ctx context.Context) (syncHeight uint32, err error) { + dbChains, err := db.OpenAll(ctx, flag.DBPath) + if err != nil { + return + } + defer func() { + if err != nil { + for _, chain := range dbChains { + if chain.Conn != nil { + chain.Close() + } + } + } + }() + Chains.Lock() + defer Chains.Unlock() + // Set whitelisted chains to Tracked. + for _, chainID := range flag.Whitelist { + Chains.m[chainID] = Chain{ChainStatus: ChainStatusTracked} + } + // Blacklist overrides whitelist. Set chains to Ignore. + for _, chainID := range flag.Blacklist { + Chains.m[chainID] = Chain{ChainStatus: ChainStatusIgnored} + } + + if len(dbChains) > 0 { + syncHeight = math.MaxUint32 + } + for i, dbChain := range dbChains { + chain := Chains.m[*dbChain.ID] + + // Close and skip any blacklisted chains or, if there was a + // whitelist, any non-tracked chain. + if chain.IsIgnored() || flag.HasWhitelist() && !chain.IsTracked() { + dbChain.Close() + // Prevent double close in defer on error. + dbChains[i].Conn = nil + continue + } + + chain.Chain = dbChain + + syncHeight = min(syncHeight, chain.SyncHeight) + + if chain.NetworkID != flag.NetworkID { + err = fmt.Errorf("invalid NetworkID: %v for Chain{%v}", + chain.NetworkID, chain.ID) + return + } + + if !flag.SkipDBValidation { + if err = chain.Validate(); err != nil { + return + } + } + + if err = chain.Sync(ctx, c); err != nil { + return + } + + chain.ChainStatus = ChainStatusTracked + Chains.trackedIDs = append(Chains.trackedIDs, chain.ID) + if chain.Issuance.Entry.IsPopulated() { + chain.ChainStatus = ChainStatusIssued + Chains.issuedIDs = append(Chains.issuedIDs, chain.ID) + } + + Chains.m[*chain.ID] = chain + } + + // Open any whitelisted chains that do not already have databases. + for id, chain := range Chains.m { + if !(chain.IsTracked() && chain.Chain.Conn == nil) { + continue + } + id := id + var chain Chain + chain, err = OpenNewByChainID(ctx, c, &id) + if err != nil { + return + } + Chains.trackedIDs = append(Chains.trackedIDs, chain.ID) + if chain.IsIssued() { + Chains.issuedIDs = append(Chains.issuedIDs, chain.ID) + } + Chains.m[*chain.ID] = chain + + // Ensure that this new chain gets closed in the defer if an + // error occurs. + dbChains = append(dbChains, chain.Chain) + } + + return +} +func min(a, b uint32) uint32 { + if a <= b { + return a + } + return b +} diff --git a/state/chainstatus.go b/internal/engine/chainstatus.go similarity index 99% rename from state/chainstatus.go rename to internal/engine/chainstatus.go index c137150..bb6b0d4 100644 --- a/state/chainstatus.go +++ b/internal/engine/chainstatus.go @@ -20,7 +20,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package state +package engine type ChainStatus uint diff --git a/state/chainstatus_test.go b/internal/engine/chainstatus_test.go similarity index 97% rename from state/chainstatus_test.go rename to internal/engine/chainstatus_test.go index f0a985b..baaec70 100644 --- a/state/chainstatus_test.go +++ b/internal/engine/chainstatus_test.go @@ -20,12 +20,11 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. -package state_test +package engine import ( "testing" - . "github.com/Factom-Asset-Tokens/fatd/state" "github.com/stretchr/testify/assert" ) diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..aace225 --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,418 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package engine + +import ( + "context" + "fmt" + "os" + "runtime" + "strings" + "sync" + "time" + + "github.com/nightlyone/lockfile" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" + _log "github.com/Factom-Asset-Tokens/fatd/internal/log" +) + +var ( + log _log.Log + c = flag.FactomClient + lockFile lockfile.Lockfile +) + +const ( + scanInterval = 30 * time.Second +) + +// Start launches the main engine goroutine, which loads state and starts the +// worker goroutines. If stop is closed or if an error occurs, the engine will +// finish processing the current DBlock, cleanup and close state, all +// goroutines will exit, and done will be closed. If the done channel is closed +// before the stop channel is closed, an error occurred. +func Start(ctx context.Context) (done <-chan struct{}) { + log = _log.New("pkg", "engine") + + // Try to create the database directory. + if err := os.Mkdir(flag.DBPath, 0755); err != nil { + if !os.IsExist(err) { + log.Errorf("os.Mkdir(%q): %v", flag.DBPath, err) + return nil + } + } + // Add NetworkID subdirectory. + flag.DBPath += fmt.Sprintf("%s%c", + strings.ReplaceAll(flag.NetworkID.String(), " ", ""), os.PathSeparator) + if err := os.Mkdir(flag.DBPath, 0755); err != nil { + if !os.IsExist(err) { + log.Errorf("os.Mkdir(%q): %v", flag.DBPath, err) + return nil + } + } + + // Try to create a lockfile + lockFilePath := flag.DBPath + "db.lock" + var err error + lockFile, err = lockfile.New(lockFilePath) + if err != nil { + log.Errorf("lockfile.New(%q): %v", lockFilePath, err) + return + } + if err = lockFile.TryLock(); err != nil { + log.Errorf("lockFile.TryLock(): %v", err) + return + } + // Always clean up the lockfile if Start fails. + defer func() { + if done == nil { + if err := lockFile.Unlock(); err != nil { + log.Errorf("lockFile.Unlock(): %v", err) + } + } + }() + + // Verify Factom Blockchain NetworkID... + if err := updateFactomHeight(ctx); err != nil { + if ctx.Err() == nil { + log.Error(err) + } + return + } + var dblock factom.DBlock + dblock.Height = factomHeight + if err := dblock.Get(ctx, c); err != nil { + if ctx.Err() == nil { + log.Errorf("dblock.Get(): %v", err) + } + return + } + if dblock.NetworkID != flag.NetworkID { + log.Errorf("invalid Factom Blockchain NetworkID: %v, expected: %v", + dblock.NetworkID, flag.NetworkID) + return + } + + // Load and sync all existing and whitelisted chains. + log.Infof("Loading chain databases from %v...", flag.DBPath) + if flag.SkipDBValidation { + log.Warn("Skipping database validation...") + } + syncHeight, err = loadChains(ctx) + if ctx.Err() != nil { + return + } + if err != nil { + log.Error(err) + return + } + // Always close all chain databases if Start fails. + defer func() { + if done == nil { + Chains.Close() + } + }() + + if flag.IgnoreNewChains() { + // We can assume that all chains are synced to their + // chainheads, so we can start at the current height if we are + // ignoring new chains. + syncHeight = factomHeight + if len(Chains.trackedIDs) == 0 { + log.Error("no chains to track") + return + } + } else if flag.StartScanHeight > -1 { // If -startscanheight was set... + if flag.StartScanHeight > int32(factomHeight) { + log.Errorf("-startscanheight %v > Factom height (%v)", + flag.StartScanHeight, factomHeight) + return + } + if !flag.IgnoreNewChains() && + flag.StartScanHeight > int32(syncHeight)+1 { + log.Warnf("-startscanheight %v skips over %v blocks from the last saved last saved block height which will result in missing any new FAT Chains created in those blocks.", + flag.StartScanHeight, + flag.StartScanHeight-int32(syncHeight)-1) + } + // We start syncing at syncHeight+1, so subtract one. This + // overflows for 0 but it's OK as long as we don't rely on the + // value until the first scan loop. + syncHeight = uint32(flag.StartScanHeight - 1) + } else if syncHeight == 0 { // else if the syncHeight has not been set... + switch flag.NetworkID { + case factom.MainnetID(): + const mainnetStart = 163180 + syncHeight = mainnetStart // Set for mainnet + case factom.TestnetID(): + const testnetStart = 60783 + syncHeight = testnetStart // Set for testnet + default: + var zero uint32 // Avoid constant overflow compile error. + syncHeight = zero - 1 // Start scan at 0. + } + } + + _done := make(chan struct{}) + go engine(ctx, _done) + return _done +} + +func engine(ctx context.Context, done chan struct{}) { + // Always close all chains and remove lockfile on exit. + defer func() { + Chains.Close() + if err := lockFile.Unlock(); err != nil { + log.Errorf("lockFile.Unlock(): %v", err) + } + log.Infof("Synced to block height %v.", syncHeight) + + close(done) + }() + + // eblocks is used to send new EBlocks to the workers for processing. + eblocks := make(chan factom.EBlock) + + // eblocksWG is used to signal that all EBlocks for the current DBlock + // are done being processed. This is reused each DBlock. + var eblocksWG sync.WaitGroup + + // stopWorkers may be called multiple times by any worker or this + // goroutine, but eblocks will only ever be closed once. + var once sync.Once + stopWorkers := func() { + once.Do(func() { + close(eblocks) + // Drain remaining Eblocks and mark them all done. + var n int + for range eblocks { + n++ + } + eblocksWG.Add(-n) + }) + } + + // Always stop all workers on exit. + defer stopWorkers() + + // dblock is declared here and reused so that the workers below can + // form a closure around it. + var dblock factom.DBlock + + // Launch workers to process new EBlocks. + numWorkers := runtime.NumCPU() + for i := 0; i < numWorkers; i++ { + go func() { + defer stopWorkers() // If one worker returns, they all return. + for eb := range eblocks { + if err := Process(ctx, dblock.KeyMR, eb); err != nil { + if ctx.Err() == nil { + log.Errorf("ChainID(%v): %v", + eb.ChainID, err) + } + eblocksWG.Done() + return + } + eblocksWG.Done() + } + }() + } + + if !flag.IgnoreNewChains() && syncHeight < factomHeight { + log.Infof("Searching for new FAT chains from block %v to %v...", + syncHeight+1, factomHeight) + } + + // synced tracks whether we have completed our first sync. + var synced bool + + // retries tracks the number of times we have had to retry querying for + // the latest factom height. + var retries int64 + + // scanTicker kicks off a new scan. + scanTicker := time.NewTicker(flag.FactomScanInterval) + + // Factom Blockchain Scan Loop + for { + if !synced && syncHeight == factomHeight { + synced = true + log.Infof("Synced to block %v.", syncHeight) + } + + // Process all new DBlocks sequentially. + for h := syncHeight + 1; h <= factomHeight; h++ { + // Get DBlock. + dblock = factom.DBlock{} + dblock.Height = h + if err := dblock.Get(ctx, c); err != nil { + if ctx.Err() == nil { + log.Errorf("%#v.Get(): %v", dblock, err) + } + return + } + + // Queue all EBlocks for processing and wait for all to + // be processed. + eblocksWG.Add(len(dblock.EBlocks)) + for _, eb := range dblock.EBlocks { + eblocks <- eb + } + eblocksWG.Wait() + + // Check if any of the workers closed the eblocks + // channel to indicate a Process() error. + select { + case <-eblocks: + // One or more of the workers had an error and + // closed the eblocks channel. + // Since we cannot consider this DBlock + // completed, we do not update sync height for + // any chains. + return + default: + } + + // DBlock completed so update the sync height for all + // chains. + setSyncHeight(h) + if err := Chains.setSync(h, dblock.KeyMR); err != nil { + if ctx.Err() == nil { + log.Errorf("Chains.setSync(): %v", err) + } + return + } + + // Check that we haven't been told to stop. + select { + case <-ctx.Done(): + return + default: + } + } + + var pe factom.PendingEntries + + // If we aren't yet synced, we want to immediately re-check the + // Factom Height as the Blockchain may have advanced in the + // time since we started the sync. + if !synced { + goto scan + } + + if flag.DisablePending { + goto wait + } + + // Get and apply any pending entries. + if err := pe.Get(ctx, c); err != nil { + if ctx.Err() != nil { + return + } + log.Errorf("factom.PendingEntries.Get(): %v", err) + } + for i, j := 0, 0; i < len(pe); i = j { + e := pe[i] + + // Unrevealed entries have no ChainID and are at the + // end of the slice. + if e.ChainID == nil { + // No more revealed entries. + break + } + + // Grab any subsequent entries with this ChainID. + for j = i + 1; j < len(pe); j++ { + chainID := pe[j].ChainID + if chainID == nil || *chainID != *e.ChainID { + break + } + } + + // Process all pending entries for this chain. + if err := ProcessPending(ctx, pe[i:j]...); err != nil { + if ctx.Err() == nil { + log.Errorf("ChainID(%v): %v", + e.ChainID, err) + } + return + } + } + + wait: + // Wait until the next scan tick or we're told to stop. + select { + case <-scanTicker.C: + case <-ctx.Done(): + return + } + + scan: + // Check the Factom blockchain height but log and retry if this + // request fails. + if err := updateFactomHeight(ctx); err != nil { + log.Error(err) + if flag.FactomScanRetries > -1 && + retries >= flag.FactomScanRetries { + return + } + retries++ + log.Infof("Retrying in %v... (%v)", scanInterval, retries) + } else { + retries = 0 + } + } +} + +var ( + syncHeight, factomHeight uint32 + heightMtx = &sync.RWMutex{} +) + +// GetSyncStatus is a threadsafe way to get the sync height and current Factom +// Blockchain height. +func GetSyncStatus() (sync, current uint32) { + heightMtx.RLock() + defer heightMtx.RUnlock() + return syncHeight, factomHeight +} + +func setSyncHeight(sync uint32) { + heightMtx.Lock() + defer heightMtx.Unlock() + syncHeight = sync +} + +func updateFactomHeight(ctx context.Context) error { + // Get the current Factom Blockchain height. + var heights factom.Heights + err := heights.Get(ctx, c) + if err != nil { + return fmt.Errorf("factom.Heights.Get(): %v", err) + } + heightMtx.Lock() + defer heightMtx.Unlock() + factomHeight = heights.Entry + return nil +} diff --git a/internal/engine/process.go b/internal/engine/process.go new file mode 100644 index 0000000..26c1b8b --- /dev/null +++ b/internal/engine/process.go @@ -0,0 +1,321 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package engine + +import ( + "bytes" + "context" + "fmt" + "time" + + "crawshaw.io/sqlite" + "crawshaw.io/sqlite/sqlitex" + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/internal/db" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" +) + +func Process(ctx context.Context, dbKeyMR *factom.Bytes32, eb factom.EBlock) error { + chain := Chains.get(eb.ChainID) + + // Skip ignored chains and if we are ignoring new chain, also skip + // unknown chains. + if chain.IsIgnored() || + (flag.IgnoreNewChains() && chain.IsUnknown()) { + return nil + } + if chain.IsUnknown() { + if err := eb.Get(ctx, c); err != nil { + return fmt.Errorf("%#v.Get(): %w", eb, err) + } + if !eb.IsFirst() { + Chains.ignore(eb.ChainID) + return nil + } + + // Load first entry of new chain. + first := &eb.Entries[0] + if err := first.Get(ctx, c); err != nil { + return fmt.Errorf("%#v.Get(): %w", first, err) + } + // Ignore chains with NameIDs that don't match the fat pattern. + nameIDs := first.ExtIDs + if !fat.ValidNameIDs(nameIDs) { + Chains.ignore(eb.ChainID) + return nil + } + + // Attempt to open a new chain. + var err error + chain, err = OpenNew(ctx, c, dbKeyMR, eb) + if err != nil { + return err + } + + log.Infof("Tracking new FAT chain: %v", chain.ID) + + // Fully sync the chain, so that it is up to date immediately. + if err := chain.Sync(ctx, c); err != nil { + return err + } + + // Save the chain back into the map. + Chains.set(chain.ID, chain, ChainStatusUnknown) + return nil + } + + // Ignore EBlocks earlier than the chain's current sync height. + if eb.Height <= chain.Head.Height { + return nil + } + + // Rollback any pending entries on the chain. + if chain.Pending.Entries != nil { + // Load any cached entries that are pending and remove them + // from the cache. + for i := range eb.Entries { + e := &eb.Entries[i] + + // Check if this entry is cached. + cachedE, ok := chain.Pending.Entries[*e.Hash] + if !ok { + continue + } + + // Use official Timestamp established by EBlock. + cachedE.Timestamp = e.Timestamp + *e = cachedE + } + + err := chain.revertPending() + // We must save the Chain back to the map at this point to + // avoid a double free panic in the event of any further + // errors. + Chains.set(chain.ID, chain, chain.ChainStatus) + if err != nil { + return err + } + } + + // prevStatus saves the initial ChainStatus so we can detect if the + // chain goes from Tracked to Issued. + prevStatus := chain.ChainStatus + + // Apply this EBlock to the chain. + chain.Log.Debugf("Applying EBlock %v...", eb.KeyMR) + if err := chain.Apply(ctx, c, dbKeyMR, eb); err != nil { + return err + } + if err := sqlitex.ExecScript(chain.Conn, + `PRAGMA main.wal_checkpoint;`); err != nil { + chain.Log.Error(err) + } + + // Save the chain back into the map. + Chains.set(chain.ID, chain, prevStatus) + + return nil +} + +func ProcessPending(ctx context.Context, es ...factom.Entry) error { + chain := Chains.get(es[0].ChainID) + + // We can only apply pending entries to Issued chains. + if !chain.IsIssued() { + return nil + } + + // Initialize Pending if Entries is not yet populated. + if chain.Pending.Entries == nil { + if err := chain.initPending(ctx); err != nil { + return err + } + // Ensure the chain is saved back into the map. + Chains.set(chain.ID, chain, chain.ChainStatus) + } + + // startLenEntries tracks the initial size of our cache so we can + // detect if any new pending entries get applied. + startLenEntries := len(chain.Pending.Entries) + + // Apply any new pending entries. + for _, e := range es { + // Ignore entries we have seen before. + if _, ok := chain.Pending.Entries[*e.Hash]; ok { + continue + } + + // Load the Entry data. + if err := e.Get(ctx, c); err != nil { + return err + } + + // The timestamp won't be established until the next EBlock so + // use the current time for now. + e.Timestamp = time.Now() + + if _, err := chain.Chain.ApplyEntry(e); err != nil { + return err + } + + // Cache the entry. + chain.Pending.Entries[*e.Hash] = e + } + + // Check if any no new entries were added. + if startLenEntries == len(chain.Pending.Entries) { + return nil + } + + chain.Log.Debugf("Applied %v new pending entries.", + len(chain.Pending.Entries)-startLenEntries) + + // Save the chain back into the map. + Chains.set(chain.ID, chain, chain.ChainStatus) + return nil +} +func (chain *Chain) initPending(ctx context.Context) (err error) { + chain.Log.Debug("Initializing pending...") + + s, err := chain.Pool.GetSnapshot(ctx) + if err != nil { + return + } + + // Start a new session so we can track all changes and later rollback + // all pending entries. + session, err := chain.Conn.CreateSession("") + if err != nil { + return err + } + defer func() { + if err != nil { + session.Delete() + } + }() + if err := session.Attach(""); err != nil { + return err + } + + // There is a chance the Identity is populated now but wasn't before, + // so update it now. + if err := chain.Identity.Get(ctx, c); err != nil { + // A jsonrpc2.Error indicates that the identity chain doesn't + // yet exist, which we tolerate. + if _, ok := err.(jsonrpc2.Error); !ok { + return err + } + } + + chain.Pending.Entries = make(map[factom.Bytes32]factom.Entry) + chain.Pending.OfficialChain = chain.Chain + chain.Pending.Session = session + chain.Pending.OfficialSnapshot = s + + return nil +} +func (chain *Chain) revertPending() error { + chain.Log.Debug("Cleaning up pending state...") + // We must clear the interrupt to prevent from panicking or being + // interrupted while reverting. + oldDone := chain.Conn.SetInterrupt(nil) + defer func() { + chain.Pending.Entries = nil + // Always clean up our session and snapshots. + chain.Pending.OfficialSnapshot = nil + chain.Chain = chain.Pending.OfficialChain + chain.Pending.OfficialChain = db.Chain{} + + chain.Pending.Session.Delete() + chain.Pending.Session = nil + chain.Conn.SetInterrupt(oldDone) + + }() + // Revert all of the pending transactions by applying the inverse of + // the changeset tracked by the session. + var changeset bytes.Buffer + if err := chain.Pending.Session.Changeset(&changeset); err != nil { + return fmt.Errorf("chain.Pending.Session.Changeset(): %w", err) + } + inverse := bytes.NewBuffer(make([]byte, 0, changeset.Len())) + if err := sqlite.ChangesetInvert(inverse, &changeset); err != nil { + return fmt.Errorf("sqlite.ChangesetInvert(): %w", err) + } + if err := chain.Conn.ChangesetApply(inverse, nil, chain.conflictFn); err != nil { + return fmt.Errorf("chain.Conn.ChangesetApply(): %w", err) + + } + return nil +} + +// Get returns a threadsafe connection to the database, and a function to +// release the connection back to the pool. If pending is true, the chain will +// reflect the state with pending entries applied. Otherwise the chain will +// reflect the official state after the most recent EBlock. +func (cm *ChainMap) Get(ctx context.Context, + id *factom.Bytes32, pending bool) (Chain, func(), error) { + + chain := cm.get(id) + if !chain.IsTracked() { + return chain, nil, nil + } + + // Pull a Conn off the Pool and set it as the main Conn. + conn := chain.Pool.Get(ctx) + if conn == nil { + return Chain{}, nil, ctx.Err() + } + chain.Conn = conn + + // If pending or if there is no pending state, then use the chain as + // is, and just return a function that returns the conn to the pool. + if pending || chain.Pending.Entries == nil { + return chain, func() { chain.Pool.Put(conn) }, nil + } + // There are pending entries, but we have been asked for the official + // state. + + // Start a read transaction on the conn that reflects the official + // state. + endRead, err := conn.StartSnapshotRead(chain.Pending.OfficialSnapshot) + if err != nil { + chain.Pool.Put(conn) + panic(err) + } + + // Use the official chain state with the conn from the Pool. + chain.Chain = chain.Pending.OfficialChain + chain.Conn = conn + + // Return a function that ends the read transaction and returns the + // conn to the Pool. + return chain, func() { + // We must clear the interrupt to prevent endRead from + // panicking. + conn.SetInterrupt(nil) + endRead() + chain.Pool.Put(conn) + }, nil +} diff --git a/flag/flag.go b/internal/flag/flag.go similarity index 62% rename from flag/flag.go rename to internal/flag/flag.go index 81bcae5..cf7e6fc 100644 --- a/flag/flag.go +++ b/internal/flag/flag.go @@ -23,13 +23,15 @@ package flag import ( + "context" "flag" "fmt" "os" + "path/filepath" "strconv" "time" - "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" "github.com/posener/complete" "github.com/sirupsen/logrus" ) @@ -41,13 +43,19 @@ const envNamePrefix = "FATD_" var ( envNames = map[string]string{ - "startscanheight": "START_SCAN_HEIGHT", - "factomscanretries": "FACTOM_SCAN_RETRIES", - "debug": "DEBUG", + "startscanheight": "START_SCAN_HEIGHT", + "factomscanretries": "FACTOM_SCAN_RETRIES", + "factomscaninterval": "FACTOM_SCAN_INTERVAL", + "debug": "DEBUG", + "disablepending": "DISABLE_PENDING", "dbpath": "DB_PATH", - "apiaddress": "API_ADDRESS", + "apiaddress": "API_ADDRESS", + "apiusername": "API_USERNAME", + "apipassword": "API_PASSWORD", + "apitlscert": "API_TLS_CERT", + "apitlskey": "API_TLS_KEY", "s": "FACTOMD_SERVER", "factomdtimeout": "FACTOMD_TIMEOUT", @@ -65,25 +73,43 @@ var ( "ecadr": "ECADR", "esadr": "ESADR", + + "networkid": "NETWORK_ID", + + "whitelist": "WHITELIST", + "blacklist": "BLACKLIST", + "ignorenewchains": "IGNORE_NEW_CHAINS", + "skipdbvalidation": "SKIP_DB_VALIDATION", } defaults = map[string]interface{}{ - "startscanheight": uint64(0), - "factomscanretries": int64(0), - "debug": false, - - "dbpath": "./fatd.db", + "startscanheight": uint64(0), + "factomscanretries": int64(0), + "factomscaninterval": 15 * time.Second, + "debug": false, + "disablepending": false, + + "dbpath": func() string { + if home, err := os.UserHomeDir(); err == nil { + return home + "/.fatd" + } + return "./fatd.db" + }(), - "apiaddress": ":8078", + "apiaddress": ":8078", + "apiusername": "", + "apipassword": "", + "apitlscert": "", + "apitlskey": "", - "s": "http://localhost:8088", - "factomdtimeout": time.Duration(0), + "s": "http://localhost:8088/v2", + "factomdtimeout": 20 * time.Second, "factomduser": "", "factomdpassword": "", //"factomdcert": "", //"factomdtls": false, - "w": "http://localhost:8089", - "wallettimeout": time.Duration(0), + "w": "http://localhost:8089/v2", + "wallettimeout": 10 * time.Second, "walletuser": "", "walletpassword": "", //"walletcert": "", @@ -91,15 +117,24 @@ var ( "ecadr": "", "esadr": "", + + "ignorenewchains": false, + "skipdbvalidation": false, } descriptions = map[string]string{ - "startscanheight": "Block height to start scanning for deposits on startup", - "factomscanretries": "Number of times to consecutively retry fetching the latest height before exiting, use -1 for unlimited", - "debug": "Log debug messages", + "startscanheight": "Block height to start scanning for deposits on startup", + "factomscanretries": "Number of times to consecutively retry fetching the latest height before exiting, use -1 for unlimited", + "factomscaninterval": "Scan interval for new blocks or pending entries", + "debug": "Log debug messages", + "disablepending": "Do not scan for pending txs, reducing memory usage", "dbpath": "Path to the folder containing all database files", - "apiaddress": "IPAddr:port# to bind to for serving the JSON RPC 2.0 API", + "apiaddress": "IPAddr:port# to bind to for serving the fatd API", + "apiusername": "Username required for connections to fatd API", + "apipassword": "Password required for connections to fatd API", + "apitlscert": "Path to TLS certificate for the fatd API", + "apitlskey": "Path to TLS Key for the fatd API", "s": "IPAddr:port# of factomd API to use to access blockchain", "factomdtimeout": "Timeout for factomd API requests, 0 means never timeout", @@ -107,6 +142,7 @@ var ( "factomdpassword": "Password for API connections to factomd", //"factomdcert": "The TLS certificate that will be provided by the factomd API server", //"factomdtls": "Set to true to use TLS when accessing the factomd API", + "networkid": `Accepts "main", "test", "localnet", or four bytes in hex`, "w": "IPAddr:port# of factom-walletd API to use to access wallet", "wallettimeout": "Timeout for factom-walletd API requests, 0 means never timeout", @@ -117,15 +153,26 @@ var ( "ecadr": "Entry Credit Public Address to use to pay for Factom entries", "esadr": "Entry Credit Secret Address to use to pay for Factom entries", + + "whitelist": "Track only these chains, creating the database if needed", + "blacklist": "Do not track or sync these chains, overrides -whitelist", + "ignorenewchains": "Do not track new chains, sync existing chain databases", + "skipdbvalidation": "Skip the full validation check of all chain databases", } flags = complete.Flags{ - "-startscanheight": complete.PredictAnything, - "-factomscanretries": complete.PredictAnything, - "-debug": complete.PredictNothing, + "-startscanheight": complete.PredictAnything, + "-factomscanretries": complete.PredictAnything, + "-factomscaninterval": complete.PredictAnything, + "-debug": complete.PredictNothing, + "-disablepending": complete.PredictNothing, "-dbpath": complete.PredictFiles("*"), - "-apiaddress": complete.PredictAnything, + "-apiaddress": complete.PredictAnything, + "-apiusername": complete.PredictAnything, + "-apipassword": complete.PredictAnything, + "-apitlscert": complete.PredictFiles("*.cert"), + "-apitlskey": complete.PredictFiles("*.key"), "-s": complete.PredictAnything, "-factomdtimeout": complete.PredictAnything, @@ -141,17 +188,33 @@ var ( //"-walletcert": complete.PredictFiles("*"), //"-wallettls": complete.PredictNothing, + "-username": complete.PredictNothing, + "-password": complete.PredictNothing, + "-cert": complete.PredictFiles("*"), + "-certkey": complete.PredictFiles("*"), + "-tls": complete.PredictNothing, + "-y": complete.PredictNothing, "-installcompletion": complete.PredictNothing, "-uninstallcompletion": complete.PredictNothing, "-ecadr": predictAddress(false, 1, "-ecadr", ""), + + "-whitelist": complete.PredictAnything, + "-blacklist": complete.PredictAnything, + "-ignorenewchains": complete.PredictNothing, + + "-networkid": complete.PredictSet("mainnet", "testnet", "localnet", "0x"), + + "-skipdbvalidation": complete.PredictNothing, } - startScanHeight uint64 // We parse the flag as unsigned. - StartScanHeight int32 = -1 // We work with the signed value. - LogDebug bool - FactomScanRetries int64 = -1 + startScanHeight uint64 // We parse the flag as unsigned. + StartScanHeight int32 = -1 // We work with the signed value. + FactomScanInterval time.Duration + LogDebug bool + DisablePending bool + FactomScanRetries int64 = -1 EsAdr factom.EsAddress ECAdr factom.ECAddress @@ -161,20 +224,40 @@ var ( APIAddress string FactomClient = factom.NewClient() + NetworkID factom.NetworkID flagset map[string]bool log *logrus.Entry Completion *complete.Complete + + Whitelist, Blacklist Bytes32List + ignoreNewChains bool + SkipDBValidation bool + + HasAuth bool + Username string + Password string + + HasTLS bool + TLSCertFile string + TLSKeyFile string ) func init() { flagVar(&startScanHeight, "startscanheight") flagVar(&FactomScanRetries, "factomscanretries") + flagVar(&FactomScanInterval, "factomscaninterval") flagVar(&LogDebug, "debug") + flagVar(&DisablePending, "disablepending") flagVar(&DBPath, "dbpath") flagVar(&APIAddress, "apiaddress") + // Added in FatD authentication info. + flagVar(&Username, "apiusername") + flagVar(&Password, "apipassword") + flagVar(&TLSCertFile, "apitlscert") + flagVar(&TLSKeyFile, "apitlskey") flagVar(&ECAdr, "ecadr") flagVar(&EsAdr, "esadr") @@ -183,6 +266,7 @@ func init() { flagVar(&FactomClient.Factomd.Timeout, "factomdtimeout") flagVar(&FactomClient.Factomd.User, "factomduser") flagVar(&FactomClient.Factomd.Password, "factomdpassword") + flagVar(&NetworkID, "networkid") //flagVar(&FactomClient.Factomd.TLSCertFile, "factomdcert") //flagVar(&FactomClient.Factomd.TLSEnable, "factomdtls") @@ -193,6 +277,11 @@ func init() { //flagVar(&FactomClient.Walletd.TLSCertFile, "walletcert") //flagVar(&FactomClient.Walletd.TLSEnable, "wallettls") + flagVar(&Whitelist, "whitelist") + flagVar(&Blacklist, "blacklist") + flagVar(&ignoreNewChains, "ignorenewchains") + flagVar(&SkipDBValidation, "skipdbvalidation") + // Add flags for self installing the CLI completion tool Completion = complete.New(os.Args[0], complete.Command{Flags: flags}) Completion.CLI.InstallName = "installcompletion" @@ -211,7 +300,9 @@ func Parse() { // specified on the command line. loadFromEnv(&startScanHeight, "startscanheight") loadFromEnv(&FactomScanRetries, "factomscanretries") + loadFromEnv(&FactomScanInterval, "factomscaninterval") loadFromEnv(&LogDebug, "debug") + loadFromEnv(&DisablePending, "disablepending") loadFromEnv(&DBPath, "dbpath") @@ -237,45 +328,84 @@ func Parse() { if flagset["startscanheight"] { StartScanHeight = int32(startScanHeight) } + if !flagset["networkid"] { + NetworkID = factom.MainnetID() + } } func Validate() { // Redact private data from debug output. - factomdPassword := "\"\"" + factomdPassword := `""` if len(FactomClient.Factomd.Password) > 0 { factomdPassword = "" } - walletdPassword := "\"\"" + walletdPassword := `""` if len(FactomClient.Walletd.Password) > 0 { walletdPassword = "" } + apiPassword := `""` + if len(Password) > 0 { + apiPassword = "" + } log.Debugf("-dbpath %#v", DBPath) log.Debugf("-apiaddress %#v", APIAddress) + debugPrintln() + log.Debugf("-startscanheight %v ", StartScanHeight) log.Debugf("-factomscanretries %v ", FactomScanRetries) + log.Debugf("-factomscaninterval %v ", FactomScanInterval) debugPrintln() - log.Debugf("-s %#v", FactomClient.FactomdServer) + log.Debugf("-networkid %v", NetworkID) + log.Debugf("-s %q", FactomClient.FactomdServer) log.Debugf("-factomdtimeout %v ", FactomClient.Factomd.Timeout) - log.Debugf("-factomduser %#v", FactomClient.Factomd.User) + log.Debugf("-factomduser %q", FactomClient.Factomd.User) log.Debugf("-factomdpass %v ", factomdPassword) - //log.Debugf("-factomdcert %#v", FactomClient.Factomd.TLSCertFile) debugPrintln() log.Debugf("-w %#v", FactomClient.WalletdServer) log.Debugf("-wallettimeout %v ", FactomClient.Walletd.Timeout) log.Debugf("-walletuser %#v", FactomClient.Walletd.User) log.Debugf("-walletpass %v ", walletdPassword) - //log.Debugf("-walletcert %#v", FactomClient.Walletd.TLSCertFile) debugPrintln() - var zero factom.EsAddress - if EsAdr == zero { - EsAdr, _ = ECAdr.GetEsAddress(FactomClient) + log.Debugf("-apiusername %#v", Username) + log.Debugf("-apipassword %v ", apiPassword) + log.Debugf("-apitlscert %#v", TLSCertFile) + log.Debugf("-apitlskey %#v", TLSKeyFile) + debugPrintln() + + var err error + DBPath, err = filepath.Abs(DBPath) + if err != nil { + log.Fatalf("-dbpath %v: %v", DBPath, err) + } + DBPath += fmt.Sprintf("%c", filepath.Separator) + + if factom.Bytes32(EsAdr).IsZero() { + EsAdr, _ = ECAdr.GetEsAddress(context.TODO(), FactomClient) } else { ECAdr = EsAdr.ECAddress() } + + if IgnoreNewChains() && flagset["startscanheight"] { + log.Fatal( + "-startscanheight incompatible with -ignorenewchains and -whitelist") + } + + if len(Username) > 0 || len(Password) > 0 { + if len(Username) == 0 || len(Password) == 0 { + log.Fatal("-apiusername and -apipassword must be used together") + } + HasAuth = true + } + if len(TLSCertFile) > 0 || len(TLSKeyFile) > 0 { + if len(TLSCertFile) == 0 || len(TLSKeyFile) == 0 { + log.Fatal("-apitlscert and -apitlskey must be used together") + } + HasTLS = true + } } func flagVar(v interface{}, name string) { @@ -367,3 +497,11 @@ func setupLogger() { } log = _log.WithField("pkg", "flag") } + +func HasWhitelist() bool { + return flagset["whitelist"] +} + +func IgnoreNewChains() bool { + return ignoreNewChains || HasWhitelist() +} diff --git a/flag/addresslist.go b/internal/flag/list.go similarity index 70% rename from flag/addresslist.go rename to internal/flag/list.go index c561c89..0657196 100644 --- a/flag/addresslist.go +++ b/internal/flag/list.go @@ -25,10 +25,10 @@ package flag import ( "strings" - . "github.com/Factom-Asset-Tokens/fatd/factom" + "github.com/Factom-Asset-Tokens/factom" ) -type FAAddressList []FAAddress +type FAAddressList []factom.FAAddress func (adrs FAAddressList) String() string { if len(adrs) == 0 { @@ -41,7 +41,7 @@ func (adrs FAAddressList) String() string { return s[:len(s)-1] } -// Set appends a comma seperated list of FAAddresses. +// Set appends a comma seperated list of factom.FAAddresses. func (adrs *FAAddressList) Set(s string) error { adrStrs := strings.Split(s, ",") newAdrs := make(FAAddressList, len(adrStrs)) @@ -53,3 +53,29 @@ func (adrs *FAAddressList) Set(s string) error { *adrs = append(*adrs, newAdrs...) return nil } + +type Bytes32List []factom.Bytes32 + +func (b32s Bytes32List) String() string { + if len(b32s) == 0 { + return "" + } + var s string + for _, b32 := range b32s { + s += b32.String() + "," + } + return s[:len(s)-1] +} + +// Set appends a comma seperated list of factom.FAAddresses. +func (b32s *Bytes32List) Set(s string) error { + b32Strs := strings.Split(s, ",") + newB32s := make(Bytes32List, len(b32Strs)) + for i, b32Str := range b32Strs { + if err := newB32s[i].Set(b32Str); err != nil { + return err + } + } + *b32s = append(*b32s, newB32s...) + return nil +} diff --git a/flag/predict.go b/internal/flag/predict.go similarity index 78% rename from flag/predict.go rename to internal/flag/predict.go index 9945525..f6b28c9 100644 --- a/flag/predict.go +++ b/internal/flag/predict.go @@ -23,12 +23,12 @@ package flag import ( + "context" "flag" "os" "strings" "time" - . "github.com/Factom-Asset-Tokens/fatd/factom" "github.com/posener/complete" ) @@ -87,29 +87,24 @@ func predictAddress(fa bool, num int, flagName, suffix string) complete.PredictF func listAddresses(fa bool) []string { parseWalletFlags() - var adrs []Address + fss, ess, err := FactomClient.GetPrivateAddresses(context.Background()) + if err != nil { + os.Exit(6) + } + var adrStrs []string if fa { - as, _ := FactomClient.GetFAAddresses() - adrs = make([]Address, len(as)) - for i, adr := range as { - adrs[i] = adr + adrStrs = make([]string, len(fss)) + for i, fs := range fss { + adrStrs[i] = fs.FAAddress().String() } } else { - as, _ := FactomClient.GetECAddresses() - adrs = make([]Address, len(as)) - for i, adr := range as { - adrs[i] = adr + adrStrs = make([]string, len(ess)) + for i, es := range ess { + adrStrs[i] = es.ECAddress().String() } } - adrStrs := make([]string, len(adrs)) - for i, adr := range adrs { - adrStrs[i] = adr.String() - } return adrStrs } -func String(adr Address) string { - return adr.String() -} var cliFlags *flag.FlagSet @@ -126,16 +121,16 @@ func parseWalletFlags() { cliFlags.StringVar(&FactomClient.WalletdServer, "w", "localhost:8089", "") cliFlags.StringVar(&FactomClient.Walletd.User, "walletuser", "", "") cliFlags.StringVar(&FactomClient.Walletd.Password, "walletpassword", "", "") - //cliFlags.StringVar(&FactomClient.Walletd.TLSCertFile, "walletcert", "~/.factom/walletAPIpub.cert", "") - //cliFlags.BoolVar(&factom.RpcConfig.WalletTLSEnable, "wallettls", false, "") // flags.Parse will print warnings if it comes across an unrecognized // flag. We don't want this so we temprorarily redirect everything to // /dev/null before we call flags.Parse(). - stdout := os.Stdout - stderr := os.Stderr - os.Stdout, _ = os.Open(os.DevNull) - os.Stderr = os.Stdout + stdout, stderr := os.Stdout, os.Stderr + devNull, err := os.Open(os.DevNull) + if err != nil { + os.Exit(5) + } + os.Stdout, os.Stderr = devNull, devNull // The current command line being typed is stored in the environment // variable COMP_LINE. We split on spaces and discard the first in the @@ -143,11 +138,8 @@ func parseWalletFlags() { cliFlags.Parse(strings.Fields(os.Getenv("COMP_LINE"))[1:]) // Restore stdout and stderr. - os.Stdout = stdout - os.Stderr = stderr + os.Stdout, os.Stderr = stdout, stderr - // We want need factom-walletd to timeout or the CLI completion will - // hang and never return. This is the whole reason we use AdamSLevy's - // fork of factom. - FactomClient.Walletd.Timeout = 1 * time.Second + // We need a short timeout or the CLI completion will hang. + FactomClient.Walletd.Timeout = time.Second / 2 } diff --git a/fat/jsonlen/compact.go b/internal/jsonlen/compact.go similarity index 100% rename from fat/jsonlen/compact.go rename to internal/jsonlen/compact.go diff --git a/fat/jsonlen/compact_test.go b/internal/jsonlen/compact_test.go similarity index 100% rename from fat/jsonlen/compact_test.go rename to internal/jsonlen/compact_test.go diff --git a/fat/jsonlen/number.go b/internal/jsonlen/number.go similarity index 100% rename from fat/jsonlen/number.go rename to internal/jsonlen/number.go diff --git a/fat/jsonlen/number_test.go b/internal/jsonlen/number_test.go similarity index 100% rename from fat/jsonlen/number_test.go rename to internal/jsonlen/number_test.go diff --git a/log/log.go b/internal/log/log.go similarity index 90% rename from log/log.go rename to internal/log/log.go index 695e6fd..a9f7796 100644 --- a/log/log.go +++ b/internal/log/log.go @@ -23,7 +23,7 @@ package log import ( - "github.com/Factom-Asset-Tokens/fatd/flag" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" "github.com/sirupsen/logrus" ) @@ -32,7 +32,7 @@ type Log struct { *logrus.Entry } -func New(pkg string) Log { +func New(key string, value interface{}) Log { log := logrus.New() log.Formatter = &logrus.TextFormatter{ForceColors: true, DisableTimestamp: true, @@ -40,5 +40,5 @@ func New(pkg string) Log { if flag.LogDebug { log.SetLevel(logrus.DebugLevel) } - return Log{Entry: log.WithField("pkg", pkg)} + return Log{Entry: log.WithField(key, value)} } diff --git a/srv/doc.go b/internal/srv/doc.go similarity index 100% rename from srv/doc.go rename to internal/srv/doc.go diff --git a/internal/srv/methods.go b/internal/srv/methods.go new file mode 100644 index 0000000..56a2d75 --- /dev/null +++ b/internal/srv/methods.go @@ -0,0 +1,650 @@ +// MIT License +// +// Copyright 2018 Canonical Ledgers, LLC +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// 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 OR COPYRIGHT HOLDERS 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. + +package srv + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "time" + + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" + + "github.com/Factom-Asset-Tokens/factom" + "github.com/Factom-Asset-Tokens/fatd/api" + "github.com/Factom-Asset-Tokens/fatd/fat" + "github.com/Factom-Asset-Tokens/fatd/fat0" + "github.com/Factom-Asset-Tokens/fatd/fat1" + "github.com/Factom-Asset-Tokens/fatd/internal/db/addresses" + "github.com/Factom-Asset-Tokens/fatd/internal/db/entries" + "github.com/Factom-Asset-Tokens/fatd/internal/db/nftokens" + "github.com/Factom-Asset-Tokens/fatd/internal/engine" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" +) + +var c = flag.FactomClient + +var jsonrpc2Methods = jsonrpc2.MethodMap{ + "get-issuance": getIssuance(false), + "get-issuance-entry": getIssuance(true), + "get-transaction": getTransaction(false), + "get-transaction-entry": getTransaction(true), + "get-transactions": getTransactions(false), + "get-transactions-entry": getTransactions(true), + "get-balance": getBalance, + "get-balances": getBalances, + "get-nf-balance": getNFBalance, + "get-stats": getStats, + "get-nf-token": getNFToken, + "get-nf-tokens": getNFTokens, + + "send-transaction": sendTransaction, + + "get-daemon-tokens": getDaemonTokens, + "get-daemon-properties": getDaemonProperties, + "get-sync-status": getSyncStatus, +} + +func getIssuance(entry bool) jsonrpc2.MethodFunc { + return func(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsToken + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + if entry { + return chain.Issuance.Entry + } + return api.ResultGetIssuance{ + ParamsToken: api.ParamsToken{ + ChainID: chain.ID, + TokenID: chain.TokenID, + IssuerChainID: chain.Identity.ChainID, + }, + Hash: chain.Issuance.Entry.Hash, + Timestamp: chain.Issuance.Entry.Timestamp.Unix(), + Issuance: chain.Issuance, + } + } +} + +func getTransaction(getEntry bool) jsonrpc2.MethodFunc { + return func(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetTransaction + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + entry, err := entries.SelectValidByHash(chain.Conn, params.Hash) + if err != nil { + panic(err) + } + if !entry.IsPopulated() { + return api.ErrorTransactionNotFound + } + + if getEntry { + return entry + } + + result := api.ResultGetTransaction{ + Hash: entry.Hash, + Timestamp: entry.Timestamp.Unix(), + Pending: chain.LatestEntryTimestamp().Before(entry.Timestamp), + } + + var tx interface{} + switch chain.Issuance.Type { + case fat0.Type: + tx, err = fat0.NewTransaction(entry, + (*factom.Bytes32)(chain.Identity.ID1Key)) + case fat1.Type: + tx, err = fat1.NewTransaction(entry, + (*factom.Bytes32)(chain.Identity.ID1Key)) + default: + panic(fmt.Sprintf("unknown FAT type: %v", chain.Issuance.Type)) + } + if err != nil { + panic(err) + } + result.Tx = tx + return result + } +} + +func getTransactions(getEntry bool) jsonrpc2.MethodFunc { + return func(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetTransactions + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + if params.NFTokenID != nil && chain.Issuance.Type != fat1.Type { + err := api.ErrorTokenNotFound + err.Data = "Token Chain is not FAT-1" + return err + } + + // Lookup Txs + var nfTkns fat1.NFTokens + if params.NFTokenID != nil { + nfTkns = fat1.NFTokens{*params.NFTokenID: struct{}{}} + } + entries, err := entries.SelectByAddress(chain.Conn, params.StartHash, + params.Addresses, nfTkns, + params.ToFrom, params.Order, + *params.Page, params.Limit) + if err != nil { + panic(err) + } + if len(entries) == 0 { + return api.ErrorTransactionNotFound + } + if getEntry { + // Omit the ChainID from the response since the client + // already knows it. + for i := range entries { + entries[i].ChainID = nil + } + return entries + } + + txs := make([]api.ResultGetTransaction, len(entries)) + for i := range txs { + entry := entries[i] + var tx interface{} + switch chain.Issuance.Type { + case fat0.Type: + tx, err = fat0.NewTransaction(entry, + (*factom.Bytes32)(chain.Identity.ID1Key)) + case fat1.Type: + tx, err = fat1.NewTransaction(entry, + (*factom.Bytes32)(chain.Identity.ID1Key)) + default: + panic(fmt.Sprintf("unknown FAT type: %v", + chain.Issuance.Type)) + } + if err != nil { + panic(err) + } + txs[i].Hash = entry.Hash + txs[i].Timestamp = entry.Timestamp.Unix() + txs[i].Pending = chain.LatestEntryTimestamp(). + Before(entry.Timestamp) + txs[i].Tx = tx + } + return txs + + } +} + +func getBalance(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetBalance + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + _, balance, err := addresses.SelectIDBalance(chain.Conn, params.Address) + if err != nil { + panic(err) + } + return balance +} + +func getBalances(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetBalances + if _, _, err := validate(ctx, data, ¶ms); err != nil { + return err + } + + issuedIDs := engine.Chains.GetIssued() + balances := make(api.ResultGetBalances, len(issuedIDs)) + for _, chainID := range issuedIDs { + chain, put, err := engine.Chains.Get(ctx, chainID, params.GetIncludePending()) + if err != nil { + // ctx is done + return err + } + defer put() + _, balance, err := addresses.SelectIDBalance(chain.Conn, params.Address) + if err != nil { + panic(err) + } + if balance > 0 { + balances[*chainID] = balance + } + } + return balances +} + +func getNFBalance(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetNFBalance + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + if chain.Issuance.Type != fat1.Type { + err := api.ErrorTokenNotFound + err.Data = "Token Chain is not FAT-1" + return err + } + + tkns, err := nftokens.SelectByOwner(chain.Conn, params.Address, + *params.Page, params.Limit, params.Order) + if err != nil { + panic(err) + } + + // Empty fat1.NFTokens cannot be marshalled by design so substitute an + // empty slice. + if len(tkns) == 0 { + return []struct{}{} + } + + return tkns +} + +var coinbaseRCDHash = fat.Coinbase() + +func getStats(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsToken + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + _, burned, err := addresses.SelectIDBalance(chain.Conn, &coinbaseRCDHash) + if err != nil { + panic(err) + } + txCount, err := entries.SelectCount(chain.Conn, true) + e, err := entries.SelectLatestValid(chain.Conn) + if err != nil { + panic(err) + } + + nonZeroBalances, err := addresses.SelectCount(chain.Conn, true) + if err != nil { + panic(err) + } + + res := api.ResultGetStats{ + CirculatingSupply: chain.NumIssued - burned, + Burned: burned, + Transactions: txCount, + IssuanceTimestamp: chain.Issuance.Entry.Timestamp.Unix(), + LastTransactionTimestamp: e.Timestamp.Unix(), + NonZeroBalances: nonZeroBalances, + } + if chain.IsIssued() { + res.Issuance = &chain.Issuance + } + res.ChainID = chain.ID + res.TokenID = chain.TokenID + res.IssuerChainID = chain.Identity.ChainID + res.IssuanceHash = chain.Issuance.Entry.Hash + return res +} + +func getNFToken(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetNFToken + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + if chain.Issuance.Type != fat1.Type { + err := api.ErrorTokenNotFound + err.Data = "Token Chain is not FAT-1" + return err + } + + owner, creationHash, metadata, err := nftokens.SelectData( + chain.Conn, *params.NFTokenID) + if err != nil { + panic(err) + } + if creationHash.IsZero() { + err := api.ErrorTokenNotFound + err.Data = "No such NFTokenID has been issued" + return err + } + + res := api.ResultGetNFToken{ + NFTokenID: *params.NFTokenID, + Metadata: metadata, + Owner: &owner, + CreationTx: &creationHash, + } + + if owner == fat.Coinbase() { + res.Owner = nil + res.Burned = true + } + return res +} + +func getNFTokens(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsGetAllNFTokens + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + + if chain.Issuance.Type != fat1.Type { + err := api.ErrorTokenNotFound + err.Data = "Token Chain is not FAT-1" + return err + } + + tkns, owners, creationHashes, metadata, err := nftokens.SelectDataAll( + chain.Conn, params.Order, *params.Page, params.Limit) + if err != nil { + panic(err) + } + + res := make([]api.ResultGetNFToken, len(tkns)) + for i := range res { + res[i].NFTokenID = tkns[i] + res[i].Metadata = metadata[i] + res[i].CreationTx = &creationHashes[i] + res[i].Owner = &owners[i] + if owners[i] == fat.Coinbase() { + res[i].Owner = nil + res[i].Burned = true + } + } + + return res +} + +func sendTransaction(ctx context.Context, data json.RawMessage) interface{} { + var params api.ParamsSendTransaction + chain, put, err := validate(ctx, data, ¶ms) + if err != nil { + return err + } + defer put() + if !params.DryRun && factom.Bytes32(flag.EsAdr).IsZero() { + return api.ErrorNoEC + } + + entry := params.Entry() + var txErr error + switch chain.Issuance.Type { + case fat0.Type: + txErr, err = attemptApplyFAT0Tx(chain, entry) + case fat1.Type: + txErr, err = attemptApplyFAT1Tx(chain, entry) + } + if err != nil { + panic(err) + } + if txErr != nil { + err := api.ErrorInvalidTransaction + err.Data = txErr.Error() + return err + } + + var txID *factom.Bytes32 + if !params.DryRun { + balance, err := flag.ECAdr.GetBalance(ctx, c) + if err != nil { + panic(err) + } + cost, _ := entry.Cost() + if balance < uint64(cost) { + return api.ErrorNoEC + } + txID = new(factom.Bytes32) + var commit []byte + commit, *txID = factom.GenerateCommit( + flag.EsAdr, params.Raw, entry.Hash, false) + if err := c.Commit(ctx, commit); err != nil { + panic(err) + } + if err := c.Reveal(ctx, params.Raw); err != nil { + panic(err) + } + + } + + return struct { + ChainID *factom.Bytes32 `json:"chainid"` + TxID *factom.Bytes32 `json:"txid,omitempty"` + Hash *factom.Bytes32 `json:"entryhash"` + }{ChainID: chain.ID, TxID: txID, Hash: entry.Hash} +} +func attemptApplyFAT0Tx(chain *engine.Chain, e factom.Entry) (txErr, err error) { + // Validate tx + valid, err := entries.CheckUniquelyValid(chain.Conn, 0, e.Hash) + if err != nil { + return + } + if !valid { + txErr = fmt.Errorf("replay: hash previously marked valid") + return + } + + tx, txErr := fat0.NewTransaction(e, (*factom.Bytes32)(chain.Identity.ID1Key)) + if txErr != nil { + return + } + + if tx.IsCoinbase() { + addIssued := tx.Inputs[fat.Coinbase()] + if chain.Issuance.Supply > 0 && + int64(chain.NumIssued+addIssued) > chain.Issuance.Supply { + txErr = fmt.Errorf("coinbase exceeds max supply") + return + } + } else { + // Check all input balances + for adr, amount := range tx.Inputs { + var bal uint64 + if _, bal, err = addresses.SelectIDBalance( + chain.Conn, &adr); err != nil { + return + } + if amount > bal { + txErr = fmt.Errorf("insufficient balance: %v", adr) + return + } + } + } + return +} +func attemptApplyFAT1Tx(chain *engine.Chain, e factom.Entry) (txErr, err error) { + // Validate tx + valid, err := entries.CheckUniquelyValid(chain.Conn, 0, e.Hash) + if err != nil { + return + } + if !valid { + txErr = fmt.Errorf("replay: hash previously marked valid") + return + } + + tx, txErr := fat1.NewTransaction(e, (*factom.Bytes32)(chain.Identity.ID1Key)) + if txErr != nil { + return + } + + if tx.IsCoinbase() { + nfTkns := tx.Inputs[fat.Coinbase()] + addIssued := uint64(len(nfTkns)) + if chain.Issuance.Supply > 0 && + int64(chain.NumIssued+addIssued) > chain.Issuance.Supply { + txErr = fmt.Errorf("coinbase exceeds max supply") + return + } + for nfID := range nfTkns { + var ownerID int64 + ownerID, err = nftokens.SelectOwnerID(chain.Conn, nfID) + if err != nil { + return + } + if ownerID != -1 { + txErr = fmt.Errorf("NFTokenID{%v} already exists", nfID) + return + } + } + } else { + for adr, nfTkns := range tx.Inputs { + var adrID int64 + var bal uint64 + adrID, bal, err = addresses.SelectIDBalance(chain.Conn, &adr) + if err != nil { + return + } + amount := uint64(len(nfTkns)) + if amount > bal { + txErr = fmt.Errorf("insufficient balance: %v", adr) + return + } + for nfTkn := range nfTkns { + var ownerID int64 + ownerID, err = nftokens.SelectOwnerID( + chain.Conn, nfTkn) + if err != nil { + return + } + if ownerID == -1 { + txErr = fmt.Errorf("no such NFToken{%v}", nfTkn) + return + } + if ownerID != adrID { + txErr = fmt.Errorf("NFToken{%v} not owned by %v", + nfTkn, adr) + return + } + } + } + } + return +} +func getDaemonTokens(ctx context.Context, data json.RawMessage) interface{} { + if _, _, err := validate(ctx, data, nil); err != nil { + return err + } + + issuedIDs := engine.Chains.GetIssued() + chains := make([]api.ParamsToken, len(issuedIDs)) + for i, chainID := range issuedIDs { + // Use pending = true because a chain that has a pending + // issuance entry will not show up in this list, and no other + // pending entries will effect the data of interest. Using the + // pending state is more efficient. + chain, put, err := engine.Chains.Get(ctx, chainID, true) + if err != nil { + // ctx is done + return err + } + defer put() + chainID := chainID + chains[i].ChainID = chainID + chains[i].TokenID = chain.TokenID + chains[i].IssuerChainID = chain.Identity.ChainID + } + return chains +} + +func getDaemonProperties(ctx context.Context, data json.RawMessage) interface{} { + if _, _, err := validate(ctx, data, nil); err != nil { + return err + } + return api.ResultGetDaemonProperties{ + FatdVersion: flag.Revision, + APIVersion: APIVersion, + NetworkID: flag.NetworkID, + } +} + +func getSyncStatus(ctx context.Context, data json.RawMessage) interface{} { + sync, current := engine.GetSyncStatus() + return api.ResultGetSyncStatus{Sync: sync, Current: current} +} + +func validate(ctx context.Context, + data json.RawMessage, params api.Params) (*engine.Chain, func(), error) { + if params == nil { + if len(data) > 0 { + return nil, nil, jsonrpc2.ErrorInvalidParams( + `no "params" accepted`) + } + return nil, nil, nil + } + if len(data) == 0 { + return nil, nil, params.IsValid() + } + if err := unmarshalStrict(data, params); err != nil { + return nil, nil, jsonrpc2.ErrorInvalidParams(err) + } + if err := params.IsValid(); err != nil { + return nil, nil, err + } + if params.GetIncludePending() && flag.DisablePending { + return nil, nil, api.ErrorPendingDisabled + } + chainID := params.ValidChainID() + if chainID != nil { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + chain, put, err := engine.Chains.Get(ctx, chainID, params.GetIncludePending()) + if err != nil { + // ctx is done + return nil, nil, err + } + if put == nil { + cancel() + return nil, nil, api.ErrorTokenNotFound + } + if !chain.IsIssued() { + cancel() + put() + return nil, nil, api.ErrorTokenNotFound + } + return &chain, func() { cancel(); put() }, nil + } + return nil, nil, nil +} + +func unmarshalStrict(data []byte, v interface{}) error { + b := bytes.NewBuffer(data) + d := json.NewDecoder(b) + d.DisallowUnknownFields() + return d.Decode(v) +} diff --git a/srv/srv.go b/internal/srv/srv.go similarity index 61% rename from srv/srv.go rename to internal/srv/srv.go index 73a39a9..3c4e788 100644 --- a/srv/srv.go +++ b/internal/srv/srv.go @@ -23,11 +23,14 @@ package srv import ( + "context" "net/http" + "time" - jrpc "github.com/AdamSLevy/jsonrpc2/v11" - "github.com/Factom-Asset-Tokens/fatd/flag" - _log "github.com/Factom-Asset-Tokens/fatd/log" + jsonrpc2 "github.com/AdamSLevy/jsonrpc2/v12" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" + _log "github.com/Factom-Asset-Tokens/fatd/internal/log" + "github.com/goji/httpauth" "github.com/rs/cors" ) @@ -44,31 +47,55 @@ var ( // closed and any goroutines will exit. The done channel is closed when the // server exits for any reason. If the done channel is closed before the stop // channel is closed, an error occurred. Errors are logged. -func Start(stop <-chan struct{}) (done <-chan struct{}) { - log = _log.New("srv") +func Start(ctx context.Context) (done <-chan struct{}) { + log = _log.New("pkg", "srv") // Set up JSON RPC 2.0 handler with correct headers. - jrpc.DebugMethodFunc = true - jrpcHandler := jrpc.HTTPRequestHandler(jrpcMethods) - var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { - header := w.Header() - header.Add(FatdVersionHeaderKey, flag.Revision) - header.Add(FatdAPIVersionHeaderKey, APIVersion) - jrpcHandler(w, r) + jsonrpc2.DebugMethodFunc = true + jrpcHandler := jsonrpc2.HTTPRequestHandler(jsonrpc2Methods, log) + + var handler http.Handler = http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + header := w.Header() + header.Add(FatdVersionHeaderKey, flag.Revision) + header.Add(FatdAPIVersionHeaderKey, APIVersion) + jrpcHandler(w, r) + }) + if flag.HasAuth { + authOpts := httpauth.AuthOptions{ + User: flag.Username, + Password: flag.Password, + UnauthorizedHandler: http.HandlerFunc( + func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{}`)) + }), + } + handler = httpauth.BasicAuth(authOpts)(handler) } // Set up server. srvMux := http.NewServeMux() + srvMux.Handle("/", handler) srvMux.Handle("/v1", handler) + cors := cors.New(cors.Options{AllowedOrigins: []string{"*"}}) srv = http.Server{Handler: cors.Handler(srvMux)} + srv.Addr = flag.APIAddress // Start server. _done := make(chan struct{}) + log.Infof("Listening on %v...", flag.APIAddress) go func() { - if err := srv.ListenAndServe(); err != http.ErrServerClosed { + var err error + if flag.HasTLS { + err = srv.ListenAndServeTLS(flag.TLSCertFile, flag.TLSKeyFile) + } else { + err = srv.ListenAndServe() + } + if err != http.ErrServerClosed { log.Errorf("srv.ListenAndServe(): %v", err) } close(_done) @@ -76,9 +103,12 @@ func Start(stop <-chan struct{}) (done <-chan struct{}) { // Listen for stop signal. go func() { select { - case <-stop: - if err := srv.Shutdown(nil); err != nil { - log.Errorf("srv.Shutdown(): %v", err) + case <-ctx.Done(): + ctx, cancel := context.WithTimeout( + context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Errorf("srv.Shutdown(): %w", err) } case <-_done: } diff --git a/main.go b/main.go index b54d274..0da5dbe 100644 --- a/main.go +++ b/main.go @@ -23,13 +23,14 @@ package main import ( + "context" "os" "os/signal" - "github.com/Factom-Asset-Tokens/fatd/engine" - "github.com/Factom-Asset-Tokens/fatd/flag" - "github.com/Factom-Asset-Tokens/fatd/log" - "github.com/Factom-Asset-Tokens/fatd/srv" + "github.com/Factom-Asset-Tokens/fatd/internal/engine" + "github.com/Factom-Asset-Tokens/fatd/internal/flag" + "github.com/Factom-Asset-Tokens/fatd/internal/log" + "github.com/Factom-Asset-Tokens/fatd/internal/srv" ) func main() { os.Exit(_main()) } @@ -43,46 +44,54 @@ func _main() (ret int) { } flag.Validate() - // Set up interrupts channel. We don't want to be interrupted during - // initialization. If the signal is sent we will handle it later. + // Listen for an Interrupt and cancel everything if it occurs. + ctx, cancel := context.WithCancel(context.Background()) sigint := make(chan os.Signal, 1) signal.Notify(sigint, os.Interrupt) + go func() { + <-sigint + cancel() + }() - log := log.New("main") + log := log.New("pkg", "main") log.Info("Fatd Version: ", flag.Revision) defer log.Info("Factom Asset Token Daemon stopped.") // Engine - stopEngine := make(chan struct{}) - engineDone := engine.Start(stopEngine) + engineDone := engine.Start(ctx) if engineDone == nil { return 1 } defer func() { - close(stopEngine) // Stop engine. - <-engineDone // Wait for engine to stop. + <-engineDone // Wait for engine to stop. log.Info("State engine stopped.") }() log.Info("State engine started.") // Server - stopSrv := make(chan struct{}) - srvDone := srv.Start(stopSrv) + srvDone := srv.Start(ctx) if srvDone == nil { return 1 } defer func() { - close(stopSrv) // Stop server. - <-srvDone // Wait for server to stop. + <-srvDone // Wait for server to stop. log.Info("JSON RPC API server stopped.") }() log.Info("JSON RPC API server started.") log.Info("Factom Asset Token Daemon started.") - defer signal.Reset() // Stop handling signals once we return. + defer func() { + // Stop handling all signals so a force quit can occur with a + // second sigint. + signal.Reset() + + // Cause our sigint listener goroutine to call cancel(). + close(sigint) + }() + select { - case <-sigint: + case <-ctx.Done(): log.Infof("SIGINT: Shutting down...") return 0 case <-engineDone: // Closed if engine exits prematurely. diff --git a/revision b/revision index 796b6bc..33fe397 100755 --- a/revision +++ b/revision @@ -24,7 +24,9 @@ gitver() { ( set -o pipefail - git describe --long --tags 2>/dev/null | sed 's/\([^-]*-g\)/r\1/;s/-/./g' | tr -d '\n' || + git describe --long --tags 2>/dev/null | + sed 's/\([^-]*-g\)/r\1/;s/-/./g' | + tr -d '\n' || printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" ) } diff --git a/srv/methods.go b/srv/methods.go deleted file mode 100644 index 82b3d3f..0000000 --- a/srv/methods.go +++ /dev/null @@ -1,640 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package srv - -import ( - "bytes" - "encoding/json" - "fmt" - - jrpc "github.com/AdamSLevy/jsonrpc2/v11" - "github.com/gocraft/dbr" - "github.com/jinzhu/gorm" - - "github.com/Factom-Asset-Tokens/fatd/engine" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat0" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/Factom-Asset-Tokens/fatd/flag" - "github.com/Factom-Asset-Tokens/fatd/state" -) - -var c = flag.FactomClient - -var jrpcMethods = jrpc.MethodMap{ - "get-issuance": getIssuance(false), - "get-issuance-entry": getIssuance(true), - "get-transaction": getTransaction(false), - "get-transaction-entry": getTransaction(true), - "get-transactions": getTransactions(false), - "get-transactions-entry": getTransactions(true), - "get-balance": getBalance, - "get-balances": getBalances, - "get-nf-balance": getNFBalance, - "get-stats": getStats, - "get-nf-token": getNFToken, - "get-nf-tokens": getNFTokens, - - "send-transaction": sendTransaction, - - "get-daemon-tokens": getDaemonTokens, - "get-daemon-properties": getDaemonProperties, - "get-sync-status": getSyncStatus, -} - -type ResultGetIssuance struct { - ParamsToken - Hash *factom.Bytes32 `json:"entryhash"` - Timestamp int64 `json:"timestamp"` - Issuance fat.Issuance `json:"issuance"` -} - -func getIssuance(entry bool) jrpc.MethodFunc { - return func(data json.RawMessage) interface{} { - params := ParamsToken{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - if entry { - return chain.Issuance.Entry.Entry - } - return ResultGetIssuance{ - ParamsToken: ParamsToken{ - ChainID: chain.ID, - TokenID: chain.Token, - IssuerChainID: chain.Identity.ChainID, - }, - Hash: chain.Issuance.Hash, - Timestamp: chain.Issuance.Timestamp.Unix(), - Issuance: chain.Issuance, - } - } -} - -type ResultGetTransaction struct { - Hash *factom.Bytes32 `json:"entryhash"` - Timestamp int64 `json:"timestamp"` - Tx interface{} `json:"data"` -} - -func getTransaction(getEntry bool) jrpc.MethodFunc { - return func(data json.RawMessage) interface{} { - params := ParamsGetTransaction{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - entry, err := chain.GetEntry(params.Hash) - if err == gorm.ErrRecordNotFound { - return ErrorTransactionNotFound - } - if err != nil { - panic(err) - } - - if getEntry { - return entry - } - - switch chain.Type { - case fat0.Type: - tx := fat0.NewTransaction(entry) - if err := tx.UnmarshalEntry(); err != nil { - panic(err) - } - return ResultGetTransaction{ - Hash: tx.Hash, - Timestamp: tx.Timestamp.Unix(), - Tx: tx, - } - case fat1.Type: - tx := fat1.NewTransaction(entry) - if err := tx.UnmarshalEntry(); err != nil { - panic(err) - } - return ResultGetTransaction{ - Hash: tx.Hash, - Timestamp: tx.Timestamp.Unix(), - Tx: tx, - } - default: - panic(fmt.Sprintf("unknown FAT type: %v", chain.Type)) - } - } -} - -func getTransactions(getEntry bool) jrpc.MethodFunc { - return func(data json.RawMessage) interface{} { - params := ParamsGetTransactions{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - if params.NFTokenID != nil && chain.Type != fat1.Type { - err := ErrorTokenNotFound - err.Data = "Token Chain is not FAT-1" - return err - } - - // Lookup Txs - entries, err := chain.GetEntries(params.StartHash, - params.Addresses, params.NFTokenID, - params.ToFrom, params.Order, - params.Page, params.Limit) - if err == dbr.ErrNotFound { - return ErrorTransactionNotFound - } - if err != nil { - panic(err) - } - if len(entries) == 0 { - return ErrorTransactionNotFound - } - if getEntry { - // Omit the ChainID from the response since the client - // already knows it. - for i := range entries { - entries[i].ChainID = nil - } - return entries - } - - switch chain.Type { - case fat0.Type: - txs := make([]ResultGetTransaction, len(entries)) - for i := range txs { - tx := fat0.NewTransaction(entries[i]) - if err := tx.UnmarshalEntry(); err != nil { - panic(err) - } - txs[i].Hash = entries[i].Hash - txs[i].Timestamp = entries[i].Timestamp.Unix() - txs[i].Tx = tx - } - return txs - case fat1.Type: - txs := make([]ResultGetTransaction, len(entries)) - for i := range txs { - tx := fat1.NewTransaction(entries[i]) - if err := tx.UnmarshalEntry(); err != nil { - panic(err) - } - txs[i].Hash = entries[i].Hash - txs[i].Timestamp = entries[i].Timestamp.Unix() - txs[i].Tx = tx - } - return txs - default: - panic(fmt.Sprintf("unknown FAT type: %v", chain.Type)) - } - - } -} - -func getBalance(data json.RawMessage) interface{} { - params := ParamsGetBalance{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - adr, err := chain.GetAddress(params.Address) - if err != nil { - panic(err) - } - return adr.Balance -} - -type ResultGetBalances map[factom.Bytes32]uint64 - -func (r ResultGetBalances) MarshalJSON() ([]byte, error) { - strMap := make(map[string]uint64, len(r)) - for chainID, balance := range r { - strMap[chainID.String()] = balance - } - return json.Marshal(strMap) -} -func (r *ResultGetBalances) UnmarshalJSON(data []byte) error { - var strMap map[string]uint64 - if err := json.Unmarshal(data, &strMap); err != nil { - return err - } - *r = make(map[factom.Bytes32]uint64, len(strMap)) - var chainID factom.Bytes32 - for str, balance := range strMap { - if err := chainID.Set(str); err != nil { - return err - } - (*r)[chainID] = balance - } - return nil -} - -func getBalances(data json.RawMessage) interface{} { - params := ParamsGetBalances{} - if _, err := validate(data, ¶ms); err != nil { - return err - } - - issuedIDs := state.Chains.GetIssued() - balances := make(ResultGetBalances, len(issuedIDs)) - for _, chainID := range issuedIDs { - chain := state.Chains.Get(&chainID) - adr, err := chain.GetAddress(params.Address) - if err != nil { - panic(err) - } - if adr.Balance > 0 { - balances[chainID] = adr.Balance - } - } - return balances -} - -func getNFBalance(data json.RawMessage) interface{} { - params := ParamsGetNFBalance{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - if chain.Type != fat1.Type { - err := ErrorTokenNotFound - err.Data = "Token Chain is not FAT-1" - return err - } - - tkns, err := chain.GetNFTokensForOwner(params.Address, - params.Page, params.Limit, params.Order) - if err != nil { - panic(err) - } - - // Empty fat1.NFTokens cannot be marshalled by design so substitute an - // empty slice. - if len(tkns) == 0 { - return []struct{}{} - } - - return tkns -} - -type ResultGetStats struct { - ParamsToken - Issuance *fat.Issuance - CirculatingSupply uint64 `json:"circulating"` - Burned uint64 `json:"burned"` - Transactions int `json:"transactions"` - IssuanceTimestamp int64 `json:"issuancets"` - LastTransactionTimestamp int64 `json:"lasttxts,omitempty"` -} - -var coinbaseRCDHash = fat.Coinbase() - -func getStats(data json.RawMessage) interface{} { - params := ParamsToken{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - coinbase, err := chain.GetAddress(&coinbaseRCDHash) - if err != nil { - panic(err) - } - burned := coinbase.Balance - txs, err := chain.GetEntries(nil, nil, nil, "", "", 0, 0) - if err != nil { - panic(err) - } - - var lastTxTs int64 - if len(txs) > 0 { - lastTxTs = txs[len(txs)-1].Timestamp.Unix() - } - res := ResultGetStats{ - CirculatingSupply: chain.Issued - burned, - Burned: burned, - Transactions: len(txs), - IssuanceTimestamp: chain.Issuance.Timestamp.Unix(), - LastTransactionTimestamp: lastTxTs, - } - if chain.IsIssued() { - res.Issuance = &chain.Issuance - } - res.ChainID = chain.ID - res.TokenID = chain.Token - res.IssuerChainID = chain.Issuer - return res -} - -type ResultGetNFToken struct { - NFTokenID fat1.NFTokenID `json:"id"` - Owner *factom.FAAddress `json:"owner"` - Metadata json.RawMessage `json:"metadata,omitempty"` -} - -func getNFToken(data json.RawMessage) interface{} { - params := ParamsGetNFToken{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - if chain.Type != fat1.Type { - err := ErrorTokenNotFound - err.Data = "Token Chain is not FAT-1" - return err - } - - tkn := state.NFToken{NFTokenID: *params.NFTokenID} - if err := chain.GetNFToken(&tkn); err != nil { - if err == gorm.ErrRecordNotFound { - err := ErrorTokenNotFound - err.Data = "No such NFTokenID has been issued" - return err - } - panic(err) - } - return ResultGetNFToken{ - NFTokenID: tkn.NFTokenID, - Metadata: tkn.Metadata, - Owner: tkn.Owner.RCDHash, - } -} - -func getNFTokens(data json.RawMessage) interface{} { - params := ParamsGetAllNFTokens{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - if chain.Type != fat1.Type { - err := ErrorTokenNotFound - err.Data = "Token Chain is not FAT-1" - return err - } - - tkns, err := chain.GetAllNFTokens(params.Page, params.Limit, params.Order) - if err != nil { - panic(err) - } - - res := make([]ResultGetNFToken, len(tkns)) - for i, tkn := range tkns { - res[i].NFTokenID = tkn.NFTokenID - res[i].Metadata = tkn.Metadata - res[i].Owner = tkn.Owner.RCDHash - } - - return res -} - -func sendTransaction(data json.RawMessage) interface{} { - var zero factom.EsAddress - if flag.EsAdr == zero { - return ErrorNoEC - } - params := ParamsSendTransaction{} - chain, err := validate(data, ¶ms) - if err != nil { - return err - } - - entry := params.Entry() - hash, _ := entry.ComputeHash() - transaction, err := chain.GetEntry(&hash) - if transaction.IsPopulated() { - err := ErrorInvalidTransaction - err.Data = "duplicate transaction" - return err - } - if err != gorm.ErrRecordNotFound { - panic(err) - } - - switch chain.Type { - case fat0.Type: - if err := validFAT0Transaction(chain, entry); err != nil { - return err - } - case fat1.Type: - if err := validFAT1Transaction(chain, entry); err != nil { - return err - } - default: - panic("invalid FAT type") - } - - balance, err := flag.ECAdr.GetBalance(c) - if err != nil { - panic(err) - } - cost, err := entry.Cost() - if err != nil { - rerr := ErrorInvalidTransaction - rerr.Data = err - return rerr - } - if balance < uint64(cost) { - return ErrorNoEC - } - txID, err := entry.ComposeCreate(c, flag.EsAdr) - if err != nil { - log.Error(err) - panic(err) - } - - return struct { - ChainID *factom.Bytes32 `json:"chainid"` - TxID *factom.Bytes32 `json:"txid"` - Hash *factom.Bytes32 `json:"entryhash"` - }{ChainID: chain.ID, TxID: txID, Hash: entry.Hash} -} - -func validFAT0Transaction(chain *state.Chain, entry factom.Entry) error { - tx := fat0.NewTransaction(entry) - rpcErr := ErrorInvalidTransaction - if err := tx.Valid(chain.ID1); err != nil { - rpcErr.Data = err.Error() - return rpcErr - } - - // check balances - if tx.IsCoinbase() { - if tx.Inputs.Sum() > uint64(chain.Supply)-chain.Issued { - rpcErr.Data = "insufficient coinbase supply" - return rpcErr - } - return nil - } - for rcdHash, amount := range tx.Inputs { - adr, err := chain.GetAddress(&rcdHash) - if err != nil { - log.Error(err) - panic(err) - } - if amount > adr.Balance { - rpcErr.Data = fmt.Sprintf("insufficient balance: %v", rcdHash) - return rpcErr - } - } - return nil -} - -func validFAT1Transaction(chain *state.Chain, entry factom.Entry) error { - tx := fat1.NewTransaction(entry) - rpcErr := ErrorInvalidTransaction - if err := tx.Valid(chain.ID1); err != nil { - rpcErr.Data = err.Error() - return rpcErr - } - - for rcdHash, tkns := range tx.Inputs { - adr, err := chain.GetAddress(&rcdHash) - if err != nil { - log.Error(err) - panic(err) - } - if tx.IsCoinbase() { - if chain.Supply > 0 && - uint64(chain.Supply)-chain.Issued < uint64(len(tkns)) { - // insufficient coinbase supply - rpcErr.Data = "insufficient coinbase supply" - return rpcErr - } - for tknID := range tkns { - tkn := state.NFToken{NFTokenID: tknID} - err := chain.GetNFToken(&tkn) - if err == nil { - rpcErr.Data = fmt.Sprintf( - "NFTokenID(%v) already exists", tknID) - return rpcErr - } - if err != gorm.ErrRecordNotFound { - log.Error(err) - panic(err) - } - } - break - } - if adr.Balance < uint64(len(tkns)) { - rpcErr.Data = fmt.Sprintf("insufficient balance: %v", rcdHash) - return rpcErr - } - for tknID := range tkns { - tkn := state.NFToken{NFTokenID: tknID, OwnerID: adr.ID} - err := chain.GetNFToken(&tkn) - if err == gorm.ErrRecordNotFound { - rpcErr.Data = fmt.Sprintf( - "NFTokenID(%v) is not owned by %v", - tknID, rcdHash) - return rpcErr - } - if err != nil { - log.Error(err) - panic(err) - } - } - } - - return nil -} - -func getDaemonTokens(data json.RawMessage) interface{} { - if _, err := validate(data, nil); err != nil { - return err - } - - issuedIDs := state.Chains.GetIssued() - chains := make([]ParamsToken, len(issuedIDs)) - for i, chainID := range issuedIDs { - chain := state.Chains.Get(&chainID) - chainID := chainID - chains[i].ChainID = &chainID - chains[i].TokenID = chain.Token - chains[i].IssuerChainID = chain.Issuer - } - return chains -} - -type ResultGetDaemonProperties struct { - FatdVersion string `json:"fatdversion"` - APIVersion string `json:"apiversion"` -} - -func getDaemonProperties(data json.RawMessage) interface{} { - if _, err := validate(data, nil); err != nil { - return err - } - return ResultGetDaemonProperties{FatdVersion: flag.Revision, APIVersion: APIVersion} -} - -type ResultGetSyncStatus struct { - Sync uint32 `json:"syncheight"` - Current uint32 `json:"factomheight"` -} - -func getSyncStatus(data json.RawMessage) interface{} { - sync, current := engine.GetSyncStatus() - return ResultGetSyncStatus{Sync: sync, Current: current} -} - -func validate(data json.RawMessage, params Params) (*state.Chain, error) { - if params == nil { - if len(data) > 0 { - return nil, jrpc.InvalidParams(`no "params" accepted`) - } - return nil, nil - } - if len(data) == 0 { - return nil, params.IsValid() - } - if err := unmarshalStrict(data, params); err != nil { - return nil, jrpc.InvalidParams(err.Error()) - } - if err := params.IsValid(); err != nil { - return nil, err - } - chainID := params.ValidChainID() - if chainID != nil { - chain := state.Chains.Get(chainID) - if !chain.IsIssued() { - return nil, ErrorTokenNotFound - } - return &chain, nil - } - return nil, nil -} - -func unmarshalStrict(data []byte, v interface{}) error { - b := bytes.NewBuffer(data) - d := json.NewDecoder(b) - d.DisallowUnknownFields() - return d.Decode(v) -} diff --git a/srv/methods_test.go b/srv/methods_test.go deleted file mode 100644 index fbc093d..0000000 --- a/srv/methods_test.go +++ /dev/null @@ -1,303 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package srv - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "math/rand" - "net/http" - "testing" - "time" - - jrpc "github.com/AdamSLevy/jsonrpc2/v11" - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/flag" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -//// We make copies because the original is modified during the method call. -//var tokenParamsRes = cpResponse(TokenParamsRes) -//var tokenNotFoundRes = cpResponse(TokenNotFoundRes) -//var transactionNotFoundRes = cpResponse(TransactionNotFoundRes) -//var getTransactionParamsRes = cpResponse(GetTransactionParamsRes) -//var getTransactionsParamsRes = cpResponse(GetTransactionsParamsRes) -//var getBalanceParamsRes = cpResponse(GetBalanceParamsRes) -var getBalanceValidRes = jrpc.NewResponse(float64(0)) - -var tokenID = "invalid" - -type Test struct { - Params interface{} - Description string - Result interface{} - Error interface{} -} - -var getIssuanceTests = []Test{{ - Description: "nil params", - Error: TokenParamsError, -}, { - Params: TokenParams{}, - Description: "empty params", - Error: TokenParamsError, -}, { - Params: struct { - TokenParams - NewField string - }{TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}, NewField: "hello"}, - Description: "unknown field", - Error: jrpc.NewInvalidParamsError(`json: unknown field "NewField"`), -}, { - Params: TokenParams{ChainID: factom.NewBytes32(nil), TokenID: &tokenID}, - Description: "chain id and token id", - Error: TokenParamsError, -}, { - Params: TokenParams{ChainID: factom.NewBytes32(nil), - IssuerChainID: factom.NewBytes32(nil)}, - Description: "chain id and issuer chain id", - Error: TokenParamsError, -}, { - Params: TokenParams{ChainID: factom.NewBytes32(nil), - IssuerChainID: factom.NewBytes32(nil), TokenID: &tokenID}, - Description: "chain id and token id and issuer chain id", - Error: TokenParamsError, -}, { - Params: TokenParams{IssuerChainID: factom.NewBytes32(nil), - TokenID: &tokenID}, - Description: "token id and issuer chain id", - Error: TokenNotFoundError, -}, { - Params: TokenParams{ChainID: factom.NewBytes32(nil)}, - Description: "chain id", - Error: TokenNotFoundError, -}, -} - -var getTransactionTests = []Test{{ - Params: TokenParams{ChainID: factom.NewBytes32(nil), - IssuerChainID: factom.NewBytes32(nil)}, - Description: "no hash", - Error: GetTransactionParamsError, -}, { - Params: GetTransactionParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}, - Hash: factom.NewBytes32(nil)}, - Description: "tx not found", - Error: TransactionNotFoundError, -}, -} - -var getTransactionsTests = []Test{{ - Params: GetTransactionsParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}, - Hash: factom.NewBytes32(nil), Start: new(uint)}, - Description: "hash and start", - Error: GetTransactionsParamsError, -}, { - Params: GetTransactionsParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}, - Hash: factom.NewBytes32(nil)}, - Description: "tx not found, with hash", - Error: TransactionNotFoundError, -}, { - Params: GetTransactionsParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}, Limit: new(uint)}, - Description: "zero limit", - Error: GetTransactionsParamsError, -}, { - Params: GetTransactionsParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}}, - Description: "tx not found", - Error: TransactionNotFoundError, -}, -} - -var getBalanceTests = []Test{{ - Params: GetBalanceParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}}, - Description: "no address", - Error: GetBalanceParamsError, -}, { - Params: GetBalanceParams{ - Address: &factom.Address{}}, - Description: "no chain", - Error: GetBalanceParamsError, -}, { - Params: GetBalanceParams{TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}, - Address: &factom.Address{}}, - Description: "valid", - Result: 0, -}, -} - -var getStatsTests = []Test{{ - Description: "no params", - Error: TokenParamsError, -}, { - Params: TokenParams{ChainID: factom.NewBytes32(nil)}, - Description: "valid", - Result: struct { - Supply int `json:"supply"` - CirculatingSupply int `json:"circulating-supply"` - Transactions int `json:"transactions"` - IssuanceTimestamp int `json:"issuance-timestamp"` - LastTransactionTimestamp int `json:"last-transaction-timestamp"` - }{}, -}} - -var NFTokenID = "test" - -var getNFTokenTests = []Test{{ - Params: GetNFTokenParams{ - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}}, - Description: "no nf token param", - Error: GetNFTokenParamsError, -}, { - Params: GetNFTokenParams{NonFungibleTokenID: &NFTokenID, - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}}, - Description: "valid", - Error: TokenNotFoundError, -}} - -var sendTransactionTests = []Test{{ - Description: "no params", - Error: SendTransactionParamsError, -}, { - Params: SendTransactionParams{Content: factom.Bytes{0x00}, - ExtIDs: []factom.Bytes{{0x00}}, - TokenParams: TokenParams{ChainID: factom.NewBytes32(nil)}}, - Description: "invalid token", - Error: TokenNotFoundError, -}} - -var getDaemonTokensTests = []Test{{ - Description: "no params", - Result: []struct { - TokenID string `json:"token-id"` - IssuerID *factom.Bytes32 `json:"issuer-id"` - ChainID *factom.Bytes32 `json:"chain-id"` - }{{}}, -}, { - Params: TokenParams{ChainID: factom.NewBytes32(nil)}, - Description: "valid", - Error: NoParamsError, -}} - -var getDaemonPropertiesTests = []Test{{ - Params: []int{0}, - Description: "invalid params", - Error: NoParamsError, -}, { - Description: "no params", - Result: struct { - FatdVersion string `json:"fatd-version"` - APIVersion string `json:"api-version"` - }{FatdVersion: "0.0.0", APIVersion: "v0"}, -}} - -var methodTests = map[string][]Test{ - "get-issuance": getIssuanceTests, - "get-transaction": getTransactionTests, - "get-transactions": getTransactionsTests, - "get-balance": getBalanceTests, - "get-stats": getStatsTests, - "get-nf-token": getNFTokenTests, - "send-transaction": sendTransactionTests, - "get-daemon-tokens": getDaemonTokensTests, - "get-daemon-properties": getDaemonPropertiesTests, -} - -func TestMethods(t *testing.T) { - flag.APIAddress = "localhost:18888" - Start() - for method, tests := range methodTests { - t.Run(method, func(t *testing.T) { - for _, test := range tests { - t.Run(test.Description, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - res, err := request(method, - test.Params, &json.RawMessage{}) - assert.NoError(err) - assert.NotNil(res.ID) - if test.Result != nil { - data, err := json.Marshal(test.Result) - require.NoError(err) - result := res.Result.(*json.RawMessage) - require.NotEmpty(result) - assert.JSONEq(string(data), string(*result), - "Result") - } else { - require.NotNil(res.Error) - assert.Equal(test.Error, *res.Error, "Error") - } - }) - } - }) - } - Stop() -} - -func request(method string, params interface{}, result interface{}) (jrpc.Response, error) { - // Generate a random ID for this request. - id := rand.Uint32()%200 + 500 - - // Marshal the JSON RPC Request. - reqBytes, err := json.Marshal(jrpc.NewRequest(method, id, params)) - if err != nil { - return jrpc.Response{}, err - } - - // Make the HTTP request. - endpoint := "http://" + flag.APIAddress - req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(reqBytes)) - if err != nil { - return jrpc.Response{}, err - } - req.Header.Add("Content-Type", "application/json") - c := http.Client{Timeout: 2 * time.Second} - res, err := c.Do(req) - if err != nil { - return jrpc.Response{}, err - } - if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusBadRequest { - return jrpc.Response{}, fmt.Errorf("http: %v", res.Status) - } - - // Read the HTTP response. - resBytes, err := ioutil.ReadAll(res.Body) - if err != nil { - return jrpc.Response{}, fmt.Errorf("ioutil.ReadAll(http.Response.Body): %v", err) - } - - // Unmarshal the HTTP response into a JSON RPC response. - resJrpc := jrpc.NewResponse(result) - if err := json.Unmarshal(resBytes, &resJrpc); err != nil { - return jrpc.Response{}, fmt.Errorf("json.Unmarshal(): %v", err) - } - return resJrpc, nil -} diff --git a/state/chain.go b/state/chain.go deleted file mode 100644 index 13a6544..0000000 --- a/state/chain.go +++ /dev/null @@ -1,77 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package state - -import ( - "fmt" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/gocraft/dbr" - "github.com/jinzhu/gorm" -) - -type Chain struct { - ID *factom.Bytes32 - ChainStatus - factom.Identity - fat.Issuance - Metadata - *gorm.DB - DBR *dbr.Connection -} - -func (chain Chain) String() string { - return fmt.Sprintf("{ChainStatus:%v, ID:%v, Metadata:%+v, "+ - "fat.Identity:%+v, fat.Issuance:%+v}", - chain.ChainStatus, chain.ID, chain.Metadata, - chain.Identity, chain.Issuance) -} - -func (chain *Chain) ignore() { - chain.ID = nil - chain.ChainStatus = ChainStatusIgnored -} -func (chain *Chain) track(first factom.Entry) error { - chain.ChainStatus = ChainStatusTracked - chain.Identity.ChainID = factom.NewBytes32(first.ExtIDs[3]) - chain.Metadata.Token = string(first.ExtIDs[1]) - chain.Metadata.Issuer = chain.Identity.ChainID - chain.Metadata.Height = first.Height - - if err := chain.setupDB(); err != nil { - return err - } - log.Debugf("Tracked: %v", chain) - return nil -} -func (chain *Chain) issue(issuance fat.Issuance) error { - chain.ChainStatus = ChainStatusIssued - chain.Issuance = issuance - - if err := chain.saveIssuance(); err != nil { - return err - } - log.Debugf("Issued: %v", chain) - return nil -} diff --git a/state/chainmap.go b/state/chainmap.go deleted file mode 100644 index c7c82ed..0000000 --- a/state/chainmap.go +++ /dev/null @@ -1,70 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package state - -import ( - "sync" - - "github.com/Factom-Asset-Tokens/fatd/factom" -) - -var ( - Chains = ChainMap{m: map[factom.Bytes32]Chain{ - factom.Bytes32{31: 0x0a}: Chain{ChainStatus: ChainStatusIgnored}, - factom.Bytes32{31: 0x0c}: Chain{ChainStatus: ChainStatusIgnored}, - factom.Bytes32{31: 0x0f}: Chain{ChainStatus: ChainStatusIgnored}, - }, RWMutex: &sync.RWMutex{}} -) - -type ChainMap struct { - m map[factom.Bytes32]Chain - ids []factom.Bytes32 - *sync.RWMutex -} - -func (cm *ChainMap) set(id *factom.Bytes32, chain *Chain) { - defer cm.Unlock() - cm.Lock() - if chain.IsIssued() { - if chain, ok := cm.m[*id]; !ok || !chain.IsIssued() { - cm.ids = append(cm.ids, *id) - } - } - cm.m[*id] = *chain -} - -func (cm ChainMap) Get(id *factom.Bytes32) Chain { - defer cm.RUnlock() - cm.RLock() - chain, ok := cm.m[*id] - if !ok { - chain.ID = id - } - return chain -} - -func (cm ChainMap) GetIssued() []factom.Bytes32 { - defer cm.RUnlock() - cm.RLock() - return cm.ids -} diff --git a/state/db.go b/state/db.go deleted file mode 100644 index 113b089..0000000 --- a/state/db.go +++ /dev/null @@ -1,546 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package state - -import ( - "database/sql" - "encoding/json" - "fmt" - "io/ioutil" - "math" - "os" - - "github.com/gocraft/dbr" - "github.com/gocraft/dbr/dialect" - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/sqlite" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/Factom-Asset-Tokens/fatd/flag" - _log "github.com/Factom-Asset-Tokens/fatd/log" -) - -var ( - SavedHeight uint32 - log _log.Log - c = flag.FactomClient -) - -// Load state from all existing databases -func Load() error { - log = _log.New("state") - // Try to create the database directory in case it doesn't already - // exist. - if err := os.Mkdir(flag.DBPath, 0755); err != nil && !os.IsExist(err) { - return fmt.Errorf("os.Mkdir(%#v)", flag.DBPath) - } - - minHeight := uint32(math.MaxUint32) - - // Scan through all files within the database directory. Ignore invalid - // file names. - files, err := ioutil.ReadDir(flag.DBPath) - if err != nil { - return fmt.Errorf("ioutil.ReadDir(%#v): %v", flag.DBPath, err) - } - for _, f := range files { - fname := f.Name() - chain := Chain{ChainStatus: ChainStatusTracked} - - if chain.ID = fnameToChainID(fname); chain.ID == nil { - continue - } - log.Debugf("loading chain: %v", chain.ID) - var err error - if err = chain.open(fname); err != nil { - return err - } - if err := chain.loadMetadata(); err != nil { - return err - } - if err := chain.loadIssuance(); err != nil { - return err - } - - Chains.set(chain.ID, &chain) - if chain.Metadata.Height == 0 { - continue - } - if chain.Metadata.Height < minHeight { - minHeight = chain.Metadata.Height - } - } - - if minHeight < math.MaxUint32 { // If a minimum was found... - SavedHeight = minHeight // use it, otherwise start at zero. - } - return nil -} -func fnameToChainID(fname string) *factom.Bytes32 { - if len(fname) != dbFileNameLen || - fname[dbFileNameLen-len(dbFileExtension):dbFileNameLen] != dbFileExtension { - return nil - } - var chainID factom.Bytes32 - if err := json.Unmarshal( - []byte(fmt.Sprintf("%#v", fname[0:64])), &chainID); err != nil { - return nil - } - return &chainID -} - -func Close() { - defer Chains.Unlock() - Chains.Lock() - for _, chain := range Chains.m { - if chain.DB == nil { - continue - } - if err := chain.Close(); err != nil { - log.Errorf(err.Error()) - } - } -} - -func SaveHeight(height uint32) error { - Chains.Lock() - defer Chains.Unlock() - - for _, chain := range Chains.m { - if !chain.IsTracked() || - chain.Metadata.Height >= height || - chain.DB.Error != nil { - continue - } - if err := chain.saveHeight(height); err != nil { - return err - } - Chains.m[*chain.ID] = chain - } - SavedHeight = height - return nil -} - -const ( - dbDriver = "sqlite3" - dbFileExtension = ".sqlite3" - dbFileNameLen = len(factom.Bytes32{})*2 + len(dbFileExtension) -) - -// open a database -func (c *Chain) open(fname string) error { - fpath := flag.DBPath + "/" + fname - db, err := gorm.Open(dbDriver, fpath) - if err != nil { - return err - } - // Ensure the db gets closed if there are any issues. - defer func() { - if err != nil { - db.Close() - } - }() - db.LogMode(false) - if err = autoMigrate(db); err != nil { - return err - } - c.DB = db - c.DBR = &dbr.Connection{ - DB: db.DB(), Dialect: dialect.SQLite3, - EventReceiver: &dbr.NullEventReceiver{}, - } - return nil -} -func autoMigrate(db *gorm.DB) error { - if err := deleteEmptyTables(db); err != nil { - return fmt.Errorf("deleteEmptyTables(): %v", err) - } - if err := db.AutoMigrate(&entry{}).Error; err != nil { - return fmt.Errorf("db.AutoMigrate(&Entry{}): %v", err) - } - if err := db.AutoMigrate(&Address{}).Error; err != nil { - return fmt.Errorf("db.AutoMigrate(&Address{}): %v", err) - } - if err := db.AutoMigrate(&Metadata{}).Error; err != nil { - return fmt.Errorf("db.AutoMigrate(&Metadata{}): %v", err) - } - if err := db.AutoMigrate(&NFToken{}).Error; err != nil { - return fmt.Errorf("db.AutoMigrate(&Metadata{}): %v", err) - } - return nil -} - -func deleteEmptyTables(db *gorm.DB) error { - var tables []struct{ Name string } - var selectQry = "SELECT name FROM sqlite_master " - qry := selectQry + "WHERE type = 'table';" - if err := db.Raw(qry).Find(&tables).Error; err != nil { - return fmt.Errorf("%#v: %v", qry, err) - } - for _, table := range tables { - table := table.Name - if table == "sqlite_sequence" { - continue - } - var count int - if err := db.Table(table).Count(&count).Error; err != nil { - return fmt.Errorf("db.Table(%v).Count(): %v", table, err) - } - if count > 0 { - continue - } - qry = fmt.Sprintf("DROP TABLE %v;", table) - if err := db.Exec(qry).Error; err != nil { - return fmt.Errorf("%#v: %v", qry, err) - } - var indexes []struct{ Name string } - qry = selectQry + "WHERE type = 'index' AND tbl_name = ?;" - if err := db.Raw(qry, table). - Scan(&indexes).Error; err != nil { - return fmt.Errorf("%#v: %v", qry, err) - } - for _, index := range indexes { - index := index.Name - qry = fmt.Sprintf("DROP INDEX %v;", index) - if err := db.Exec(qry, index).Error; err != nil { - return fmt.Errorf("%#v: %v", qry, err) - } - } - } - return nil -} - -// setupDB a database for a given token chain. -func (chain *Chain) setupDB() error { - fname := fmt.Sprintf("%v%v", chain.ID, dbFileExtension) - var err error - if err = chain.open(fname); err != nil { - return err - } - // Ensure the db gets closed if there are any issues. - defer func() { - if err != nil { - chain.Close() - chain.DB = nil - } - }() - if err := chain.Create(&chain.Metadata).Error; err != nil { - return err - } - coinbase := newAddress(fat.Coinbase()) - if err := chain.Create(&coinbase).Error; err != nil { - return err - } - return nil -} - -func (chain *Chain) loadMetadata() error { - var MetadataTableCount int - if err := chain.DB.Model(&Metadata{}). - Count(&MetadataTableCount).Error; err != nil { - return err - } - if MetadataTableCount != 1 { - return fmt.Errorf(`table "metadata" must have exactly one row`) - } - if err := chain.First(&chain.Metadata).Error; err != nil { - return err - } - if !fat.ValidTokenNameIDs(fat.NameIDs(chain.Token, *chain.Issuer)) || - *chain.ID != fat.ChainID(chain.Token, *chain.Issuer) { - return fmt.Errorf(`corrupted "metadata" table for chain %v`, chain.ID) - } - chain.Identity.ChainID = chain.Metadata.Issuer - return nil -} - -func (chain *Chain) loadIssuance() error { - e := entry{} - if err := chain.First(&e).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil - } - return err - } - if !e.IsValid() { - return fmt.Errorf("corrupted entry hash") - } - chain.Issuance = fat.NewIssuance(e.Entry()) - if err := chain.Issuance.UnmarshalEntry(); err != nil { - return err - } - chain.ChainStatus = ChainStatusIssued - if err := chain.Identity.Get(c); err != nil { - return err - } - return nil -} - -func (chain *Chain) saveIssuance() error { - var entriesTableCount int - if err := chain.DB.Model(&entry{}).Count(&entriesTableCount).Error; err != nil { - return err - } - if entriesTableCount != 0 { - return fmt.Errorf(`table "entries" must be empty prior to issuance`) - } - - if _, err := chain.createEntry(chain.Issuance.Entry.Entry); err != nil { - return err - } - chain.ChainStatus = ChainStatusIssued - return nil -} -func (chain *Chain) saveMetadata() error { - if err := chain.Save(&chain.Metadata).Error; err != nil { - return err - } - return nil -} -func (chain *Chain) createEntry(fe factom.Entry) (*entry, error) { - e := newEntry(fe) - if !e.IsValid() { - return nil, fmt.Errorf("invalid hash: factom.Entry%+v", fe) - } - if err := chain.Where("hash = ?", e.Hash).First(&e).Error; err != - gorm.ErrRecordNotFound { - return nil, err - } - if err := chain.Create(&e).Error; err != nil { - return nil, err - } - return &e, nil -} - -func (chain *Chain) createNFToken(tknID fat1.NFTokenID, - metadata json.RawMessage) (*NFToken, error) { - tkn := NFToken{NFTokenID: tknID, Metadata: metadata} - if err := chain.Where("nf_token_id = ?", tknID).First(&tkn).Error; err != - gorm.ErrRecordNotFound { - return nil, err - } - if err := chain.Create(&tkn).Error; err != nil { - return nil, err - } - return &tkn, nil -} - -func (chain *Chain) saveHeight(height uint32) error { - chain.Metadata.Height = height - if err := chain.saveMetadata(); err != nil { - return err - } - return nil -} -func (chain Chain) GetAddress(rcdHash *factom.FAAddress) (Address, error) { - a := Address{RCDHash: rcdHash} - if err := chain.Where(&a).First(&a).Error; err != nil && - err != gorm.ErrRecordNotFound { - return a, err - } - return a, nil -} - -func (chain Chain) GetNFToken(tkn *NFToken) error { - qry := chain.Where("nf_token_id = ?", tkn.NFTokenID) - if tkn.OwnerID != 0 { - qry = chain.Where("nf_token_id = ? AND owner_id = ?", - tkn.NFTokenID, tkn.OwnerID) - } - if err := qry.Preload("Owner").First(tkn).Error; err != nil { - return err - } - return nil -} - -func (chain Chain) GetNFTokensForOwner(rcdHash *factom.FAAddress, - page, limit uint64, order string) (fat1.NFTokens, error) { - sess := chain.DBR.NewSession(nil) - ownerID := dbr.Select("id").From("addresses"). - Where("rcd_hash = ?", rcdHash) - stmt := sess.Select("nf_token_id").From("nf_tokens"). - Where("owner_id = ?", ownerID). - Paginate(page, limit) - - switch order { - case "", "asc": - stmt.OrderAsc("nf_token_id") - case "desc": - stmt.OrderDesc("nf_token_id") - default: - panic(fmt.Sprintf("invalid order value: %#v", order)) - } - - var dbtkns []NFToken - if _, err := stmt.Load(&dbtkns); err != nil { - return nil, err - } - tkns := make(fat1.NFTokens, len(dbtkns)) - for _, tkn := range dbtkns { - tkns[tkn.NFTokenID] = struct{}{} - } - return tkns, nil -} - -func (chain Chain) GetAllNFTokens(page, limit uint64, order string) ([]NFToken, error) { - var tkns []NFToken - if err := chain.Offset(page * limit).Limit(limit). - Order("nf_token_id " + order). - Preload("Owner").Find(&tkns).Error; err != nil { - return nil, err - } - return tkns, nil -} -func (chain *Chain) rollbackUnlessCommitted(savedChain Chain, err *error) { - // This rollback will silently fail if the db tx has already - // been committed. - rberr := chain.Rollback().Error - chain.DB = savedChain.DB - if rberr == sql.ErrTxDone { - // already committed - return - } - if rberr != nil && *err != nil { - // Report other Rollback errors if there wasn't already - // a returned error. - *err = rberr - return - } - // complete rollback - chain.Issued = savedChain.Issued -} - -func (chain Chain) GetEntry(hash *factom.Bytes32) (factom.Entry, error) { - e, err := chain.getEntry(hash) - if e == nil { - return factom.Entry{}, err - } - return e.Entry(), nil -} - -func (chain Chain) getEntry(hash *factom.Bytes32) (*entry, error) { - e := entry{} - if err := chain.Not("id = ?", 1). - Where("hash = ?", hash).First(&e).Error; err != nil { - return nil, err - } - e.Hash = hash - return &e, nil -} - -const LimitMax = 1000 - -func (chain Chain) GetEntries(hash *factom.Bytes32, - rcdHashes []factom.FAAddress, tknID *fat1.NFTokenID, - toFrom, order string, - page, limit uint64) ([]factom.Entry, error) { - if limit == 0 || limit > LimitMax { - limit = LimitMax - } - - sess := chain.DBR.NewSession(nil) - stmt := sess.Select("*").From("entries").Where("id != 1"). - Paginate(page, limit) - - var sign string - switch order { - case "", "asc": - stmt.OrderAsc("id") - sign = ">" - case "desc": - stmt.OrderDesc("id") - sign = "<" - default: - panic(fmt.Sprintf("invalid order value: %#v", order)) - } - - if hash != nil { - entryID := dbr.Select("id").From("entries").Where("hash = ?", hash) - stmt.Where(fmt.Sprintf("id %v= ?", sign), entryID) - } - - if len(rcdHashes) > 0 { - addressIDs := dbr.Select("id").From("addresses"). - Where("rcd_hash IN ?", rcdHashes) - var entryIDs dbr.Builder - switch toFrom { - case "to", "from": - entryIDs = dbr.Select("entry_id"). - From("address_transactions_"+toFrom). - Where("address_id IN ?", addressIDs) - case "": - entryIDs = dbr.UnionAll( - dbr.Select("entry_id").From("address_transactions_to"). - Where("address_id IN ?", addressIDs), - dbr.Select("entry_id").From("address_transactions_from"). - Where("address_id IN ?", addressIDs)) - default: - panic(fmt.Sprintf("invalid toFrom value: %#v", toFrom)) - } - stmt.Where("id IN ?", entryIDs) - } - - if tknID != nil { - tokenIDStmt := dbr.Select("id").From("nf_tokens"). - Where("nf_token_id == ?", tknID) - entryIDs := dbr.Select("entry_id"). - From("nf_token_transactions"). - Where("nf_token_id == ?", tokenIDStmt) - stmt.Where("id IN ?", entryIDs) - } - - var es []entry - if _, err := stmt.Load(&es); err != nil { - return nil, err - } - entries := make([]factom.Entry, len(es)) - for i, e := range es { - entries[i] = e.Entry() - } - return entries, nil -} - -type erlog struct{} - -func (e erlog) Event(eventName string) { - log.Debugf("Event: %#v", eventName) -} -func (e erlog) EventKv(eventName string, kvs map[string]string) { - log.Debugf("Event: %#v Kv: %v", eventName, kvs) -} -func (e erlog) EventErr(eventName string, err error) error { - log.Debugf("Event: %#v Err: %v", eventName, err) - return err -} -func (e erlog) EventErrKv(eventName string, err error, kvs map[string]string) error { - log.Debugf("Event: %#v Err: %v Kvs: %v", eventName, err, kvs) - return err -} -func (e erlog) Timing(eventName string, nanoseconds int64) { - log.Debugf("Event: %#v Timing: %v", eventName, nanoseconds) -} -func (e erlog) TimingKv(eventName string, nanoseconds int64, kvs map[string]string) { - log.Debugf("Event: %#v Timing: %v Kvs: %v", eventName, nanoseconds, kvs) -} diff --git a/state/doc.go b/state/doc.go deleted file mode 100644 index 8986e11..0000000 --- a/state/doc.go +++ /dev/null @@ -1,23 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package state diff --git a/state/process.go b/state/process.go deleted file mode 100644 index 8124492..0000000 --- a/state/process.go +++ /dev/null @@ -1,367 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package state - -import ( - "fmt" - - jrpc "github.com/AdamSLevy/jsonrpc2/v11" - "github.com/jinzhu/gorm" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat" - "github.com/Factom-Asset-Tokens/fatd/fat/fat0" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" -) - -func Process(eb factom.EBlock) error { - // Skip ignored chains or EBlocks for heights earlier than this chain's - // state. - chain := Chains.Get(eb.ChainID) - if chain.IsIgnored() || eb.Height <= chain.Metadata.Height { - return nil - } - return chain.Process(eb) -} - -func (chain *Chain) Process(eb factom.EBlock) error { - // Ensure changes to chain are saved in Chains. - defer Chains.set(eb.ChainID, chain) - - // Load this Entry Block. - if err := eb.Get(c); err != nil { - return fmt.Errorf("%#v.Get(c): %v", eb, err) - } - - // Check if the EBlock represents a new chain. - if eb.IsFirst() { - // Load first entry of new chain. - first := eb.Entries[0] - if err := first.Get(c); err != nil { - return fmt.Errorf("%#v.Get(c): %v", first, err) - } - - // Ignore chains with NameIDs that don't match the fat pattern. - // FAT1 will also match here. - if !fat.ValidTokenNameIDs(first.ExtIDs) { - chain.ignore() - return nil - } - - // Track this chain going forward. - if err := chain.track(first); err != nil { - return err - } - if len(eb.Entries) == 1 { - return nil - } - // The first entry cannot be a valid Issuance entry, so discard - // it and process the rest. - eb.Entries = eb.Entries[1:] - } else if !chain.IsTracked() { - // Ignore chains that are not already tracked. - chain.ignore() - return nil - } - - return chain.process(eb) -} - -func (chain *Chain) process(eb factom.EBlock) (err error) { - defer func() { - if err != nil { - return - } - chain.saveHeight(eb.Height) - }() - es := eb.Entries - if !chain.IsIssued() { - return chain.processIssuance(es) - } - return chain.processTransactions(es) -} - -// In general the following checks are ordered from cheapest to most expensive -// in terms of computation and memory. -func (chain *Chain) processIssuance(es []factom.Entry) error { - if !chain.Identity.IsPopulated() { - // The Identity may not have existed when this chain was first tracked. - // Attempt to retrieve it. - if err := chain.Identity.Get(c); err != nil { - if _, ok := err.(jrpc.Error); ok { - return nil - } - return err - } - } - // If these entries were created in a lower block height than the - // Identity entry, then none of them can be a valid Issuance entry. - if es[0].Height < chain.Identity.Height { - return nil - } - - for i, e := range es { - // If this entry was created before the Identity entry then it - // can't be valid. - if e.Timestamp.Before(chain.Identity.Timestamp) { - log.Debugf("Invalid Issuance Entry: %v, %v", e.Hash, - "created before identity") - continue - } - // Get the data for the entry. - if err := e.Get(c); err != nil { - return fmt.Errorf("Entry%+v.Get(c): %v", e, err) - } - issuance := fat.NewIssuance(e) - if err := issuance.Valid(&chain.Identity.ID1); err != nil { - log.Debugf("Invalid Issuance Entry: %v, %v", e.Hash, err) - continue - } - - if err := chain.issue(issuance); err != nil { - return err - } - - // Process remaining entries as transactions - return chain.processTransactions(es[i+1:]) - } - return nil -} - -func (chain *Chain) processTransactions(es []factom.Entry) error { - for _, e := range es { - if err := e.Get(c); err != nil { - return fmt.Errorf("Entry%v.Get(c): %v", e, err) - } - switch chain.Type { - case fat0.Type: - transaction := fat0.NewTransaction(e) - if err := transaction.Valid(chain.Identity.ID1); err != nil { - log.Debugf("Invalid Transaction Entry: %v, %v", e.Hash, err) - continue - } - if err := chain.applyFAT0(transaction); err != nil { - return err - } - case fat1.Type: - transaction := fat1.NewTransaction(e) - if err := transaction.Valid(chain.Identity.ID1); err != nil { - log.Debugf("Invalid Transaction Entry: %v, %v", e.Hash, err) - continue - } - if err := chain.applyFAT1(transaction); err != nil { - return err - } - } - } - return nil -} - -func (chain *Chain) applyFAT0(transaction fat0.Transaction) (err error) { - db := chain.Begin() - defer chain.rollbackUnlessCommitted(*chain, &err) - chain.DB = db - - entry, err := chain.createEntry(transaction.Entry.Entry) - if err != nil { - return err - } - if entry == nil { - // replayed transaction - log.Debugf("Invalid Transaction Entry: %v, replayed transaction", - transaction.Hash) - return nil - } - - for rcdHash, amount := range transaction.Inputs { - adr, err := chain.GetAddress(&rcdHash) - if err != nil { - return err - } - if err := chain.DB.Model(&adr).Association("From"). - Append(entry).Error; err != nil { - return err - } - if transaction.IsCoinbase() { - if chain.Supply > 0 && - uint64(chain.Supply)-chain.Issued < amount { - // insufficient coinbase supply - log.Debugf("Invalid Transaction Entry: %v, "+ - "insufficient coinbase supply", - entry.Hash) - return nil - } - chain.Issued += amount - if err := chain.saveMetadata(); err != nil { - return err - } - break - } - if adr.Balance < amount { - // insufficient balance - log.Debugf("Invalid Transaction Entry: %v, "+ - "insufficient balance: %v", - entry.Hash, adr.Address()) - return nil - } - adr.Balance -= amount - if err := chain.Save(&adr).Error; err != nil { - return err - } - } - - for rcdHash, amount := range transaction.Outputs { - a, err := chain.GetAddress(&rcdHash) - if err != nil { - return err - } - a.Balance += amount - if err := chain.Save(&a).Error; err != nil { - return err - } - if err := chain.DB.Model(&a).Association("To"). - Append(entry).Error; err != nil { - return err - } - } - log.Debugf("Valid Transaction Entry: %+v", transaction) - - return chain.Commit().Error -} - -func (chain *Chain) applyFAT1(transaction fat1.Transaction) (err error) { - db := chain.Begin() - defer chain.rollbackUnlessCommitted(*chain, &err) - chain.DB = db - - entry, err := chain.createEntry(transaction.Entry.Entry) - if err != nil { - return err - } - if entry == nil { - // replayed transaction - log.Debugf("Invalid Transaction Entry: %v, replayed transaction", - transaction.Hash) - return nil - } - - allTkns := make(map[fat1.NFTokenID]NFToken, transaction.Inputs.NumNFTokenIDs()) - for rcdHash, tkns := range transaction.Inputs { - adr, err := chain.GetAddress(&rcdHash) - if err != nil { - return err - } - if err := chain.DB.Model(&adr).Association("From"). - Append(entry).Error; err != nil { - return err - } - if transaction.IsCoinbase() { - if chain.Supply > 0 && - uint64(chain.Supply)-chain.Issued < uint64(len(tkns)) { - // insufficient coinbase supply - log.Debugf("Invalid Transaction Entry: %v, "+ - "insufficient coinbase supply", - entry.Hash) - return nil - } - chain.Issued += uint64(len(tkns)) - if err := chain.saveMetadata(); err != nil { - return err - } - for tknID := range tkns { - tkn, err := chain.createNFToken(tknID, - transaction.TokenMetadata[tknID]) - if err != nil { - return err - } - if tkn == nil { - log.Debugf("Invalid Transaction Entry: %v, "+ - "NFTokenID(%v) already exists", - entry.Hash, tknID) - return nil - } - allTkns[tknID] = *tkn - } - break - } - if adr.Balance < uint64(len(tkns)) { - // insufficient balance - log.Debugf("Invalid Transaction Entry: %v, "+ - "insufficient balance: %v", - entry.Hash, adr.Address()) - return nil - } - adr.Balance -= uint64(len(tkns)) - if err := chain.Save(&adr).Error; err != nil { - return err - } - for tknID := range tkns { - tkn := NFToken{NFTokenID: tknID, OwnerID: adr.ID} - err := chain.GetNFToken(&tkn) - if err == gorm.ErrRecordNotFound { - log.Debugf("Invalid Transaction Entry: %v, "+ - "NFTokenID(%v) is not owned by %v", - entry.Hash, tknID, rcdHash) - return nil - } - if err != nil { - return err - } - if err := chain.DB.Model(&tkn).Association("PreviousOwners"). - Append(&adr).Error; err != nil { - return err - } - allTkns[tknID] = tkn - } - } - - for rcdHash, tkns := range transaction.Outputs { - a, err := chain.GetAddress(&rcdHash) - if err != nil { - return err - } - a.Balance += uint64(len(tkns)) - if err := chain.Save(&a).Error; err != nil { - return err - } - if err := chain.DB.Model(&a).Association("To"). - Append(entry).Error; err != nil { - return err - } - for tknID := range tkns { - tkn := allTkns[tknID] - tkn.Owner = a - tkn.OwnerID = a.ID - if err := chain.Save(&tkn).Error; err != nil { - return err - } - if err := chain.DB.Model(&tkn).Association("Transactions"). - Append(entry).Error; err != nil { - return err - } - } - } - log.Debugf("Valid Transaction Entry: %T%+v", transaction, transaction) - - return chain.Commit().Error -} diff --git a/state/schema.go b/state/schema.go deleted file mode 100644 index c972e9f..0000000 --- a/state/schema.go +++ /dev/null @@ -1,97 +0,0 @@ -// MIT License -// -// Copyright 2018 Canonical Ledgers, LLC -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to -// deal in the Software without restriction, including without limitation the -// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// 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 OR COPYRIGHT HOLDERS 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. - -package state - -import ( - "time" - - "github.com/Factom-Asset-Tokens/fatd/factom" - "github.com/Factom-Asset-Tokens/fatd/fat/fat1" - "github.com/jinzhu/gorm" -) - -type Metadata struct { - gorm.Model - - Height uint32 - - Token string - Issuer *factom.Bytes32 - - Issued uint64 -} - -type entry struct { - ID uint64 - Hash *factom.Bytes32 `gorm:"type:VARCHAR(32); UNIQUE_INDEX; NOT NULL;"` - Timestamp time.Time `gorm:"NOT NULL;"` - Data factom.Bytes `gorm:"NOT NULL;"` -} - -func newEntry(e factom.Entry) entry { - b, _ := e.MarshalBinary() - return entry{ - Hash: e.Hash, - Timestamp: e.Timestamp, - Data: b, - } -} - -func (e entry) IsValid() bool { - return *e.Hash == factom.EntryHash(e.Data) -} - -func (e entry) Entry() factom.Entry { - fe := factom.Entry{Hash: e.Hash, Timestamp: e.Timestamp} - fe.UnmarshalBinary(e.Data) - return fe -} - -type Address struct { - gorm.Model - RCDHash *factom.FAAddress `gorm:"type:varchar(32); UNIQUE_INDEX; NOT NULL;"` - - Balance uint64 `gorm:"NOT NULL;"` - - To []entry `gorm:"many2many:address_transactions_to;"` - From []entry `gorm:"many2many:address_transactions_from;"` -} - -func newAddress(fa factom.FAAddress) Address { - return Address{RCDHash: &fa} -} - -func (a Address) Address() factom.FAAddress { - return *a.RCDHash -} - -type NFToken struct { - gorm.Model - NFTokenID fat1.NFTokenID `gorm:"UNIQUE_INDEX"` - Metadata []byte - OwnerID uint `gorm:"INDEX"` - Owner Address `gorm:"foreignkey:OwnerID"` - - PreviousOwners []Address `gorm:"many2many:nf_token_previousowners;"` - Transactions []entry `gorm:"many2many:nf_token_transactions;"` -} diff --git a/sysusers-fatd.conf b/sysusers-fatd.conf new file mode 100644 index 0000000..2625030 --- /dev/null +++ b/sysusers-fatd.conf @@ -0,0 +1 @@ +u fatd - - -