diff --git a/bns-test/src/bns-client.ts b/bns-test/src/bns-client.ts index 8788fd84d3..8ba63f4564 100644 --- a/bns-test/src/bns-client.ts +++ b/bns-test/src/bns-client.ts @@ -99,7 +99,7 @@ export class BNSClient extends Client { const tx = this.createTransaction({ method: { name: "namespace-reveal", - args: [`"${namespace}"`, `"${salt}"`, ...priceFuncAsArgs, `u${renewalRule}`, `'${nameImporter}`] + args: [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(salt)}`, ...priceFuncAsArgs, `u${renewalRule}`, `'${nameImporter}`] } }); await tx.sign(params.sender); @@ -109,18 +109,18 @@ export class BNSClient extends Client { // (name-import (namespace (buff 20)) // (name (buff 16)) - // (zonefile-content (buff 40960))) + // (zonefile-hash (buff 20))) async nameImport(namespace: string, name: string, beneficiary: string, - zonefileContent: string, + zonefileHash: string, params: { sender: string }): Promise { const tx = this.createTransaction({ method: { name: "name-import", - args: [`"${namespace}"`, `"${name}"`, `'${beneficiary}`, `"${zonefileContent}"`] + args: [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`, `'${beneficiary}`, `0x${this.toHexString(zonefileHash)}`] } }); await tx.sign(params.sender); @@ -135,7 +135,7 @@ export class BNSClient extends Client { const tx = this.createTransaction({ method: { name: "namespace-ready", - args: [`"${namespace}"`] + args: [`0x${this.toHexString(namespace)}`] } }); await tx.sign(params.sender); @@ -170,18 +170,18 @@ export class BNSClient extends Client { // (name-register (namespace (buff 20)) // (name (buff 16)) // (salt (buff 20)) - // (zonefile-content (buff 40960))) + // (zonefile-hash (buff 20))) async nameRegister(namespace: string, name: string, salt: string, - zonefileContent: string, + zonefileHash: string, params: { sender: string }): Promise { const tx = this.createTransaction({ method: { name: "name-register", - args: [`"${namespace}"`, `"${name}"`, `"${salt}"`, `"${zonefileContent}"`] + args: [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`, `0x${this.toHexString(salt)}`, `0x${this.toHexString(zonefileHash)}`] } }); await tx.sign(params.sender); @@ -191,17 +191,17 @@ export class BNSClient extends Client { // (name-update (namespace (buff 20)) // (name (buff 16)) - // (zonefile-content (buff 40960))) + // (zonefile-hash (buff 20))) async nameUpdate(namespace: string, name: string, - zonefileContent: string, + zonefileHash: string, params: { sender: string }): Promise { const tx = this.createTransaction({ method: { name: "name-update", - args: [`"${namespace}"`, `"${name}"`, `"${zonefileContent}"`] + args: [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`, `0x${this.toHexString(zonefileHash)}`] } }); await tx.sign(params.sender); @@ -212,16 +212,16 @@ export class BNSClient extends Client { // (name-transfer (namespace (buff 20)) // (name (buff 16)) // (new-owner principal) - // (zonefile-content (optional (buff 40960)))) + // (zonefile-hash (optional (buff 20)))) async nameTransfer(namespace: string, name: string, newOwner: string, - zonefileContent: string | null, + zonefileHash: string | null, params: { sender: string }): Promise { - const args = [`"${namespace}"`, `"${name}"`, `'${newOwner}`]; - args.push(zonefileContent === null ? "none" : `(some\ "${zonefileContent}")`); + const args = [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`, `'${newOwner}`]; + args.push(zonefileHash === null ? "none" : `(some\ 0x${this.toHexString(zonefileHash)})`); const tx = this.createTransaction({ method: { @@ -244,7 +244,7 @@ export class BNSClient extends Client { const tx = this.createTransaction({ method: { name: "name-revoke", - args: [`"${namespace}"`, `"${name}"`] + args: [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`] } }); await tx.sign(params.sender); @@ -256,18 +256,18 @@ export class BNSClient extends Client { // (name (buff 16)) // (stx-to-burn uint) // (new-owner (optional principal)) - // (zonefile-content (optional (buff 40960)))) + // (zonefile-hash (optional (buff 20)))) async nameRenewal(namespace: string, name: string, STX: number, newOwner: null | string, - zonefileContent: null | string, + zonefileHash: null | string, params: { sender: string }): Promise { - const args = [`"${namespace}"`, `"${name}"`, `u${STX}`]; + const args = [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`, `u${STX}`]; args.push(newOwner === null ? "none" : `(some\ '${newOwner})`); - args.push(zonefileContent === null ? "none" : `(some\ "${zonefileContent}")`); + args.push(zonefileHash === null ? "none" : `(some\ 0x${this.toHexString(zonefileHash)})`); const tx = this.createTransaction({ method: { @@ -290,7 +290,7 @@ export class BNSClient extends Client { const tx = this.createTransaction({ method: { name: "name-resolve", - args: [`"${namespace}"`, `"${name}"`] + args: [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`] } }); await tx.sign(params.sender); @@ -302,7 +302,7 @@ export class BNSClient extends Client { // (name (buff 16)) async canNameBeRegistered(namespace: string, name: string): Promise { - const args = [`"${namespace}"`, `"${name}"`]; + const args = [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`]; const query = this.createQuery({ atChaintip: false, method: { @@ -314,16 +314,50 @@ export class BNSClient extends Client { return res; } + // (get-name-price (namespace (buff 20)) + // (name (buff 16)) + async getNamePrice(namespace: string, + name: string): Promise { + const args = [`0x${this.toHexString(namespace)}`, `0x${this.toHexString(name)}`]; + const query = this.createQuery({ + atChaintip: false, + method: { + name: "get-name-price", + args: args + } + }); + const res = await this.submitQuery(query); + return res; + } + + // (get-namespace-price (namespace (buff 20)) + async getNamespacePrice(namespace: string): Promise { + const args = [`0x${this.toHexString(namespace)}`]; + const query = this.createQuery({ + atChaintip: false, + method: { + name: "get-namespace-price", + args: args + } + }); + const res = await this.submitQuery(query); + return res; + } + async mineBlocks(blocks: number) { for (let index = 0; index < blocks; index++) { const query = this.createQuery({ atChaintip: false, method: { - name: "compute-namespace-price?", + name: "get-namespace-price", args: ['0x0000'] } }); const res = await this.submitQuery(query); } } + + toHexString(input: String): String { + return Buffer.from(input).toString('hex'); + } } \ No newline at end of file diff --git a/bns-test/test/name_import.test.ts b/bns-test/test/name_import.test.ts index c970155595..619871cb06 100644 --- a/bns-test/test/name_import.test.ts +++ b/bns-test/test/name_import.test.ts @@ -139,6 +139,7 @@ describe("BNS Test Suite - NAME_IMPORT", () => { expect(receipt.success).eq(true); expect(receipt.result).include('Returned: true'); + // Bob trying to import 'dave.blockstack' should succeed receipt = await bns.nameImport(cases[0].namespace, "delta", dave, "4444", { sender: bob }) @@ -180,6 +181,12 @@ describe("BNS Test Suite - NAME_IMPORT", () => { expect(receipt.result).include('true'); await bns.mineBlocks(1); + receipt = await bns.nameImport(cases[0].namespace, "beta", bob, cases[0].zonefile, { + sender: bob + }) + expect(receipt.success).eq(false); + expect(receipt.error).include('Aborted: 1014'); + // Now that the namespace is ready, Dave should be able to update his name receipt = await bns.nameUpdate( cases[0].namespace, @@ -224,7 +231,7 @@ describe("BNS Test Suite - NAME_IMPORT", () => { sender: charlie }); expect(receipt.success).eq(true); - expect(receipt.result).include('u28'); + expect(receipt.result).include('u29'); receipt = await bns.nameRegister( cases[0].namespace, @@ -236,7 +243,6 @@ describe("BNS Test Suite - NAME_IMPORT", () => { expect(receipt.error).include('2004'); expect(receipt.success).eq(false); - // Charlie trying to renew 'alpha.blockstack' should fail receipt = await bns.nameRenewal(cases[0].namespace, "alpha", 160000, charlie, cases[0].zonefile, { sender: charlie diff --git a/bns-test/test/name_prices.test.ts b/bns-test/test/name_prices.test.ts new file mode 100644 index 0000000000..fa7ee0fe9e --- /dev/null +++ b/bns-test/test/name_prices.test.ts @@ -0,0 +1,173 @@ +import { + NativeClarityBinProvider + } from "@blockstack/clarity"; + import { + expect + } from "chai"; + import { + getTempFilePath + } from "@blockstack/clarity/lib/utils/fsUtil"; + import { + getDefaultBinaryFilePath + } from "@blockstack/clarity-native-bin"; + import { + BNSClient, + PriceFunction + } from "../src/bns-client"; + import { + mineBlocks + } from "./utils"; + + describe("BNS Test Suite - Namespace prices", () => { + let bns: BNSClient; + let provider: NativeClarityBinProvider; + + const addresses = [ + "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", + "S02J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKPVKG2CE", + "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", + "SPMQEKN07D1VHAB8XQV835E3PTY3QWZRZ5H0DM36" + ]; + const alice = addresses[0]; + const bob = addresses[1]; + const charlie = addresses[2]; + const dave = addresses[3]; + + const cases = [{ + namespace: "blockstack", + version: 1, + salt: "0000", + value: 96, + namespaceOwner: alice, + nameOwner: bob, + priceFunction: { + buckets: [7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + base: 4, + coeff: 250, + noVowelDiscount: 4, + nonAlphaDiscount: 4, + }, + renewalRule: 4294967295, + nameImporter: alice, + zonefile: "0000", + }, { + namespace: "id", + version: 1, + salt: "0000", + value: 9600, + namespaceOwner: alice, + nameOwner: bob, + priceFunction: { + buckets: [6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + base: 4, + coeff: 250, + noVowelDiscount: 20, + nonAlphaDiscount: 20, + }, + renewalRule: 52595, + nameImporter: alice, + zonefile: "1111", + }]; + + beforeEach(async () => { + const allocations = [{ + principal: alice, + amount: 10_000_000_000 + }, + { + principal: bob, + amount: 10_000_000 + }, + { + principal: charlie, + amount: 10_000_000 + }, + { + principal: dave, + amount: 10_000_000 + }, + ] + const binFile = getDefaultBinaryFilePath(); + const dbFileName = getTempFilePath(); + provider = await NativeClarityBinProvider.create(allocations, dbFileName, binFile); + bns = new BNSClient(provider); + await bns.deployContract(); + }); + + + it("Testing name prices", async () => { + // Given a launched namespace 'blockstack', owned by Alice + var receipt = await bns.namespacePreorder(cases[0].namespace, cases[0].salt, cases[0].value, { + sender: cases[0].namespaceOwner + }); + expect(receipt.success).eq(true); + expect(receipt.result).include('u12'); + + receipt = await bns.namespaceReveal( + cases[0].namespace, + cases[0].salt, + cases[0].priceFunction, + cases[0].renewalRule, + cases[0].nameImporter, { + sender: cases[0].namespaceOwner + }); + expect(receipt.success).eq(true); + expect(receipt.result).include('true'); + + receipt = await bns.namespaceReady(cases[0].namespace, { + sender: cases[0].namespaceOwner + }); + expect(receipt.success).eq(true); + expect(receipt.result).include('true'); + + // Price function used: + // buckets: [7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + // base: 4, + // coeff: 250, + // noVowelDiscount: 4, + // nonAlphaDiscount: 4, + + var receipt = await bns.getNamePrice(cases[0].namespace, "a"); + expect(receipt.success).eq(true); + // curl https://core.blockstack.org/v2/prices/names/a.blockstack + expect(receipt.result).include(`(ok u40960000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "1"); + expect(receipt.success).eq(true); + // curl https://core.blockstack.org/v2/prices/names/1.blockstack + expect(receipt.result).include(`(ok u10240000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "ab"); + expect(receipt.success).eq(true); + // curl https://core.blockstack.org/v2/prices/names/ab.blockstack + expect(receipt.result).include(`(ok u10240000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abc"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u2560000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abcd"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u640000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abcde"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u160000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abcdef"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u40000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abcdefg"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u10000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abcdefgh"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u10000)`); + + var receipt = await bns.getNamePrice(cases[0].namespace, "abcdefghi"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u10000)`); + }); + }); \ No newline at end of file diff --git a/bns-test/test/namespace_preorder.test.ts b/bns-test/test/namespace_preorder.test.ts index d98f9769fd..eaf3ea5c4c 100644 --- a/bns-test/test/namespace_preorder.test.ts +++ b/bns-test/test/namespace_preorder.test.ts @@ -114,7 +114,7 @@ describe("BNS Test Suite - NAMESPACE_PREORDER", () => { const tx = bns.createTransaction({ method: { name: "namespace-preorder", - args: [`""`, `u${cases[0].value}`] + args: [`0x`, `u${cases[0].value}`] } }); await tx.sign(cases[0].namespaceOwner); diff --git a/bns-test/test/namespace_prices.test.ts b/bns-test/test/namespace_prices.test.ts new file mode 100644 index 0000000000..16af5f74df --- /dev/null +++ b/bns-test/test/namespace_prices.test.ts @@ -0,0 +1,138 @@ +import { + NativeClarityBinProvider + } from "@blockstack/clarity"; + import { + expect + } from "chai"; + import { + getTempFilePath + } from "@blockstack/clarity/lib/utils/fsUtil"; + import { + getDefaultBinaryFilePath + } from "@blockstack/clarity-native-bin"; + import { + BNSClient, + PriceFunction + } from "../src/bns-client"; + import { + mineBlocks + } from "./utils"; + + describe("BNS Test Suite - Namespace prices", () => { + let bns: BNSClient; + let provider: NativeClarityBinProvider; + + const addresses = [ + "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7", + "S02J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKPVKG2CE", + "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", + "SPMQEKN07D1VHAB8XQV835E3PTY3QWZRZ5H0DM36" + ]; + const alice = addresses[0]; + const bob = addresses[1]; + const charlie = addresses[2]; + const dave = addresses[3]; + + const cases = [{ + namespace: "blockstack", + version: 1, + salt: "0000", + value: 96, + namespaceOwner: alice, + nameOwner: bob, + priceFunction: { + buckets: [7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + base: 4, + coeff: 250, + noVowelDiscount: 4, + nonAlphaDiscount: 4, + }, + renewalRule: 4294967295, + nameImporter: alice, + zonefile: "0000", + }, { + namespace: "id", + version: 1, + salt: "0000", + value: 9600, + namespaceOwner: alice, + nameOwner: bob, + priceFunction: { + buckets: [6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + base: 4, + coeff: 250, + noVowelDiscount: 20, + nonAlphaDiscount: 20, + }, + renewalRule: 52595, + nameImporter: alice, + zonefile: "1111", + }]; + + beforeEach(async () => { + const allocations = [{ + principal: alice, + amount: 10_000_000_000 + }, + { + principal: bob, + amount: 10_000_000 + }, + { + principal: charlie, + amount: 10_000_000 + }, + { + principal: dave, + amount: 10_000_000 + }, + ] + const binFile = getDefaultBinaryFilePath(); + const dbFileName = getTempFilePath(); + provider = await NativeClarityBinProvider.create(allocations, dbFileName, binFile); + bns = new BNSClient(provider); + await bns.deployContract(); + }); + + it("Testing namespace prices", async () => { + var receipt = await bns.getNamespacePrice("a"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u96000)`); + + var receipt = await bns.getNamespacePrice("1"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u96000)`); + + var receipt = await bns.getNamespacePrice("ab"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u9600)`); + + var receipt = await bns.getNamespacePrice("abc"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u9600)`); + + var receipt = await bns.getNamespacePrice("abcd"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u960)`); + + var receipt = await bns.getNamespacePrice("abcde"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u960)`); + + var receipt = await bns.getNamespacePrice("abcdef"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u960)`); + + var receipt = await bns.getNamespacePrice("abcdefg"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u960)`); + + var receipt = await bns.getNamespacePrice("abcdefgh"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u96)`); + + var receipt = await bns.getNamespacePrice("abcdefghi"); + expect(receipt.success).eq(true); + expect(receipt.result).include(`(ok u96)`); + }); + }); \ No newline at end of file diff --git a/bns-test/yarn.lock b/bns-test/yarn.lock index 09b330b0b2..217765a90c 100644 --- a/bns-test/yarn.lock +++ b/bns-test/yarn.lock @@ -2,73 +2,54 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" - integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.12.1", "@babel/compat-data@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.5.tgz#f56db0c4bb1bbbf221b4e81345aab4141e7cb0e9" - integrity sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg== +"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" + integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw== "@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.7.5": - version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" - integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" + integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.1" + "@babel/generator" "^7.12.10" "@babel/helper-module-transforms" "^7.12.1" - "@babel/helpers" "^7.12.1" - "@babel/parser" "^7.12.3" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.1" - "@babel/types" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.10" + "@babel/types" "^7.12.10" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" json5 "^2.1.2" lodash "^4.17.19" - resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.12.1", "@babel/generator@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" - integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== - dependencies: - "@babel/types" "^7.12.5" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/generator@^7.4.0", "@babel/generator@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a" - integrity sha512-1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA== +"@babel/generator@^7.12.10", "@babel/generator@^7.4.0": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.10.tgz#2b188fc329fb8e4f762181703beffc0fe6df3460" + integrity sha512-6mCdfhWgmqLdtTkhXjnIz0LcdVCd26wS2JXRtj2XY0u5klDsXBREA/pG5NVOuVnF2LUrBGNFtQkIqqTbblg0ww== dependencies: - "@babel/types" "^7.5.0" + "@babel/types" "^7.12.10" jsesc "^2.5.1" - lodash "^4.17.11" source-map "^0.5.0" - trim-right "^1.0.1" "@babel/helper-annotate-as-pure@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" - integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" + integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.12.10" "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": version "7.10.4" @@ -78,7 +59,7 @@ "@babel/helper-explode-assignable-expression" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.12.1": +"@babel/helper-compilation-targets@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831" integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw== @@ -100,12 +81,11 @@ "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-regexp-features-plugin@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz#18b1302d4677f9dc4740fe8c9ed96680e29d37e8" - integrity sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA== + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f" + integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.1" "@babel/helper-define-map@^7.10.4": @@ -124,15 +104,6 @@ dependencies: "@babel/types" "^7.12.1" -"@babel/helper-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" - integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== - dependencies: - "@babel/helper-get-function-arity" "^7.0.0" - "@babel/template" "^7.1.0" - "@babel/types" "^7.0.0" - "@babel/helper-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" @@ -142,19 +113,12 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" - integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== - dependencies: - "@babel/types" "^7.0.0" - "@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" + integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.12.10" "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" @@ -164,13 +128,13 @@ "@babel/types" "^7.10.4" "@babel/helper-member-expression-to-functions@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" - integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz#aa77bd0396ec8114e5e30787efa78599d874a855" + integrity sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw== dependencies: - "@babel/types" "^7.12.1" + "@babel/types" "^7.12.7" -"@babel/helper-module-imports@^7.12.1": +"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== @@ -193,24 +157,17 @@ lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz#94ca4e306ee11a7dd6e9f42823e2ac6b49881e2d" + integrity sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.12.10" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== -"@babel/helper-regex@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" - integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== - dependencies: - lodash "^4.17.19" - "@babel/helper-remap-async-to-generator@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" @@ -251,13 +208,6 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" - integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== - dependencies: - "@babel/types" "^7.4.4" - "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" @@ -278,7 +228,7 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.12.1": +"@babel/helpers@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== @@ -287,15 +237,6 @@ "@babel/traverse" "^7.12.5" "@babel/types" "^7.12.5" -"@babel/highlight@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" - integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" @@ -305,15 +246,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.12.3", "@babel/parser@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" - integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== - -"@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7" - integrity sha512-I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.10", "@babel/parser@^7.12.7", "@babel/parser@^7.4.3": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.10.tgz#824600d59e96aea26a5a2af5a9d812af05c3ae81" + integrity sha512-PJdRPwyoOqFAWfLytxrWwGrAxghCgh/yTNCYciOz8QgjflA7aZhECPZAa2VUedKg2+QMWkI0L9lynh2SNmNEgA== "@babel/plugin-proposal-async-generator-functions@^7.12.1": version "7.12.1" @@ -372,10 +308,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.12.1": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.5.tgz#b1ce757156d40ed79d59d467cb2b154a5c4149ba" - integrity sha512-UiAnkKuOrCyjZ3sYNHlRlfuZJbBHknMQ9VMwVeX97Ofwx7RpD6gS2HfqTCh8KNUQgcOm8IKt103oR4KIjh7Q8g== +"@babel/plugin-proposal-numeric-separator@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b" + integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" @@ -397,10 +333,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz#cce122203fc8a32794296fc377c6dedaf4363797" - integrity sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw== +"@babel/plugin-proposal-optional-chaining@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c" + integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" @@ -740,13 +676,12 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" -"@babel/plugin-transform-sticky-regex@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz#5c24cf50de396d30e99afc8d1c700e8bce0f5caf" - integrity sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ== +"@babel/plugin-transform-sticky-regex@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad" + integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-regex" "^7.10.4" "@babel/plugin-transform-template-literals@^7.12.1": version "7.12.1" @@ -755,10 +690,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a" - integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q== +"@babel/plugin-transform-typeof-symbol@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b" + integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" @@ -787,13 +722,13 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/preset-env@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.1.tgz#9c7e5ca82a19efc865384bb4989148d2ee5d7ac2" - integrity sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg== + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.10.tgz#ca981b95f641f2610531bd71948656306905e6ab" + integrity sha512-Gz9hnBT/tGeTE2DBNDkD7BiWRELZt+8lSysHuDwmYXUIvtwZl0zI+D6mZgXZX0u8YBlLS4tmai9ONNY9tjRgRA== dependencies: - "@babel/compat-data" "^7.12.1" - "@babel/helper-compilation-targets" "^7.12.1" - "@babel/helper-module-imports" "^7.12.1" + "@babel/compat-data" "^7.12.7" + "@babel/helper-compilation-targets" "^7.12.5" + "@babel/helper-module-imports" "^7.12.5" "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-validator-option" "^7.12.1" "@babel/plugin-proposal-async-generator-functions" "^7.12.1" @@ -803,10 +738,10 @@ "@babel/plugin-proposal-json-strings" "^7.12.1" "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" - "@babel/plugin-proposal-numeric-separator" "^7.12.1" + "@babel/plugin-proposal-numeric-separator" "^7.12.7" "@babel/plugin-proposal-object-rest-spread" "^7.12.1" "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" - "@babel/plugin-proposal-optional-chaining" "^7.12.1" + "@babel/plugin-proposal-optional-chaining" "^7.12.7" "@babel/plugin-proposal-private-methods" "^7.12.1" "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" "@babel/plugin-syntax-async-generators" "^7.8.0" @@ -848,14 +783,14 @@ "@babel/plugin-transform-reserved-words" "^7.12.1" "@babel/plugin-transform-shorthand-properties" "^7.12.1" "@babel/plugin-transform-spread" "^7.12.1" - "@babel/plugin-transform-sticky-regex" "^7.12.1" + "@babel/plugin-transform-sticky-regex" "^7.12.7" "@babel/plugin-transform-template-literals" "^7.12.1" - "@babel/plugin-transform-typeof-symbol" "^7.12.1" + "@babel/plugin-transform-typeof-symbol" "^7.12.10" "@babel/plugin-transform-unicode-escapes" "^7.12.1" "@babel/plugin-transform-unicode-regex" "^7.12.1" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.12.1" - core-js-compat "^3.6.2" + "@babel/types" "^7.12.10" + core-js-compat "^3.8.0" semver "^5.5.0" "@babel/preset-modules@^0.1.3": @@ -870,11 +805,12 @@ esutils "^2.0.2" "@babel/preset-typescript@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.12.1.tgz#86480b483bb97f75036e8864fe404cc782cc311b" - integrity sha512-hNK/DhmoJPsksdHuI/RVrcEws7GN5eamhi28JkO52MqIxU8Z0QpmiSOQxZHWOHV7I3P4UjHV97ay4TcamMA6Kw== + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz#fc7df8199d6aae747896f1e6c61fc872056632a3" + integrity sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw== dependencies: "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-validator-option" "^7.12.1" "@babel/plugin-transform-typescript" "^7.12.1" "@babel/runtime@^7.8.4": @@ -884,67 +820,34 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.1.0", "@babel/template@^7.4.0": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" - integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.4.4" - "@babel/types" "^7.4.4" - -"@babel/template@^7.10.4", "@babel/template@^7.3.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== +"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3", "@babel/template@^7.4.0": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" + integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" - integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.5", "@babel/traverse@^7.4.3": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.10.tgz#2d1f4041e8bf42ea099e5b2dc48d6a594c00017a" + integrity sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg== dependencies: "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.5" + "@babel/generator" "^7.12.10" "@babel/helper-function-name" "^7.10.4" "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.12.5" - "@babel/types" "^7.12.5" + "@babel/parser" "^7.12.10" + "@babel/types" "^7.12.10" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/traverse@^7.4.3": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485" - integrity sha512-SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.5.0" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.5.0" - "@babel/types" "^7.5.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.11" - -"@babel/types@^7.0.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab" - integrity sha512-UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+CtQ== - dependencies: - esutils "^2.0.2" - lodash "^4.17.11" - to-fast-properties "^2.0.0" - -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.12.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" - integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0", "@babel/types@^7.4.4": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.10.tgz#7965e4a7260b26f09c56bcfcb0498af1f6d9b260" + integrity sha512-sf6wboJV5mGyip2hIpDSKsr80RszPinEFjsHTalMxZAZkoQ2/2yQzxlcFN52SJqsyPfLtPmenL4g2KB3KJXPDw== dependencies: "@babel/helper-validator-identifier" "^7.10.4" lodash "^4.17.19" @@ -956,19 +859,19 @@ integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@blockstack/clarity-native-bin@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@blockstack/clarity-native-bin/-/clarity-native-bin-0.3.0.tgz#dfe4b8696d2b8ff1f623331cbc67ec53b0ef137c" - integrity sha512-X+rKBhSMLCAtEn3ILSnwhqzAVJpfWlU69H1IIGc7lsuVDQMYkHVj5rQ4OmHH5k8rwZUVplinDTNzlgjBC0x0jQ== + version "0.3.4" + resolved "https://registry.yarnpkg.com/@blockstack/clarity-native-bin/-/clarity-native-bin-0.3.4.tgz#3774def85fc5f0aa7b44c88d39094628912dfcef" + integrity sha512-YCTfxN/sovPxEf13Xk61lWYJuVn5XUoTWfIgYXl1QQRU6fjhkLNfT1HyUChgse2cORB3+bZhA6XnpBPgTxAhuQ== dependencies: fs-extra "^8.0.1" node-fetch "^2.6.0" semver "^6.1.1" - tar "^4.4.8" + unzipper "^0.10.11" "@blockstack/clarity@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@blockstack/clarity/-/clarity-0.3.0.tgz#ba856cc343778e8469fb9e6be7caf7025eb0e3af" - integrity sha512-XhoCrmNSv85Fp5BgoQbO4LK6s6c9tOQEl5svcDS/G3yjXA4LcPOu/TtZm2AcGTf6leu/9SydbWcP7JIKINTyxA== + version "0.3.4" + resolved "https://registry.yarnpkg.com/@blockstack/clarity/-/clarity-0.3.4.tgz#4d226db48e340823fd0ec36ca0541429179940d3" + integrity sha512-GuSiy/yxOMwbUPXMUkF5Zxp93s3NhqAfQIu2JHGATobEbexVR3TSc3lgSUZU5J8F6MxvG3rXVLLTDev6TGN0Iw== "@cnakazawa/watch@^1.0.3": version "1.0.4" @@ -1228,21 +1131,21 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" - integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== + version "7.11.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0" + integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg== dependencies: "@babel/types" "^7.3.0" "@types/chai@^4.1.7": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.7.tgz#1b8e33b61a8c09cbe1f85133071baa0dbf9fa71a" - integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== + version "4.2.14" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.14.tgz#44d2dd0b5de6185089375d976b4ec5caf6861193" + integrity sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ== "@types/fs-extra@^9.0.4": - version "9.0.4" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa" - integrity sha512-50GO5ez44lxK5MDH90DYHFFfqxH7+fTqEEnvguQRzJ/tY9qFrMSHLiYHite+F3SNmf7+LHC1eMXojuD+E3Qcyg== + version "9.0.5" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.5.tgz#2afb76a43a4bef80a363b94b314d0ca1694fc4f8" + integrity sha512-wr3t7wIW1c0A2BIJtdVp4EflriVaVVAsCAIHVzzh8B+GiFv9X1xeJjCs4upRXtzp7kQ6lP5xvskjoD4awJ1ZeA== dependencies: "@types/node" "*" @@ -1289,14 +1192,14 @@ pretty-format "^25.2.1" "@types/node@*": - version "12.11.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.1.tgz#1fd7b821f798b7fa29f667a1be8f3442bb8922a3" - integrity sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A== + version "14.14.13" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.13.tgz#9e425079799322113ae8477297ae6ef51b8e0cdf" + integrity sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ== "@types/node@^10": - version "10.14.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.12.tgz#0eec3155a46e6c4db1f27c3e588a205f767d622f" - integrity sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg== + version "10.17.49" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.49.tgz#ecf0b67bab4b84d0ec9b0709db4aac3824a51c4a" + integrity sha512-PGaJNs5IZz5XgzwJvL/1zRfZB7iaJ5BydZ8/Picm+lUNYoNO9iVTQkVy5eUh0dZDrx3rBOIs3GCbCRmMuYyqwg== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1333,9 +1236,9 @@ integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== "@types/yargs@^15.0.0": - version "15.0.10" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.10.tgz#0fe3c8173a0d5c3e780b389050140c3f5ea6ea74" - integrity sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ== + version "15.0.11" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.11.tgz#361d7579ecdac1527687bcebf9946621c12ab78c" + integrity sha512-jfcNBxHFYJ4nPIacsi3woz1+kvUO6s1CyeEhtnDHBjHUMNj5UlW2GynmnSgiJJEdNg9yW5C8lfoNRZrHGv5EqA== dependencies: "@types/yargs-parser" "*" @@ -1437,9 +1340,9 @@ archy@^1.0.0: integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= arg@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" - integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== argparse@^1.0.7: version "1.0.10" @@ -1608,9 +1511,9 @@ babel-preset-current-node-syntax@^0.1.2: "@babel/plugin-syntax-optional-chaining" "^7.8.3" babel-preset-current-node-syntax@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" - integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -1666,6 +1569,24 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +big-integer@^1.6.17: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== + +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1709,16 +1630,16 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" -browserslist@^4.14.5, browserslist@^4.14.6: - version "4.14.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.7.tgz#c071c1b3622c1c2e790799a37bb09473a4351cb6" - integrity sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ== +browserslist@^4.14.5, browserslist@^4.15.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b" + integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ== dependencies: - caniuse-lite "^1.0.30001157" + caniuse-lite "^1.0.30001165" colorette "^1.2.1" - electron-to-chromium "^1.3.591" + electron-to-chromium "^1.3.621" escalade "^3.1.1" - node-releases "^1.1.66" + node-releases "^1.1.67" bs-logger@0.x: version "0.2.6" @@ -1739,6 +1660,16 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-indexof-polyfill@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c" + integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A== + +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= + builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1787,10 +1718,10 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001157: - version "1.0.30001159" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001159.tgz#bebde28f893fa9594dadcaa7d6b8e2aa0299df20" - integrity sha512-w9Ph56jOsS8RL20K9cLND3u/+5WASWdhC/PPrf+V3/HsM3uHOavWOR1Xzakbv4Puo/srmPHudkmCRWM7Aq+/UA== +caniuse-lite@^1.0.30001165: + version "1.0.30001166" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001166.tgz#ca73e8747acfd16a4fd6c4b784f1b995f9698cf8" + integrity sha512-nCL4LzYK7F4mL0TjEMeYavafOGnBa98vTudH5c8lW9izUjnB99InG6pmC1ElAI1p0GlyZajv4ltUdFXvOHIl1A== capture-exit@^2.0.0: version "2.0.0" @@ -1816,6 +1747,13 @@ chai@^4.2.0: pathval "^1.1.0" type-detect "^4.0.5" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= + dependencies: + traverse ">=0.3.0 <0.4" + chalk@^2.0.0, chalk@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1846,11 +1784,6 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chownr@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" - integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A== - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -1938,10 +1871,10 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@~2.20.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" - integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +commander@^2.12.1: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commondir@^1.0.1: version "1.0.1" @@ -1958,34 +1891,27 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -convert-source-map@^1.4.0, convert-source-map@^1.7.0: +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" -convert-source-map@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" - integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== - dependencies: - safe-buffer "~5.1.1" - copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js-compat@^3.6.2: - version "3.7.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.7.0.tgz#8479c5d3d672d83f1f5ab94cf353e57113e065ed" - integrity sha512-V8yBI3+ZLDVomoWICO6kq/CD28Y4r1M7CWeO4AGpMdMfseu8bkSubBmUPySMGKRTS+su4XQ07zUkAsiu9FCWTg== +core-js-compat@^3.8.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e" + integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ== dependencies: - browserslist "^4.14.6" + browserslist "^4.15.0" semver "7.0.0" -core-util-is@1.0.2: +core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= @@ -2070,11 +1996,11 @@ debug@^2.2.0, debug@^2.3.3: ms "2.0.0" debug@^4.1.0, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: - ms "^2.1.1" + ms "2.1.2" decamelize@^1.2.0: version "1.2.0" @@ -2154,15 +2080,10 @@ diff-sequences@^25.2.6: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== -diff@^3.2.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" - integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== domexception@^1.0.1: version "1.0.1" @@ -2171,6 +2092,13 @@ domexception@^1.0.1: dependencies: webidl-conversions "^4.0.2" +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= + dependencies: + readable-stream "^2.0.2" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -2179,10 +2107,10 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.3.591: - version "1.3.599" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.599.tgz#3fbb004733f3c0dcf59934c8644ddf800b94443a" - integrity sha512-u6VGpFsIzSCNrWJb1I72SUypz3EGoBaiEgygoMkd0IOcGR3WF3je5VTx9OIRI9Qd8UOMHinLImyJFkYHTq6nsg== +electron-to-chromium@^1.3.621: + version "1.3.626" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.626.tgz#48acdf322be07feb2c1330ba05e4bf6327f721a3" + integrity sha512-7CanEvJx74EnvjHu1X8gf93KieyxvFLnqOXAH/ddjWD4RrUZYqdg3pykrQ/7t6SLI7DTsp4tfQXEfzeK5t6oAw== emoji-regex@^7.0.1: version "7.0.3" @@ -2195,9 +2123,9 @@ emoji-regex@^8.0.0: integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" - integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" @@ -2213,11 +2141,6 @@ es6-error@^4.0.1: resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== -es6-object-assign@^1.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" - integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -2256,9 +2179,9 @@ estraverse@^4.2.0: integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" - integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== exec-sh@^0.3.2: version "0.3.4" @@ -2484,13 +2407,6 @@ fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^1.0.0" -fs-minipass@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" - integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== - dependencies: - minipass "^2.2.1" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2501,6 +2417,16 @@ fsevents@^2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.2.1.tgz#1fb02ded2036a8ac288d507a65962bd87b97628d" integrity sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA== +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2561,19 +2487,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob@^7.0.0, glob@^7.1.1, glob@^7.1.3: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.2, glob@^7.1.4: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -2590,12 +2504,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b" - integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg== - -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -2605,17 +2514,6 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -handlebars@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== - dependencies: - neo-async "^2.6.0" - optimist "^0.6.1" - source-map "^0.6.1" - optionalDependencies: - uglify-js "^3.1.4" - har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -2683,12 +2581,13 @@ has@^1.0.3: function-bind "^1.1.1" hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" hasha@^3.0.0: version "3.0.0" @@ -2698,9 +2597,9 @@ hasha@^3.0.0: is-stream "^1.0.1" hosted-git-info@^2.1.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" - integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== html-encoding-sniffer@^1.0.2: version "1.0.2" @@ -2756,20 +2655,15 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== interpret@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== - -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== ip-regex@^2.1.0: version "2.1.0" @@ -2808,9 +2702,9 @@ is-ci@^2.0.0: ci-info "^2.0.0" is-core-module@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.1.0.tgz#a4cc031d9b1aca63eecbd18a650e13cb4eeab946" - integrity sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== dependencies: has "^1.0.3" @@ -2924,7 +2818,7 @@ is-wsl@^2.1.1: dependencies: is-docker "^2.0.0" -isarray@1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= @@ -3030,11 +2924,11 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^2.2.4: - version "2.2.6" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" - integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + version "2.2.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" + integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== dependencies: - handlebars "^4.1.2" + html-escaper "^2.0.0" istanbul-reports@^3.0.2: version "3.0.2" @@ -3492,9 +3386,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -3633,13 +3527,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -3658,6 +3545,11 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= + load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -3698,11 +3590,6 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.11: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - lodash@^4.17.19: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" @@ -3738,16 +3625,11 @@ make-dir@^3.0.0: dependencies: semver "^6.0.0" -make-error@1.x: +make-error@1.x, make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-error@^1.1.1: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" - integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== - makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -3755,13 +3637,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -3774,15 +3649,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - merge-source-map@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" @@ -3834,7 +3700,7 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.44.0" -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -3846,41 +3712,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -minimist@^1.1.1, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" - integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= - -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - -minipass@^2.2.1, minipass@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== - dependencies: - minipass "^2.2.1" - mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -3889,26 +3725,19 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.x: +mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" -mkdirp@^0.5.0, mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== @@ -3935,11 +3764,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -neo-async@^2.6.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" - integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== - nested-error-stacks@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" @@ -3951,9 +3775,9 @@ nice-try@^1.0.4: integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== node-fetch@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" - integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + version "2.6.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" + integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== node-int64@^0.4.0: version "0.4.0" @@ -3976,7 +3800,7 @@ node-notifier@^6.0.0: shellwords "^0.1.1" which "^1.3.1" -node-releases@^1.1.66: +node-releases@^1.1.67: version "1.1.67" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== @@ -4110,14 +3934,6 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -4135,24 +3951,10 @@ os-homedir@^1.0.1: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-each-series@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" - integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" + integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== p-finally@^1.0.0: version "1.0.0" @@ -4164,19 +3966,7 @@ p-finally@^2.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - -p-limit@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" - integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== - dependencies: - p-try "^2.0.0" - -p-limit@^2.2.0: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -4348,6 +4138,11 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + prompts@^2.0.1: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" @@ -4425,6 +4220,28 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +readable-stream@^2.0.2, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + realpath-native@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-2.0.0.tgz#7377ac429b6e1fd599dc38d08ed942d0d7beb866" @@ -4594,14 +4411,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2: - version "1.11.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" - integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== - dependencies: - path-parse "^1.0.6" - -resolve@^1.17.0: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.17.0, resolve@^1.3.2: version "1.19.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== @@ -4614,10 +4424,10 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -rimraf@^2.6.2, rimraf@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== +rimraf@2, rimraf@^2.6.2, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" @@ -4641,12 +4451,12 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.1: +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -4685,12 +4495,12 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.x, semver@^6.3.0: +semver@6.x, semver@^6.0.0, semver@^6.1.1, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -4700,16 +4510,6 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.1: - version "6.2.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" - integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== - set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -4725,6 +4525,11 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + sha.js@^2.4.11: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -4757,10 +4562,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" - integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== +shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -4772,18 +4577,17 @@ shellwords@^0.1.1: integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== shx@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.2.tgz#40501ce14eb5e0cbcac7ddbd4b325563aad8c123" - integrity sha512-aS0mWtW3T2sHAenrSrip2XGv39O9dXIFUqxAEWHEOS1ePtGIBavdPJY1kE2IHl14V/4iCbUiNDPGdyYTtmhSoA== + version "0.3.3" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.3.tgz#681a88c7c10db15abe18525349ed474f0f1e7b9f" + integrity sha512-nZJ3HFWVoTSyyB+evEKjJ1STiixGztlqwKLTUNV5KqMWtGey9fTd4KU1gdZ1X9BV6215pswQ/Jew9NsuS/fNDA== dependencies: - es6-object-assign "^1.0.3" - minimist "^1.2.0" - shelljs "^0.8.1" + minimist "^1.2.3" + shelljs "^0.8.4" signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== sisteransi@^1.0.5: version "1.0.5" @@ -4836,10 +4640,10 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.12, source-map-support@^0.5.6: - version "0.5.12" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" - integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== +source-map-support@^0.5.12, source-map-support@^0.5.17, source-map-support@^0.5.6: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -4865,9 +4669,9 @@ source-map@^0.7.3: integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== spawn-wrap@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" - integrity sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg== + version "1.4.3" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.3.tgz#81b7670e170cca247d80bf5faf0cfb713bdcf848" + integrity sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw== dependencies: foreground-child "^1.5.6" mkdirp "^0.5.0" @@ -4877,30 +4681,30 @@ spawn-wrap@^1.4.2: which "^1.3.0" spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz#75ecd1a88de8c184ef015eafb51b5b48bfd11bb1" - integrity sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA== + version "3.0.7" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" + integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -4930,9 +4734,9 @@ sshpk@^1.7.0: tweetnacl "~0.14.0" stack-utils@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.3.tgz#db7a475733b5b8bf6521907b18891d29006f7751" - integrity sha512-WldO+YmqhEpjp23eHZRhOT1NQF51STsbxZ+/AdpFD+EhheFxAe5d0WoK4DQVJkSHacPrJJX3OqRAl9CgHf78pg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.4.tgz#4b600971dcfc6aed0cbdf2a8268177cc916c87c8" + integrity sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w== dependencies: escape-string-regexp "^2.0.0" @@ -4975,6 +4779,20 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -5043,19 +4861,6 @@ symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tar@^4.4.8: - version "4.4.10" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" - integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== - dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.5" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" - terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" @@ -5154,10 +4959,10 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -trim-right@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" - integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= ts-jest@^25.3.0: version "25.5.1" @@ -5176,31 +4981,31 @@ ts-jest@^25.3.0: yargs-parser "18.x" ts-node@^8.2.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.3.0.tgz#e4059618411371924a1fb5f3b125915f324efb57" - integrity sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ== + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.6" - yn "^3.0.0" + source-map-support "^0.5.17" + yn "3.1.1" tslib@^1.8.0, tslib@^1.8.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslint@^5.17.0: - version "5.18.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6" - integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w== + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== dependencies: "@babel/code-frame" "^7.0.0" builtin-modules "^1.1.1" chalk "^2.3.0" commander "^2.12.1" - diff "^3.2.0" + diff "^4.0.1" glob "^7.1.1" js-yaml "^3.13.1" minimatch "^3.0.4" @@ -5264,17 +5069,9 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" typescript@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" - integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== - -uglify-js@^3.1.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== - dependencies: - commander "~2.20.0" - source-map "~0.6.1" + version "4.1.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7" + integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg== unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" @@ -5332,6 +5129,22 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +unzipper@^0.10.11: + version "0.10.11" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e" + integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + graceful-fs "^4.2.2" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" + uri-js@^4.2.2: version "4.4.0" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" @@ -5349,10 +5162,15 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + uuid@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" - integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== v8-to-istanbul@^4.1.3: version "4.1.4" @@ -5453,11 +5271,6 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -5501,9 +5314,9 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.0.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" - integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== + version "7.4.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.1.tgz#a333be02696bd0e54cea0434e21dcc8a9ac294bb" + integrity sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ== xml-name-validator@^3.0.0: version "3.0.0" @@ -5516,20 +5329,15 @@ xmlchars@^2.1.1: integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" - integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== - yargs-parser@18.x, yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -5538,30 +5346,29 @@ yargs-parser@18.x, yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.0.0, yargs-parser@^13.1.0: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== +yargs-parser@^13.0.0, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" yargs@^13.2.2: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== dependencies: cliui "^5.0.0" find-up "^3.0.0" get-caller-file "^2.0.1" - os-locale "^3.1.0" require-directory "^2.1.1" require-main-filename "^2.0.0" set-blocking "^2.0.0" string-width "^3.0.0" which-module "^2.0.0" y18n "^4.0.0" - yargs-parser "^13.1.0" + yargs-parser "^13.1.2" yargs@^15.3.1: version "15.4.1" @@ -5580,7 +5387,7 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yn@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.0.tgz#fcbe2db63610361afcc5eb9e0ac91e976d046114" - integrity sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg== +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/sip/sip-001-burn-election.md b/sip/sip-001-burn-election.md index 606901b963..7d022e2f84 100644 --- a/sip/sip-001-burn-election.md +++ b/sip/sip-001-burn-election.md @@ -911,8 +911,8 @@ with the following data: ``` 0 2 3 35 67 71 73 77 79 80 |------|--|-------------|---------------|------|------|-----|-----|-----| - magic op block hash new seed parent parent key key memo - block txoff block txoff + magic op block hash new seed parent parent key key burn parent + block txoff block txoff modulus ``` Where `op = [` and: @@ -923,7 +923,11 @@ Where `op = [` and: * `parent_txoff` is the vtxindex for this block's parent's block commit. * `key_block` is the burn block height of the miner's VRF key registration * `key_txoff` is the vtxindex for this miner's VRF key registration -* `memo` is a short field for including a miner memo +* `burn_parent_modulus` is the burn block height at which this leader block commit + was created modulo `BURN_COMMITMENT_WINDOW` (=6). That is, if the block commit is + included in the intended burn block then this value should be equal to: + `(commit_burn_height - 1) % 6`. This field is used to link burn commitments from + the same miner together even if a commitment was included in a late burn block. The second output is the burn commitment. It must send funds to the canonical burn address. diff --git a/src/blockstack_cli.rs b/src/blockstack_cli.rs index 5e01d5a3cd..78e657fd2f 100644 --- a/src/blockstack_cli.rs +++ b/src/blockstack_cli.rs @@ -276,13 +276,13 @@ fn make_standard_single_sig_tx( payload: TransactionPayload, publicKey: &StacksPublicKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, ) -> StacksTransaction { let mut spending_condition = TransactionSpendingCondition::new_singlesig_p2pkh(publicKey.clone()) .expect("Failed to create p2pkh spending condition from public key."); spending_condition.set_nonce(nonce); - spending_condition.set_fee_rate(fee_rate); + spending_condition.set_tx_fee(tx_fee); let auth = TransactionAuth::Standard(spending_condition); let mut tx = StacksTransaction::new(version, auth, payload); tx.chain_id = chain_id; @@ -360,7 +360,7 @@ fn handle_contract_publish( } let anchor_mode = parse_anchor_mode(&mut args, PUBLISH_USAGE)?; let sk_publisher = &args[0]; - let fee_rate = args[1].parse()?; + let tx_fee = args[1].parse()?; let nonce = args[2].parse()?; let contract_name = &args[3]; let contract_file = &args[4]; @@ -382,7 +382,7 @@ fn handle_contract_publish( payload.into(), &StacksPublicKey::from_private(&sk_publisher), nonce, - fee_rate, + tx_fee, ); unsigned_tx.anchor_mode = anchor_mode; @@ -417,7 +417,7 @@ fn handle_contract_call( } let anchor_mode = parse_anchor_mode(&mut args, CALL_USAGE)?; let sk_origin = &args[0]; - let fee_rate = args[1].parse()?; + let tx_fee = args[1].parse()?; let nonce = args[2].parse()?; let contract_address = &args[3]; let contract_name = &args[4]; @@ -468,7 +468,7 @@ fn handle_contract_call( payload.into(), &StacksPublicKey::from_private(&sk_origin), nonce, - fee_rate, + tx_fee, ); unsigned_tx.anchor_mode = anchor_mode; @@ -506,7 +506,7 @@ fn handle_token_transfer( let anchor_mode = parse_anchor_mode(&mut args, TOKEN_TRANSFER_USAGE)?; let sk_origin = StacksPrivateKey::from_hex(&args[0])?; - let fee_rate = args[1].parse()?; + let tx_fee = args[1].parse()?; let nonce = args[2].parse()?; let recipient_address = PrincipalData::parse(&args[3]).map_err(|_e| "Failed to parse recipient")?; @@ -530,7 +530,7 @@ fn handle_token_transfer( payload, &StacksPublicKey::from_private(&sk_origin), nonce, - fee_rate, + tx_fee, ); unsigned_tx.anchor_mode = anchor_mode; diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index 6f94b33041..c3ac039d02 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -57,8 +57,8 @@ use burnchains::bitcoin::{BitcoinInputType, BitcoinTxInput, BitcoinTxOutput}; use chainstate::burn::db::sortdb::{PoxId, SortitionDB, SortitionHandleConn, SortitionHandleTx}; use chainstate::burn::distribution::BurnSamplePoint; use chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, StackStxOp, - TransferStxOp, UserBurnSupportOp, + leader_block_commit::MissedBlockCommit, BlockstackOperationType, LeaderBlockCommitOp, + LeaderKeyRegisterOp, PreStxOp, StackStxOp, TransferStxOp, UserBurnSupportOp, }; use chainstate::burn::{BlockSnapshot, Opcodes}; @@ -111,6 +111,7 @@ impl BurnchainStateTransition { sort_tx: &mut SortitionHandleTx, parent_snapshot: &BlockSnapshot, block_ops: &Vec, + missed_commits: &Vec, sunset_end: u64, ) -> Result { // block commits and support burns discovered in this block. @@ -161,7 +162,20 @@ impl BurnchainStateTransition { // assemble the commit windows let mut windowed_block_commits = vec![block_commits]; - let mut windowed_user_burns = vec![user_burns]; + let mut windowed_missed_commits = vec![]; + + // build a map of intended sortition -> missed commit for the missed commits + // discovered in this block + let mut missed_commits_map: HashMap<_, Vec<_>> = HashMap::new(); + for missed in missed_commits.iter() { + if let Some(commits_at_sortition) = + missed_commits_map.get_mut(&missed.intended_sortition) + { + commits_at_sortition.push(missed); + } else { + missed_commits_map.insert(missed.intended_sortition.clone(), vec![missed]); + } + } for blocks_back in 0..(MINING_COMMITMENT_WINDOW - 1) { if parent_snapshot.block_height < (blocks_back as u64) { @@ -179,15 +193,19 @@ impl BurnchainStateTransition { sort_tx.tx(), &sortition_id, )?); - windowed_user_burns.push(SortitionDB::get_user_burns_by_block( - sort_tx.tx(), - &sortition_id, - )?); + let mut missed_commits_at_height = + SortitionDB::get_missed_commits_by_intended(sort_tx.tx(), &sortition_id)?; + if let Some(missed_commit_in_block) = missed_commits_map.remove(&sortition_id) { + missed_commits_at_height + .extend(missed_commit_in_block.into_iter().map(|x| x.clone())); + } + + windowed_missed_commits.push(missed_commits_at_height); } // reverse vecs so that windows are in ascending block height order windowed_block_commits.reverse(); - windowed_user_burns.reverse(); + windowed_missed_commits.reverse(); // figure out if the PoX sunset finished during the window let window_end_height = parent_snapshot.block_height + 1; @@ -206,7 +224,7 @@ impl BurnchainStateTransition { // consume the same key will be rejected) let burn_dist = BurnSamplePoint::make_min_median_distribution( windowed_block_commits, - windowed_user_burns, + windowed_missed_commits, sunset_finished_at, ); @@ -425,6 +443,11 @@ impl Burnchain { return 0; } + // no sunset burn needed in prepare phase -- it's already getting burnt + if self.is_in_prepare_phase(burn_height) { + return 0; + } + let reward_cycle_height = self.reward_cycle_to_block_height( self.block_height_to_reward_cycle(burn_height) .expect("BUG: Sunset start is less than first_block_height"), @@ -472,6 +495,23 @@ impl Burnchain { ) } + pub fn is_in_prepare_phase(&self, block_height: u64) -> bool { + if block_height <= self.first_block_height { + // not a reward cycle start if we're the first block after genesis. + false + } else { + let effective_height = block_height - self.first_block_height; + let reward_index = effective_height % (self.pox_constants.reward_cycle_length as u64); + + // NOTE: first block in reward cycle is mod 1, so mod 0 is the last block in the + // prepare phase. + reward_index == 0 + || reward_index + > ((self.pox_constants.reward_cycle_length - self.pox_constants.prepare_length) + as u64) + } + } + pub fn regtest(working_dir: &str) -> Burnchain { let ret = Burnchain::new(working_dir, &"bitcoin".to_string(), &"regtest".to_string()).unwrap(); @@ -643,10 +683,10 @@ impl Burnchain { /// `pre_stx_op_map` should contain any valid PreStxOps that occurred before /// the currently-being-evaluated tx in the same burn block. pub fn classify_transaction( + burnchain: &Burnchain, burnchain_db: &BurnchainDB, block_header: &BurnchainBlockHeader, burn_tx: &BurnchainTransaction, - sunset_end_ht: u64, pre_stx_op_map: &HashMap, ) -> Option { match burn_tx.opcode() { @@ -665,7 +705,7 @@ impl Burnchain { } } x if x == Opcodes::LeaderBlockCommit as u8 => { - match LeaderBlockCommitOp::from_tx(block_header, burn_tx, sunset_end_ht) { + match LeaderBlockCommitOp::from_tx(burnchain, block_header, burn_tx) { Ok(op) => Some(BlockstackOperationType::LeaderBlockCommit(op)), Err(e) => { warn!( @@ -693,7 +733,7 @@ impl Burnchain { } } x if x == Opcodes::PreStx as u8 => { - match PreStxOp::from_tx(block_header, burn_tx, sunset_end_ht) { + match PreStxOp::from_tx(block_header, burn_tx, burnchain.pox_constants.sunset_end) { Ok(op) => Some(BlockstackOperationType::PreStx(op)), Err(e) => { warn!( @@ -743,7 +783,12 @@ impl Burnchain { }; if let Some(BlockstackOperationType::PreStx(pre_stack_stx)) = pre_stx_tx { let sender = &pre_stack_stx.output; - match StackStxOp::from_tx(block_header, burn_tx, sender, sunset_end_ht) { + match StackStxOp::from_tx( + block_header, + burn_tx, + sender, + burnchain.pox_constants.sunset_end, + ) { Ok(op) => Some(BlockstackOperationType::StackStx(op)), Err(e) => { warn!( @@ -878,9 +923,9 @@ impl Burnchain { /// Top-level entry point to check and process a block. pub fn process_block( + burnchain: &Burnchain, burnchain_db: &mut BurnchainDB, block: &BurnchainBlock, - sunset_end_ht: u64, ) -> Result { debug!( "Process block {} {}", @@ -888,7 +933,7 @@ impl Burnchain { &block.block_hash() ); - let _blockstack_txs = burnchain_db.store_new_burnchain_block(&block, sunset_end_ht)?; + let _blockstack_txs = burnchain_db.store_new_burnchain_block(burnchain, &block)?; let header = block.header(); @@ -910,7 +955,7 @@ impl Burnchain { ); let header = block.header(); - let blockstack_txs = burnchain_db.store_new_burnchain_block(&block, u64::max_value())?; + let blockstack_txs = burnchain_db.store_new_burnchain_block(burnchain, &block)?; let sortition_tip = SortitionDB::get_canonical_sortition_tip(db.conn())?; @@ -1284,6 +1329,8 @@ impl Burnchain { let mut downloader = indexer.downloader(); let mut parser = indexer.parser(); + let myself = self.clone(); + // TODO: don't re-process blocks. See if the block hash is already present in the burn db, // and if so, do nothing. let download_thread: thread::JoinHandle> = @@ -1336,7 +1383,6 @@ impl Burnchain { Ok(()) }); - let sunset_end = self.pox_constants.sunset_end; let db_thread: thread::JoinHandle> = thread::spawn(move || { let mut last_processed = burn_chain_tip; @@ -1349,7 +1395,7 @@ impl Burnchain { let insert_start = get_epoch_time_ms(); last_processed = - Burnchain::process_block(&mut burnchain_db, &burnchain_block, sunset_end)?; + Burnchain::process_block(&myself, &mut burnchain_db, &burnchain_block)?; if !coord_comm.announce_new_burn_block() { return Err(burnchain_error::CoordinatorClosed); } @@ -1442,7 +1488,8 @@ pub mod tests { use util::log; use chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS, BlockstackOperationType, + LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, }; use chainstate::burn::distribution::BurnSamplePoint; @@ -1815,6 +1862,7 @@ pub mod tests { .unwrap(), vtxindex: 444, block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_124_hash_initial.clone(), }; @@ -1855,6 +1903,7 @@ pub mod tests { .unwrap(), vtxindex: 445, block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_124_hash_initial.clone(), }; @@ -1895,6 +1944,7 @@ pub mod tests { .unwrap(), vtxindex: 446, block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_124_hash_initial.clone(), }; @@ -2054,10 +2104,6 @@ pub mod tests { BlockstackOperationType::LeaderBlockCommit(block_commit_3.clone()), ], vec![ - BlockstackOperationType::UserBurnSupport(user_burn_1.clone()), - BlockstackOperationType::UserBurnSupport(user_burn_1_2.clone()), - BlockstackOperationType::UserBurnSupport(user_burn_2.clone()), - BlockstackOperationType::UserBurnSupport(user_burn_2_2.clone()), BlockstackOperationType::LeaderBlockCommit(block_commit_1.clone()), BlockstackOperationType::LeaderBlockCommit(block_commit_2.clone()), BlockstackOperationType::LeaderBlockCommit(block_commit_3.clone()), @@ -2194,7 +2240,7 @@ pub mod tests { let burn_total = block_ops_124.iter().fold(0u64, |mut acc, op| { let bf = match op { BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, - BlockstackOperationType::UserBurnSupport(ref op) => op.burn_fee, + BlockstackOperationType::UserBurnSupport(ref op) => 0, _ => 0, }; acc += bf; @@ -2345,6 +2391,7 @@ pub mod tests { .unwrap(), vtxindex: (i + 2) as u32, block_height: 5, + burn_parent_modulus: (4 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash([0xff; 32]), }); block_commits.push(op); @@ -2375,6 +2422,7 @@ pub mod tests { txid: Txid([0xdd; 32]), vtxindex: 1, block_height: 5, + burn_parent_modulus: (4 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash([0xff; 32]), }); @@ -2604,6 +2652,9 @@ pub mod tests { .unwrap(), vtxindex: (2 * i) as u32, block_height: first_block_height + ((i + 1) as u64), + burn_parent_modulus: ((first_block_height + (i as u64)) + % BURN_BLOCK_MINED_AT_MODULUS) + as u8, burn_header_hash: burn_block_hash.clone(), }; diff --git a/src/burnchains/db.rs b/src/burnchains/db.rs index 4824475a65..075e7a4f96 100644 --- a/src/burnchains/db.rs +++ b/src/burnchains/db.rs @@ -293,9 +293,9 @@ impl BurnchainDB { /// Return the ordered list of blockstack operations by vtxindex fn get_blockstack_transactions( &self, + burnchain: &Burnchain, block: &BurnchainBlock, block_header: &BurnchainBlockHeader, - sunset_end_ht: u64, ) -> Vec { debug!( "Extract Blockstack transactions from block {} {}", @@ -307,13 +307,8 @@ impl BurnchainDB { let mut pre_stx_ops = HashMap::new(); for tx in block.txs().iter() { - let result = Burnchain::classify_transaction( - self, - block_header, - &tx, - sunset_end_ht, - &pre_stx_ops, - ); + let result = + Burnchain::classify_transaction(burnchain, self, block_header, &tx, &pre_stx_ops); if let Some(classified_tx) = result { if let BlockstackOperationType::PreStx(pre_stx_op) = classified_tx { pre_stx_ops.insert(pre_stx_op.txid.clone(), pre_stx_op); @@ -336,13 +331,13 @@ impl BurnchainDB { pub fn store_new_burnchain_block( &mut self, + burnchain: &Burnchain, block: &BurnchainBlock, - sunset_end_ht: u64, ) -> Result, BurnchainError> { let header = block.header(); info!("Storing new burnchain block"; "burn_header_hash" => %header.block_hash.to_string()); - let mut blockstack_ops = self.get_blockstack_transactions(block, &header, sunset_end_ht); + let mut blockstack_ops = self.get_blockstack_transactions(burnchain, block, &header); apply_blockstack_txs_safety_checks(header.block_height, &mut blockstack_ops); let db_tx = self.tx_begin()?; @@ -380,6 +375,7 @@ mod tests { use burnchains::bitcoin::address::*; use burnchains::bitcoin::blocks::*; use burnchains::bitcoin::*; + use burnchains::PoxConstants; use burnchains::BLOCKSTACK_MAGIC_MAINNET; use chainstate::burn::*; use chainstate::stacks::*; @@ -403,6 +399,11 @@ mod tests { BurnchainDB::connect(":memory:", first_height, &first_bhh, first_timestamp, true) .unwrap(); + let mut burnchain = Burnchain::regtest(":memory:"); + burnchain.pox_constants = PoxConstants::test_default(); + burnchain.pox_constants.sunset_start = 999; + burnchain.pox_constants.sunset_end = 1000; + let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); assert_eq!(&first_block_header.block_hash, &first_bhh); assert_eq!(&first_block_header.block_height, &first_height); @@ -422,7 +423,7 @@ mod tests { 485, )); let ops = burnchain_db - .store_new_burnchain_block(&canonical_block, 1000) + .store_new_burnchain_block(&burnchain, &canonical_block) .unwrap(); assert_eq!(ops.len(), 0); @@ -460,7 +461,7 @@ mod tests { )); let ops = burnchain_db - .store_new_burnchain_block(&non_canonical_block, 1000) + .store_new_burnchain_block(&burnchain, &non_canonical_block) .unwrap(); assert_eq!(ops.len(), expected_ops.len()); for op in ops.iter() { @@ -510,6 +511,11 @@ mod tests { BurnchainDB::connect(":memory:", first_height, &first_bhh, first_timestamp, true) .unwrap(); + let mut burnchain = Burnchain::regtest(":memory:"); + burnchain.pox_constants = PoxConstants::test_default(); + burnchain.pox_constants.sunset_start = 999; + burnchain.pox_constants.sunset_end = 1000; + let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); assert_eq!(&first_block_header.block_hash, &first_bhh); assert_eq!(&first_block_header.block_height, &first_height); @@ -529,7 +535,7 @@ mod tests { 485, )); let ops = burnchain_db - .store_new_burnchain_block(&canonical_block, 1000) + .store_new_burnchain_block(&burnchain, &canonical_block) .unwrap(); assert_eq!(ops.len(), 0); @@ -680,7 +686,7 @@ mod tests { )); let processed_ops_0 = burnchain_db - .store_new_burnchain_block(&block_0, 1000) + .store_new_burnchain_block(&burnchain, &block_0) .unwrap(); assert_eq!( @@ -690,7 +696,7 @@ mod tests { ); let processed_ops_1 = burnchain_db - .store_new_burnchain_block(&block_1, 1000) + .store_new_burnchain_block(&burnchain, &block_1) .unwrap(); assert_eq!( diff --git a/src/burnchains/mod.rs b/src/burnchains/mod.rs index 5d0d994001..eb0e0939bd 100644 --- a/src/burnchains/mod.rs +++ b/src/burnchains/mod.rs @@ -342,7 +342,7 @@ impl PoxConstants { sunset_end: u64, ) -> PoxConstants { assert!(anchor_threshold > (prepare_length / 2)); - + assert!(prepare_length < reward_cycle_length); assert!(sunset_start <= sunset_end); PoxConstants { @@ -358,11 +358,12 @@ impl PoxConstants { } #[cfg(test)] pub fn test_default() -> PoxConstants { + // 20 reward slots; 10 prepare-phase slots PoxConstants::new(10, 5, 3, 25, 5, 5000, 10000) } pub fn reward_slots(&self) -> u32 { - self.reward_cycle_length * (OUTPUTS_PER_COMMIT as u32) + (self.reward_cycle_length - self.prepare_length) * (OUTPUTS_PER_COMMIT as u32) } /// is participating_ustx enough to engage in PoX in the next reward cycle? @@ -379,7 +380,7 @@ impl PoxConstants { PoxConstants::new( POX_REWARD_CYCLE_LENGTH, POX_PREPARE_WINDOW_LENGTH, - 192, + 80, 25, 5, POX_SUNSET_START, @@ -389,7 +390,7 @@ impl PoxConstants { pub fn testnet_default() -> PoxConstants { PoxConstants::new( - 120, + 150, // 120 reward slots; 30 prepare-phase slots 30, 20, 3333333333333333, @@ -679,6 +680,8 @@ pub mod test { pub block_commits: Vec, pub id: usize, pub nonce: u64, + pub spent_at_nonce: HashMap, // how much uSTX this miner paid in a given tx's nonce + pub test_with_tx_fees: bool, // set to true to make certain helper methods attach a pre-defined tx fee } pub struct TestMinerFactory { @@ -704,6 +707,8 @@ pub mod test { block_commits: vec![], id: 0, nonce: 0, + spent_at_nonce: HashMap::new(), + test_with_tx_fees: true, } } @@ -1019,7 +1024,7 @@ pub mod test { } }; - txop.block_height = self.block_height; + txop.set_burn_height(self.block_height); txop.vtxindex = self.txs.len() as u32; txop.burn_header_hash = BurnchainHeaderHash::from_test_data( txop.block_height, diff --git a/src/chainstate/burn/db/processing.rs b/src/chainstate/burn/db/processing.rs index 68579a289a..78350d363c 100644 --- a/src/chainstate/burn/db/processing.rs +++ b/src/chainstate/burn/db/processing.rs @@ -23,7 +23,10 @@ use chainstate::burn::db::sortdb::{InitialMiningBonus, PoxId, SortitionHandleTx, use chainstate::coordinator::RewardCycleInfo; -use chainstate::burn::operations::{leader_block_commit::RewardSetInfo, BlockstackOperationType}; +use chainstate::burn::operations::{ + leader_block_commit::{MissedBlockCommit, RewardSetInfo}, + BlockstackOperationType, Error as OpError, +}; use burnchains::{ Burnchain, BurnchainBlockHeader, BurnchainHeaderHash, BurnchainStateTransition, @@ -97,37 +100,6 @@ impl<'a> SortitionHandleTx<'a> { } } - /// Generate the list of blockstack operations that will be snapshotted -- a subset of the - /// blockstack operations extracted from get_blockstack_transactions. - /// Return the list of parsed blockstack operations whose check() method has returned true. - fn check_block_ops( - &mut self, - burnchain: &Burnchain, - mut block_ops: Vec, - reward_info: Option<&RewardSetInfo>, - ) -> Result, BurnchainError> { - debug!( - "Check Blockstack transactions from sortition_id: {}", - &self.context.chain_tip - ); - - // classify and check each transaction - block_ops.retain(|blockstack_op| { - self.check_transaction(burnchain, blockstack_op, reward_info) - .is_ok() - }); - - // block-wide check: no duplicate keys registered - let ret_filtered = Burnchain::filter_block_VRF_dups(block_ops); - assert!(Burnchain::ops_are_sorted(&ret_filtered)); - - // block-wide check: at most one block-commit can consume a VRF key - let ret_filtered = Burnchain::filter_block_commits_with_same_VRF_key(ret_filtered); - assert!(Burnchain::ops_are_sorted(&ret_filtered)); - - Ok(ret_filtered) - } - /// Process all block's checked transactions /// * make the burn distribution /// * insert the ones that went into the burn distribution @@ -139,6 +111,7 @@ impl<'a> SortitionHandleTx<'a> { parent_snapshot: &BlockSnapshot, block_header: &BurnchainBlockHeader, this_block_ops: &Vec, + missed_commits: &Vec, next_pox_info: Option, parent_pox: PoxId, reward_info: Option<&RewardSetInfo>, @@ -148,7 +121,7 @@ impl<'a> SortitionHandleTx<'a> { let this_block_hash = block_header.block_hash.clone(); // make the burn distribution, and in doing so, identify the user burns that we'll keep - let state_transition = BurnchainStateTransition::from_block_ops(self, parent_snapshot, this_block_ops, burnchain.pox_constants.sunset_end) + let state_transition = BurnchainStateTransition::from_block_ops(self, parent_snapshot, this_block_ops, missed_commits, burnchain.pox_constants.sunset_end) .map_err(|e| { error!("TRANSACTION ABORTED when converting {} blockstack operations in block {} ({}) to a burn distribution: {:?}", this_block_ops.len(), this_block_height, &this_block_hash, e); e @@ -251,6 +224,7 @@ impl<'a> SortitionHandleTx<'a> { parent_snapshot, &snapshot, &state_transition.accepted_ops, + missed_commits, next_pox_info, reward_info, initialize_bonus, @@ -309,15 +283,32 @@ impl<'a> SortitionHandleTx<'a> { blockstack_txs.sort_by(|ref a, ref b| a.vtxindex().partial_cmp(&b.vtxindex()).unwrap()); // check each transaction, and filter out only the ones that are valid - let block_ops = self - .check_block_ops(burnchain, blockstack_txs, reward_set_info) - .map_err(|e| { - error!( - "TRANSACTION ABORTED when checking block {} ({}): {:?}", - block_header.block_height, &block_header.block_hash, e - ); - e - })?; + debug!( + "Check Blockstack transactions from sortition_id: {}", + &self.context.chain_tip + ); + + let mut missed_block_commits = vec![]; + + // classify and check each transaction + blockstack_txs.retain(|blockstack_op| { + match self.check_transaction(burnchain, blockstack_op, reward_set_info) { + Ok(_) => true, + Err(BurnchainError::OpError(OpError::MissedBlockCommit(missed_op))) => { + missed_block_commits.push(missed_op); + false + } + Err(_) => false, + } + }); + + // block-wide check: no duplicate keys registered + let ret_filtered = Burnchain::filter_block_VRF_dups(blockstack_txs); + assert!(Burnchain::ops_are_sorted(&ret_filtered)); + + // block-wide check: at most one block-commit can consume a VRF key + let block_ops = Burnchain::filter_block_commits_with_same_VRF_key(ret_filtered); + assert!(Burnchain::ops_are_sorted(&block_ops)); // process them let res = self @@ -326,6 +317,7 @@ impl<'a> SortitionHandleTx<'a> { parent_snapshot, block_header, &block_ops, + &missed_block_commits, next_pox_info, parent_pox, reward_set_info, diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index 8c22ddbee5..1228aa34e4 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -48,7 +48,7 @@ use chainstate::coordinator::{Error as CoordinatorError, PoxAnchorBlockStatus, R use core::CHAINSTATE_VERSION; use chainstate::burn::operations::{ - leader_block_commit::{RewardSetInfo, OUTPUTS_PER_COMMIT}, + leader_block_commit::{MissedBlockCommit, RewardSetInfo, OUTPUTS_PER_COMMIT}, BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, StackStxOp, TransferStxOp, UserBurnSupportOp, }; @@ -118,6 +118,21 @@ impl FromRow for SortitionId { } } +impl FromRow for MissedBlockCommit { + fn from_row<'a>(row: &'a Row) -> Result { + let intended_sortition = SortitionId::from_column(row, "intended_sortition_id")?; + let input_json: String = row.get("input"); + let input = + serde_json::from_str(&input_json).map_err(|e| db_error::SerializationError(e))?; + let txid = Txid::from_column(row, "txid")?; + Ok(MissedBlockCommit { + input, + txid, + intended_sortition, + }) + } +} + impl FromRow for BlockSnapshot { fn from_row<'a>(row: &'a Row) -> Result { let block_height = u64::from_column(row, "block_height")?; @@ -263,6 +278,8 @@ impl FromRow for LeaderBlockCommitOp { .parse::() .expect("DB Corruption: Sunset burn is not parseable as u64"); + let burn_parent_modulus: u8 = row.get("burn_parent_modulus"); + let block_commit = LeaderBlockCommitOp { block_header_hash, new_seed, @@ -271,6 +288,7 @@ impl FromRow for LeaderBlockCommitOp { key_block_ptr, key_vtxindex, memo, + burn_parent_modulus, burn_fee, input, @@ -498,6 +516,7 @@ const BURNDB_SETUP: &'static [&'static str] = &[ sunset_burn TEXT NOT NULL, -- use text to encode really big numbers input TEXT NOT NULL, apparent_sender TEXT NOT NULL, + burn_parent_modulus INTEGER NOT NULL, PRIMARY KEY(txid,sortition_id), FOREIGN KEY(sortition_id) REFERENCES snapshots(sortition_id) @@ -551,6 +570,14 @@ const BURNDB_SETUP: &'static [&'static str] = &[ PRIMARY KEY(txid) );"#, r#" + CREATE TABLE missed_commits ( + txid TEXT NOT NULL, + input TEXT NOT NULL, + intended_sortition_id TEXT NOT NULL, + + PRIMARY KEY(txid, intended_sortition_id) + );"#, + r#" CREATE TABLE canonical_accepted_stacks_blocks( tip_consensus_hash TEXT NOT NULL, consensus_hash TEXT NOT NULL, @@ -1093,9 +1120,12 @@ impl<'a> SortitionHandleTx<'a> { /// Returns None if: /// * The reward cycle had an anchor block, but it isn't known by this node. /// * The reward cycle did not have anchor block + /// * The block is in the prepare phase of a reward cycle, in which case miners must burn /// * The Stacking recipient set is empty (either because this reward cycle has already exhausted the set of addresses or because no one ever Stacked). fn pick_recipients( &mut self, + burnchain: &Burnchain, + block_height: u64, reward_set_vrf_seed: &SortitionHash, next_pox_info: Option<&RewardCycleInfo>, ) -> Result, BurnchainError> { @@ -1103,6 +1133,19 @@ impl<'a> SortitionHandleTx<'a> { if let PoxAnchorBlockStatus::SelectedAndKnown(ref anchor_block, ref reward_set) = next_pox_info.anchor_status { + if burnchain.is_in_prepare_phase(block_height) { + debug!( + "No recipients for block {}, since in prepare phase", + block_height + ); + return Ok(None); + } + + test_debug!( + "Pick recipients for anchor block {} -- {} reward recipient(s)", + anchor_block, + reward_set.len() + ); if reward_set.len() == 0 { return Ok(None); } @@ -1129,6 +1172,7 @@ impl<'a> SortitionHandleTx<'a> { .collect(), })) } else { + test_debug!("No anchor block known for this reward cycle"); Ok(None) } } else { @@ -1138,6 +1182,10 @@ impl<'a> SortitionHandleTx<'a> { // get the reward set size let reward_set_size = self.get_reward_set_size()?; if reward_set_size == 0 { + test_debug!( + "No more reward recipients descending from anchor block {}", + anchor_block + ); Ok(None) } else { let chosen_recipients = reward_set_vrf_seed.choose_two(reward_set_size as u32); @@ -1147,6 +1195,7 @@ impl<'a> SortitionHandleTx<'a> { let recipient = self.get_reward_set_entry(ix)?; recipients.push((recipient, ix)); } + test_debug!("PoX reward recipients: {:?}", &recipients); Ok(Some(RewardSetInfo { anchor_block, recipients, @@ -1154,6 +1203,7 @@ impl<'a> SortitionHandleTx<'a> { } } else { // no anchor block selected + test_debug!("No anchor block selected for this reward cycle"); Ok(None) } } @@ -2510,7 +2560,12 @@ impl SortitionDB { let reward_set_info = if burn_header.block_height >= burnchain.pox_constants.sunset_end { None } else { - sortition_db_handle.pick_recipients(&reward_set_vrf_hash, next_pox_info.as_ref())? + sortition_db_handle.pick_recipients( + burnchain, + burn_header.block_height, + &reward_set_vrf_hash, + next_pox_info.as_ref(), + )? }; // Get any initial mining bonus which would be due to the winner of this block. @@ -2547,18 +2602,18 @@ impl SortitionDB { #[cfg(test)] pub fn test_get_next_block_recipients( &mut self, + burnchain: &Burnchain, next_pox_info: Option<&RewardCycleInfo>, - sunset_ht: u64, ) -> Result, BurnchainError> { let parent_snapshot = SortitionDB::get_canonical_burn_chain_tip(self.conn())?; - self.get_next_block_recipients(&parent_snapshot, next_pox_info, sunset_ht) + self.get_next_block_recipients(burnchain, &parent_snapshot, next_pox_info) } pub fn get_next_block_recipients( &mut self, + burnchain: &Burnchain, parent_snapshot: &BlockSnapshot, next_pox_info: Option<&RewardCycleInfo>, - sunset_end_ht: u64, ) -> Result, BurnchainError> { let reward_set_vrf_hash = parent_snapshot .sortition_hash @@ -2566,10 +2621,15 @@ impl SortitionDB { let mut sortition_db_handle = SortitionHandleTx::begin(self, &parent_snapshot.sortition_id)?; - if parent_snapshot.block_height + 1 >= sunset_end_ht { + if parent_snapshot.block_height + 1 >= burnchain.pox_constants.sunset_end { Ok(None) } else { - sortition_db_handle.pick_recipients(&reward_set_vrf_hash, next_pox_info) + sortition_db_handle.pick_recipients( + burnchain, + parent_snapshot.block_height + 1, + &reward_set_vrf_hash, + next_pox_info, + ) } } @@ -2872,6 +2932,18 @@ impl SortitionDB { query_rows(conn, qry, args) } + /// Get all the missed block commits that were intended to be included in the given + /// block but were not + pub fn get_missed_commits_by_intended( + conn: &Connection, + sortition: &SortitionId, + ) -> Result, db_error> { + let qry = "SELECT * FROM missed_commits WHERE intended_sortition_id = ?1"; + let args: &[&dyn ToSql] = &[sortition]; + + query_rows(conn, qry, args) + } + /// Get all leader keys registered in a block on the burn chain's history in this fork. /// Returns the list of leader keys in order by vtxindex. pub fn get_leader_keys_by_block( @@ -3132,6 +3204,7 @@ impl<'a> SortitionHandleTx<'a> { parent_snapshot: &BlockSnapshot, snapshot: &BlockSnapshot, block_ops: &Vec, + missed_commits: &Vec, next_pox_info: Option, reward_info: Option<&RewardSetInfo>, initialize_bonus: Option, @@ -3171,6 +3244,10 @@ impl<'a> SortitionHandleTx<'a> { self.store_burnchain_transaction(block_op, &sn.sortition_id)?; } + for missed_commit in missed_commits { + self.insert_missed_block_commit(missed_commit)?; + } + Ok(root_hash) } @@ -3192,6 +3269,30 @@ impl<'a> SortitionHandleTx<'a> { .map(|s| s.parse().expect("BUG: bad mining bonus stored in DB"))) } + #[cfg(test)] + fn store_burn_distribution( + &mut self, + new_sortition: &SortitionId, + transition: &BurnchainStateTransition, + ) { + let create = "CREATE TABLE IF NOT EXISTS snapshot_burn_distributions (sortition_id TEXT PRIMARY KEY, data TEXT NOT NULL);"; + self.execute(create, NO_PARAMS).unwrap(); + let sql = "INSERT INTO snapshot_burn_distributions (sortition_id, data) VALUES (?, ?)"; + let args: &[&dyn ToSql] = &[ + new_sortition, + &serde_json::to_string(&transition.burn_dist).unwrap(), + ]; + self.execute(sql, args).unwrap(); + } + + #[cfg(not(test))] + fn store_burn_distribution( + &mut self, + _new_sortition: &SortitionId, + _transition: &BurnchainStateTransition, + ) { + } + fn store_transition_ops( &mut self, new_sortition: &SortitionId, @@ -3204,6 +3305,7 @@ impl<'a> SortitionHandleTx<'a> { &serde_json::to_string(&transition.consumed_leader_keys).unwrap(), ]; self.execute(sql, args)?; + self.store_burn_distribution(new_sortition, transition); Ok(()) } @@ -3348,7 +3450,7 @@ impl<'a> SortitionHandleTx<'a> { let tx_input_str = serde_json::to_string(&block_commit.input) .map_err(|e| db_error::SerializationError(e))?; - // serialize tx input to JSON + // serialize apparent sender to JSON let apparent_sender_str = serde_json::to_string(&block_commit.apparent_sender) .map_err(|e| db_error::SerializationError(e))?; @@ -3370,10 +3472,11 @@ impl<'a> SortitionHandleTx<'a> { &serde_json::to_value(&block_commit.commit_outs).unwrap(), &block_commit.sunset_burn.to_string(), &apparent_sender_str, + &block_commit.burn_parent_modulus, ]; - self.execute("INSERT INTO block_commits (txid, vtxindex, block_height, burn_header_hash, block_header_hash, new_seed, parent_block_ptr, parent_vtxindex, key_block_ptr, key_vtxindex, memo, burn_fee, input, sortition_id, commit_outs, sunset_burn, apparent_sender) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)", args)?; + self.execute("INSERT INTO block_commits (txid, vtxindex, block_height, burn_header_hash, block_header_hash, new_seed, parent_block_ptr, parent_vtxindex, key_block_ptr, key_vtxindex, memo, burn_fee, input, sortition_id, commit_outs, sunset_burn, apparent_sender, burn_parent_modulus) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", args)?; Ok(()) } @@ -3414,6 +3517,28 @@ impl<'a> SortitionHandleTx<'a> { Ok(()) } + /// Insert a missed block commit + fn insert_missed_block_commit(&mut self, op: &MissedBlockCommit) -> Result<(), db_error> { + // serialize tx input to JSON + let tx_input_str = + serde_json::to_string(&op.input).map_err(|e| db_error::SerializationError(e))?; + + let args: &[&dyn ToSql] = &[&op.txid, &op.intended_sortition, &tx_input_str]; + + self.execute( + "INSERT OR REPLACE INTO missed_commits (txid, intended_sortition_id, input) \ + VALUES (?1, ?2, ?3)", + args, + )?; + info!( + "ACCEPTED missed block commit"; + "txid" => %op.txid, + "intended_sortition" => %op.intended_sortition, + ); + + Ok(()) + } + /// Insert a snapshots row from a block's-worth of operations. /// Do not call directly -- use append_chain_tip_snapshot to preserve the fork table structure. fn insert_block_snapshot(&self, snapshot: &BlockSnapshot) -> Result<(), db_error> { @@ -3798,7 +3923,8 @@ mod tests { use util::get_epoch_time_secs; use chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS, BlockstackOperationType, + LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, }; use burnchains::bitcoin::address::BitcoinAddress; @@ -3854,7 +3980,7 @@ mod tests { sn.consensus_hash = ConsensusHash(Hash160::from_data(&sn.consensus_hash.0).0); let index_root = tx - .append_chain_tip_snapshot(&sn_parent, &sn, block_ops, None, None, None) + .append_chain_tip_snapshot(&sn_parent, &sn, block_ops, &vec![], None, None, None) .unwrap(); sn.index_root = index_root; @@ -4024,6 +4150,7 @@ mod tests { .unwrap(), vtxindex: vtxindex, block_height: block_height + 2, + burn_parent_modulus: ((block_height + 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash([0x03; 32]), }; @@ -4141,7 +4268,7 @@ mod tests { sn.consensus_hash = ConsensusHash([0x23; 20]); let index_root = tx - .append_chain_tip_snapshot(&sn_parent, &sn, &vec![], None, None, None) + .append_chain_tip_snapshot(&sn_parent, &sn, &vec![], &vec![], None, None, None) .unwrap(); sn.index_root = index_root; @@ -4476,6 +4603,7 @@ mod tests { &last_snapshot, &snapshot_row, &vec![], + &vec![], None, None, None, @@ -4723,6 +4851,7 @@ mod tests { &last_snapshot, &snapshot_row, &vec![], + &vec![], None, None, None, @@ -4827,6 +4956,7 @@ mod tests { .unwrap(), vtxindex: vtxindex, block_height: block_height + 2, + burn_parent_modulus: ((block_height + 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash([0x03; 32]), }; @@ -5052,6 +5182,7 @@ mod tests { &chain_tip, &snapshot_without_sortition, &vec![], + &vec![], None, None, None, @@ -5080,6 +5211,7 @@ mod tests { &chain_tip, &snapshot_with_sortition, &vec![], + &vec![], None, None, None, @@ -5270,8 +5402,16 @@ mod tests { ]); let mut tx = SortitionHandleTx::begin(&mut db, &last_snapshot.sortition_id).unwrap(); - tx.append_chain_tip_snapshot(&last_snapshot, &next_snapshot, &vec![], None, None, None) - .unwrap(); + tx.append_chain_tip_snapshot( + &last_snapshot, + &next_snapshot, + &vec![], + &vec![], + None, + None, + None, + ) + .unwrap(); tx.commit().unwrap(); last_snapshot = next_snapshot.clone(); @@ -5410,6 +5550,7 @@ mod tests { &last_snapshot, &next_snapshot, &vec![], + &vec![], None, None, None, @@ -5495,6 +5636,7 @@ mod tests { &last_snapshot, &next_snapshot, &vec![], + &vec![], None, None, None, @@ -5534,6 +5676,7 @@ mod tests { &last_snapshot, &next_snapshot, &vec![], + &vec![], None, None, None, @@ -5732,6 +5875,7 @@ mod tests { &last_snapshot, &snapshot_row, &vec![], + &vec![], None, None, None, @@ -5973,7 +6117,15 @@ mod tests { { let mut tx = SortitionHandleTx::begin(db, &last_snapshot.sortition_id).unwrap(); let _index_root = tx - .append_chain_tip_snapshot(&last_snapshot, &snapshot, &vec![], None, None, None) + .append_chain_tip_snapshot( + &last_snapshot, + &snapshot, + &vec![], + &vec![], + None, + None, + None, + ) .unwrap(); tx.commit().unwrap(); } diff --git a/src/chainstate/burn/distribution.rs b/src/chainstate/burn/distribution.rs index 4488c10252..3c53d423fd 100644 --- a/src/chainstate/burn/distribution.rs +++ b/src/chainstate/burn/distribution.rs @@ -14,11 +14,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::cmp; use std::collections::{BTreeMap, HashMap}; use std::convert::TryInto; use chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, + leader_block_commit::MissedBlockCommit, BlockstackOperationType, LeaderBlockCommitOp, + LeaderKeyRegisterOp, UserBurnSupportOp, }; use burnchains::Address; @@ -41,7 +43,7 @@ use util::vrf::VRFPublicKey; use core::MINING_COMMITMENT_WINDOW; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BurnSamplePoint { pub burns: u128, pub range_start: Uint256, @@ -50,11 +52,16 @@ pub struct BurnSamplePoint { pub user_burns: Vec, } +#[derive(Clone)] +enum LinkedCommitIdentifier { + Missed(MissedBlockCommit), + Valid(LeaderBlockCommitOp), +} + #[derive(Clone)] struct LinkedCommitmentScore { rel_block_height: u8, - op: LeaderBlockCommitOp, - user_burns: u64, + op: LinkedCommitIdentifier, } #[derive(PartialEq, Eq, Hash)] @@ -65,15 +72,38 @@ struct UserBurnIdentifier { block_hash: Hash160, } +impl LinkedCommitIdentifier { + fn spent_txid(&self) -> &Txid { + match self { + LinkedCommitIdentifier::Missed(ref op) => &op.input.0, + LinkedCommitIdentifier::Valid(ref op) => &op.input.0, + } + } + + fn spent_output(&self) -> u32 { + match self { + LinkedCommitIdentifier::Missed(ref op) => op.input.1, + LinkedCommitIdentifier::Valid(ref op) => op.input.1, + } + } + + fn burn_fee(&self) -> u64 { + match self { + LinkedCommitIdentifier::Missed(_) => 1, + LinkedCommitIdentifier::Valid(ref op) => op.burn_fee, + } + } +} + impl BurnSamplePoint { fn sanity_check_window( block_commits: &Vec>, - user_burns: &Vec>, + missed_commits: &Vec>, ) { - assert_eq!(block_commits.len(), user_burns.len()); assert!(block_commits.len() <= (MINING_COMMITMENT_WINDOW as usize)); + assert_eq!(missed_commits.len() + 1, block_commits.len()); let mut block_height_at_index = None; - for (index, (commits, burns)) in block_commits.iter().zip(user_burns.iter()).enumerate() { + for (index, commits) in block_commits.iter().enumerate() { let index = index as u64; for commit in commits.iter() { if let Some((first_block_height, first_index)) = block_height_at_index { @@ -86,17 +116,6 @@ impl BurnSamplePoint { block_height_at_index = Some((commit.block_height, index)); } } - for burn in burns.iter() { - if let Some((first_block_height, first_index)) = block_height_at_index { - assert_eq!( - burn.block_height, - first_block_height + (index - first_index), - "Commits and Burns should be in block height order" - ); - } else { - block_height_at_index = Some((burn.block_height, index)); - } - } } } @@ -109,27 +128,32 @@ impl BurnSamplePoint { /// If a burn refers to more than one commitment, its burn amount is *split* between those /// commitments /// + /// Burns are evaluated over MINING_COMMITMENT_WINDOW, where the effective burn for + /// a commitment is := min(last_burn_amount, median over MINING_COMMITMENT_WINDOW) + /// /// Returns the distribution, which consumes the given lists of operations. /// /// * `block_commits`: this is a mapping from relative block_height to the block /// commits that occurred at that height. These relative block heights start /// at 0 and increment towards the present. When the mining window is 6, the /// "current" sortition's block commits would be in index 5. - /// * `user_burns`: this is a mapping from relative block_height to the user - /// burns that occurred at that height. When the mining window is 6, the - /// "current" sortition's user burns would be in index 5. + /// * `missed_commits`: this is a mapping from relative block_height to the + /// block commits that were intended to be included at that height. These + /// relative block heights start at 0 and increment towards the present. There + /// will be no such commits for the current sortition, so this vec will have + /// `missed_commits.len() = block_commits.len() - 1` /// * `sunset_finished_at`: if set, this indicates that the PoX sunset finished before or /// during the mining window. This value is the first index in the block_commits /// for which PoX is fully disabled (i.e., the block commit has a single burn output). pub fn make_min_median_distribution( mut block_commits: Vec>, - user_burns: Vec>, + mut missed_commits: Vec>, sunset_finished_at: Option, ) -> Vec { // sanity check assert!(MINING_COMMITMENT_WINDOW > 0); let window_size = block_commits.len() as u8; - BurnSamplePoint::sanity_check_window(&block_commits, &user_burns); + BurnSamplePoint::sanity_check_window(&block_commits, &missed_commits); // first, let's link all of the current block commits to the priors let mut commits_with_priors: Vec<_> = @@ -141,145 +165,62 @@ impl BurnSamplePoint { let mut linked_commits = vec![None; window_size as usize]; linked_commits[0] = Some(LinkedCommitmentScore { rel_block_height: window_size - 1, - op, - user_burns: 0 + op: LinkedCommitIdentifier::Valid(op), }); linked_commits }) .collect(); - let mut user_burn_targets: HashMap> = HashMap::new(); - - for (ix, linked_commit) in commits_with_priors.iter().enumerate() { - let cur_commit = &linked_commit[0].as_ref().unwrap().op; - let user_burn_target_key = UserBurnIdentifier { - rel_block_height: window_size - 1, - key_vtxindex: cur_commit.key_vtxindex, - key_block_ptr: cur_commit.key_block_ptr, - block_hash: Hash160::from_sha256(&cur_commit.block_header_hash.0), - }; - if let Some(user_burn_recipients) = user_burn_targets.get_mut(&user_burn_target_key) { - user_burn_recipients.push(ix); - } else { - user_burn_targets.insert(user_burn_target_key, vec![ix]); - } - } - for rel_block_height in (0..(window_size - 1)).rev() { let cur_commits = block_commits.remove(rel_block_height as usize); + let cur_missed = missed_commits.remove(rel_block_height as usize); + // build a map from txid -> block commit for all the block commits + // in the current block let mut cur_commits_map: HashMap<_, _> = cur_commits .into_iter() .map(|commit| (commit.txid.clone(), commit)) .collect(); + // build a map from txid -> missed block commit for the current block + let mut cur_missed_map: HashMap<_, _> = cur_missed + .into_iter() + .map(|missed| (missed.txid.clone(), missed)) + .collect(); + let sunset_finished = if let Some(sunset_finished_at) = sunset_finished_at { sunset_finished_at <= rel_block_height } else { false }; let expected_index = LeaderBlockCommitOp::expected_chained_utxo(sunset_finished); - for (commitment_ix, linked_commit) in commits_with_priors.iter_mut().enumerate() { + for linked_commit in commits_with_priors.iter_mut() { let end = linked_commit.iter().rev().find_map(|o| o.as_ref()).unwrap(); // guaranteed to be at least 1 non-none entry // check that the commit is using the right output index - if end.op.input.1 != expected_index { + if end.op.spent_output() != expected_index { continue; } - if let Some(referenced_commit) = cur_commits_map.remove(&end.op.input.0) { - let user_burn_target_key = UserBurnIdentifier { - rel_block_height, - key_vtxindex: referenced_commit.key_vtxindex, - key_block_ptr: referenced_commit.key_block_ptr, - block_hash: Hash160::from_sha256(&referenced_commit.block_header_hash.0), - }; - - if let Some(user_burn_recipients) = - user_burn_targets.get_mut(&user_burn_target_key) - { - user_burn_recipients.push(commitment_ix); + let referenced_op = + if let Some(referenced_commit) = cur_commits_map.remove(&end.op.spent_txid()) { + // found a chained utxo + Some(LinkedCommitIdentifier::Valid(referenced_commit)) + } else if let Some(missed_op) = cur_missed_map.remove(&end.op.spent_txid()) { + // found a missed commit + Some(LinkedCommitIdentifier::Missed(missed_op)) } else { - user_burn_targets.insert(user_burn_target_key, vec![commitment_ix]); - } - - // found a chained utxo, connect + None + }; + // if we found a referenced op, connect it + if let Some(referenced_op) = referenced_op { linked_commit[(window_size - 1 - rel_block_height) as usize] = Some(LinkedCommitmentScore { - op: referenced_commit, + op: referenced_op, rel_block_height, - user_burns: 0, }); } } } - // next, we need to associate user burns with the leader block commits. - // this is where things start to go a little wild: - // - // User burns identify a block commit using VRF public key, so we'll use the - // user_burn_targets map to figure out which linked commitment should receive - // the user burn - let mut commit_txid_to_user_burns: HashMap<_, Vec> = HashMap::new(); - - // iterate across user burns in block_height order - for (rel_block_height, user_burns_at_height) in user_burns.into_iter().enumerate() { - for mut user_burn in user_burns_at_height.into_iter() { - let UserBurnSupportOp { - key_vtxindex, - key_block_ptr, - block_header_hash_160, - burn_fee, - .. - } = user_burn.clone(); - - let user_burn_target_key = UserBurnIdentifier { - rel_block_height: rel_block_height as u8, - key_vtxindex: key_vtxindex, - key_block_ptr: key_block_ptr, - block_hash: block_header_hash_160, - }; - - if let Some(user_burn_recipients) = user_burn_targets.get(&user_burn_target_key) { - let per_recipient = burn_fee / (user_burn_recipients.len() as u64); - // set the burn fee to the per recipient amount for when we include this - // user burn op in the burn samples - user_burn.burn_fee = per_recipient; - - for recipient in user_burn_recipients.iter() { - let recipient_commit = commits_with_priors[*recipient] - .get_mut(window_size as usize - 1 - rel_block_height) - .expect("BUG: (window_size - i) should be in window range") - .as_mut() - .expect("BUG: Should have a non-none commit entry"); - // cheap sanity checks - assert_eq!( - recipient_commit.op.key_block_ptr, - user_burn_target_key.key_block_ptr - ); - assert_eq!( - recipient_commit.op.key_vtxindex, - user_burn_target_key.key_vtxindex - ); - // are we at the last block in the window? - // if so, track the user burn op - if rel_block_height as u8 == window_size - 1 { - if let Some(user_burns) = - commit_txid_to_user_burns.get_mut(&recipient_commit.op.txid) - { - user_burns.push(user_burn.clone()); - } else { - commit_txid_to_user_burns.insert( - recipient_commit.op.txid.clone(), - vec![user_burn.clone()], - ); - } - } - - recipient_commit.user_burns += per_recipient; - } - } - } - } - - // now, commits_with_priors has the burn amounts and user burn supports for each + // now, commits_with_priors has the burn amounts for each // linked commitment, we can now generate the burn sample points. let mut burn_sample = commits_with_priors .into_iter() @@ -288,7 +229,7 @@ impl BurnSamplePoint { .iter() .map(|commit| { if let Some(commit) = commit { - (commit.op.burn_fee as u128) + (commit.user_burns as u128) + commit.op.burn_fee() as u128 } else { // use 1 as the linked commit min. this gives a miner a _small_ // chance of winning a block even if they haven't performed chained utxos yet @@ -296,8 +237,9 @@ impl BurnSamplePoint { } }) .collect(); + let most_recent_burn = all_burns[0]; + all_burns.sort(); - let min_burn = all_burns[0]; let median_burn = if window_size % 2 == 0 { (all_burns[(window_size / 2) as usize] + all_burns[(window_size / 2 - 1) as usize]) @@ -306,24 +248,28 @@ impl BurnSamplePoint { all_burns[(window_size / 2) as usize] }; - let burns = (min_burn + median_burn) / 2; - let candidate = linked_commits.remove(0).unwrap().op; + let burns = cmp::min(median_burn, most_recent_burn); + let candidate = if let LinkedCommitIdentifier::Valid(op) = + linked_commits.remove(0).unwrap().op + { + op + } else { + unreachable!("BUG: first linked commit should always be valid"); + }; + assert_eq!(candidate.burn_fee as u128, most_recent_burn); + debug!("Burn sample"; "txid" => %candidate.txid.to_string(), - "min_burn" => %min_burn, + "most_recent_burn" => %most_recent_burn, "median_burn" => %median_burn, "all_burns" => %format!("{:?}", all_burns)); - let user_burns = commit_txid_to_user_burns - .get(&candidate.txid) - .cloned() - .unwrap_or_default(); BurnSamplePoint { burns, range_start: Uint256::zero(), // To be filled in range_end: Uint256::zero(), // To be filled in candidate, - user_burns, + user_burns: vec![], } }) .collect(); @@ -339,7 +285,7 @@ impl BurnSamplePoint { _consumed_leader_keys: Vec, user_burns: Vec, ) -> Vec { - Self::make_min_median_distribution(vec![all_block_candidates], vec![user_burns], None) + Self::make_min_median_distribution(vec![all_block_candidates], vec![], None) } /// Calculate the ranges between 0 and 2**256 - 1 over which each point in the burn sample @@ -410,6 +356,7 @@ impl BurnSamplePoint { mod tests { use super::BurnSamplePoint; + use core::MINING_COMMITMENT_WINDOW; use std::marker::PhantomData; use burnchains::Address; @@ -417,7 +364,9 @@ mod tests { use burnchains::BurnchainSigner; use burnchains::PublicKey; + use chainstate::burn::db::sortdb::SortitionId; use chainstate::burn::operations::{ + leader_block_commit::{MissedBlockCommit, BURN_BLOCK_MINED_AT_MODULUS}, BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, UserBurnSupportOp, }; @@ -479,6 +428,20 @@ mod tests { } } + fn make_missed_commit(txid_id: u64, input_tx: u64) -> MissedBlockCommit { + let mut txid = [0; 32]; + txid[0..8].copy_from_slice(&txid_id.to_be_bytes()); + let mut input_txid = [0; 32]; + input_txid[0..8].copy_from_slice(&input_tx.to_be_bytes()); + let txid = Txid(txid); + let input_txid = Txid(input_txid); + MissedBlockCommit { + txid, + input: (input_txid, 3), + intended_sortition: SortitionId([0; 32]), + } + } + fn make_block_commit( burn_fee: u64, vrf_ident: u32, @@ -517,6 +480,11 @@ mod tests { txid, vtxindex: 0, block_height: block_ht, + burn_parent_modulus: if block_ht > 0 { + ((block_ht - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8 + } else { + BURN_BLOCK_MINED_AT_MODULUS as u8 - 1 + }, burn_header_hash: BurnchainHeaderHash([0; 32]), } } @@ -531,8 +499,8 @@ mod tests { // 0 1 0 0 0 0 // .. - // miner 1 => min = 1, median = 1. - // miner 2 => min = 1, median = 1. + // miner 1 => min = 1, median = 1, last_burn = 4 + // miner 2 => min = 1, median = 1, last_burn = 3 let mut commits = vec![ vec![ @@ -571,7 +539,7 @@ mod tests { let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), - user_burns.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], Some(3), ); @@ -587,9 +555,7 @@ mod tests { assert_eq!(result[1].candidate.txid, commits[5][1].txid); assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 1); - - assert_eq!(result[1].user_burns[0].txid, user_burns[5][0].txid); + assert_eq!(result[1].user_burns.len(), 0); // now correct the back pointers so that they point // at the correct UTXO position *post-sunset* @@ -601,9 +567,14 @@ mod tests { } } + // miner 1: 3 4 5 4 5 4 + // miner 2: 1 3 3 3 3 3 + // miner 1 => min = 3, median = 4, last_burn = 4 + // miner 2 => min = 1, median = 3, last_burn = 3 + let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), - user_burns.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], Some(3), ); @@ -619,9 +590,7 @@ mod tests { assert_eq!(result[1].candidate.txid, commits[5][1].txid); assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 1); - - assert_eq!(result[1].user_burns[0].txid, user_burns[5][0].txid); + assert_eq!(result[1].user_burns.len(), 0); } #[test] @@ -634,8 +603,10 @@ mod tests { // 0 1 0 0 0 0 // .. - // miner 1 => min = 4, median = 4. - // miner 2 => min = 2, median = 4. + // user burns are ignored: + // + // miner 1 => min = 3, median = 4, last_burn = 4 + // miner 2 => min = 1, median = 3, last_burn = 3 let commits = vec![ vec![ @@ -674,7 +645,7 @@ mod tests { let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), - user_burns.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], None, ); @@ -690,9 +661,7 @@ mod tests { assert_eq!(result[1].candidate.txid, commits[5][1].txid); assert_eq!(result[0].user_burns.len(), 0); - assert_eq!(result[1].user_burns.len(), 1); - - assert_eq!(result[1].user_burns[0].txid, user_burns[5][0].txid); + assert_eq!(result[1].user_burns.len(), 0); // test case 2: // miner 1: 4 4 5 4 5 3 @@ -700,8 +669,8 @@ mod tests { // ub : 0 0 0 0 0 2 // *split* - // miner 1 => min = 4, median = 4. - // miner 2 => min = 2, median = 4. + // miner 1 => min = 3, median = 4, last_burn = 3 + // miner 2 => min = 1, median = 4, last_burn = 1 let commits = vec![ vec![ @@ -740,7 +709,7 @@ mod tests { let mut result = BurnSamplePoint::make_min_median_distribution( commits.clone(), - user_burns.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], None, ); @@ -748,18 +717,71 @@ mod tests { result.sort_by_key(|sample| sample.candidate.txid); - assert_eq!(result[0].burns, 4); - assert_eq!(result[1].burns, 3); + assert_eq!(result[0].burns, 3); + assert_eq!(result[1].burns, 1); // make sure that we're associating with the last commit in the window. assert_eq!(result[0].candidate.txid, commits[5][0].txid); assert_eq!(result[1].candidate.txid, commits[5][1].txid); - assert_eq!(result[0].user_burns.len(), 1); - assert_eq!(result[1].user_burns.len(), 1); + assert_eq!(result[0].user_burns.len(), 0); + assert_eq!(result[1].user_burns.len(), 0); + } + + #[test] + fn missed_block_commits() { + // test case 1: + // miner 1: 3 4 5 4 missed 4 + // miner 2: 3 3 missed 3 3 3 + // + // miner 1 => min = 0, median = 4, last_burn = 4 + // miner 2 => min = 0, median = 3, last_burn = 3 + + let commits = vec![ + vec![ + make_block_commit(3, 1, 1, 1, None, 1), + make_block_commit(1, 2, 2, 2, None, 1), + ], + vec![ + make_block_commit(4, 3, 3, 3, Some(1), 2), + make_block_commit(3, 4, 4, 4, Some(2), 2), + ], + vec![make_block_commit(5, 5, 5, 5, Some(3), 3)], + vec![ + make_block_commit(4, 7, 7, 7, Some(5), 4), + make_block_commit(3, 8, 8, 8, Some(6), 4), + ], + vec![make_block_commit(3, 10, 10, 10, Some(8), 5)], + vec![ + make_block_commit(4, 11, 11, 11, Some(9), 6), + make_block_commit(3, 12, 12, 12, Some(10), 6), + ], + ]; - assert_eq!(result[1].user_burns[0].txid, user_burns[5][0].txid); - assert_eq!(result[0].user_burns[0].txid, user_burns[5][0].txid); + let missed_commits = vec![ + vec![], + vec![], + vec![make_missed_commit(6, 4)], + vec![], + vec![make_missed_commit(9, 7)], + ]; + + let mut result = BurnSamplePoint::make_min_median_distribution( + commits.clone(), + missed_commits.clone(), + None, + ); + + assert_eq!(result.len(), 2, "Should be two miners"); + + result.sort_by_key(|sample| sample.candidate.txid); + + assert_eq!(result[0].burns, 4); + assert_eq!(result[1].burns, 3); + + // make sure that we're associating with the last commit in the window. + assert_eq!(result[0].candidate.txid, commits[5][0].txid); + assert_eq!(result[1].candidate.txid, commits[5][1].txid); } #[test] @@ -1095,6 +1117,7 @@ mod tests { .unwrap(), vtxindex: 443, block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash::from_hex( "0000000000000000000000000000000000000000000000000000000000000004", ) @@ -1139,6 +1162,7 @@ mod tests { .unwrap(), vtxindex: 444, block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash::from_hex( "0000000000000000000000000000000000000000000000000000000000000004", ) @@ -1183,6 +1207,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: BurnchainHeaderHash::from_hex( "0000000000000000000000000000000000000000000000000000000000000004", ) @@ -1328,24 +1353,24 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: (block_commit_1.burn_fee + user_burn_1.burn_fee).into(), + burns: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256([ - 0x441d393138e5a796, - 0xbada4a3d4046d839, - 0xa24749933957018c, - 0xa4e5f328cf38744d, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![user_burn_1.clone()], + user_burns: vec![], }, BurnSamplePoint { burns: block_commit_2.burn_fee.into(), range_start: Uint256([ - 0x441d393138e5a796, - 0xbada4a3d4046d839, - 0xa24749933957018c, - 0xa4e5f328cf38744d, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0x7fffffffffffffff, ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), @@ -1364,28 +1389,28 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: (block_commit_1.burn_fee + user_burn_1.burn_fee).into(), + burns: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256([ - 0x65db6527a5c06ed7, - 0xfbf9725ae754dd80, - 0xeafb8d991cf9964d, - 0x6898693a2f1713b4, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![user_burn_1.clone()], + user_burns: vec![], }, BurnSamplePoint { - burns: (block_commit_2.burn_fee + user_burn_2.burn_fee).into(), + burns: block_commit_2.burn_fee.into(), range_start: Uint256([ - 0x65db6527a5c06ed7, - 0xfbf9725ae754dd80, - 0xeafb8d991cf9964d, - 0x6898693a2f1713b4, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0x7fffffffffffffff, ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![user_burn_2.clone()], + user_burns: vec![], }, ], }, @@ -1402,34 +1427,28 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: (block_commit_1.burn_fee - + user_burn_1.burn_fee - + user_burn_1_2.burn_fee) - .into(), + burns: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256([ - 0xbc9e168afe8ad47e, - 0xbbb6d3eb8d1be6c9, - 0x45a410039d0a7dc5, - 0x6b7815d84b0f9fc0, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0x7fffffffffffffff, ]), candidate: block_commit_1.clone(), - user_burns: vec![user_burn_1.clone(), user_burn_1_2.clone()], + user_burns: vec![], }, BurnSamplePoint { - burns: (block_commit_2.burn_fee - + user_burn_2.burn_fee - + user_burn_2_2.burn_fee) - .into(), + burns: block_commit_2.burn_fee.into(), range_start: Uint256([ - 0xbc9e168afe8ad47e, - 0xbbb6d3eb8d1be6c9, - 0x45a410039d0a7dc5, - 0x6b7815d84b0f9fc0, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0xffffffffffffffff, + 0x7fffffffffffffff, ]), range_end: Uint256::max(), candidate: block_commit_2.clone(), - user_burns: vec![user_burn_2.clone(), user_burn_2_2.clone()], + user_burns: vec![], }, ], }, @@ -1454,47 +1473,41 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: (block_commit_1.burn_fee - + user_burn_1.burn_fee - + user_burn_1_2.burn_fee) - .into(), + burns: block_commit_1.burn_fee.into(), range_start: Uint256::zero(), range_end: Uint256([ - 0xcb48ed15c5086a5c, - 0x6b29682cfbe4089c, - 0x4a30e732285c18c9, - 0x5a7416b691bddbad, + 0x3ed94d3cb0a84709, + 0x0963dded799a7c1a, + 0x70989faf596c8b65, + 0x41a3ed94d3cb0a84, ]), candidate: block_commit_1.clone(), - user_burns: vec![user_burn_1.clone(), user_burn_1_2.clone()], + user_burns: vec![], }, BurnSamplePoint { - burns: (block_commit_2.burn_fee - + user_burn_2.burn_fee - + user_burn_2_2.burn_fee) - .into(), + burns: block_commit_2.burn_fee.into(), range_start: Uint256([ - 0xcb48ed15c5086a5c, - 0x6b29682cfbe4089c, - 0x4a30e732285c18c9, - 0x5a7416b691bddbad, + 0x3ed94d3cb0a84709, + 0x0963dded799a7c1a, + 0x70989faf596c8b65, + 0x41a3ed94d3cb0a84, ]), range_end: Uint256([ - 0xa224e0451efa00f5, - 0xa57394a7b38d5b1c, - 0x6bfdbf24cdb0b617, - 0xd777aa6d9e769e59, + 0x7db29a7961508e12, + 0x12c7bbdaf334f834, + 0xe1313f5eb2d916ca, + 0x8347db29a7961508, ]), candidate: block_commit_2.clone(), - user_burns: vec![user_burn_2.clone(), user_burn_2_2.clone()], + user_burns: vec![], }, BurnSamplePoint { burns: (block_commit_3.burn_fee).into(), range_start: Uint256([ - 0xa224e0451efa00f5, - 0xa57394a7b38d5b1c, - 0x6bfdbf24cdb0b617, - 0xd777aa6d9e769e59, + 0x7db29a7961508e12, + 0x12c7bbdaf334f834, + 0xe1313f5eb2d916ca, + 0x8347db29a7961508, ]), range_end: Uint256::max(), candidate: block_commit_3.clone(), @@ -1506,6 +1519,7 @@ mod tests { for i in 0..fixtures.len() { let f = &fixtures[i]; + eprintln!("Fixture #{}", i); let dist = BurnSamplePoint::make_distribution( f.block_commits.iter().cloned().collect(), f.consumed_leader_keys.iter().cloned().collect(), diff --git a/src/chainstate/burn/mod.rs b/src/chainstate/burn/mod.rs index cad8794b5a..0657e57d51 100644 --- a/src/chainstate/burn/mod.rs +++ b/src/chainstate/burn/mod.rs @@ -498,6 +498,7 @@ mod tests { &prev_snapshot, &snapshot_row, &vec![], + &vec![], None, None, None, diff --git a/src/chainstate/burn/operations/leader_block_commit.rs b/src/chainstate/burn/operations/leader_block_commit.rs index 1152beae5d..110cb48767 100644 --- a/src/chainstate/burn/operations/leader_block_commit.rs +++ b/src/chainstate/burn/operations/leader_block_commit.rs @@ -21,7 +21,7 @@ use chainstate::burn::db::sortdb::{SortitionDB, SortitionHandleTx}; use chainstate::burn::operations::Error as op_error; use chainstate::burn::ConsensusHash; use chainstate::burn::Opcodes; -use chainstate::burn::{BlockHeaderHash, VRFSeed}; +use chainstate::burn::{BlockHeaderHash, SortitionId, VRFSeed}; use chainstate::stacks::index::TrieHash; use chainstate::stacks::{StacksAddress, StacksPrivateKey, StacksPublicKey}; @@ -57,10 +57,12 @@ struct ParsedData { parent_vtxindex: u16, key_block_ptr: u32, key_vtxindex: u16, - memo: Vec, + burn_parent_modulus: u8, + memo: u8, } pub static OUTPUTS_PER_COMMIT: usize = 2; +pub static BURN_BLOCK_MINED_AT_MODULUS: u64 = 5; impl LeaderBlockCommitOp { #[cfg(test)] @@ -76,6 +78,11 @@ impl LeaderBlockCommitOp { LeaderBlockCommitOp { sunset_burn: 0, block_height: block_height, + burn_parent_modulus: if block_height > 0 { + ((block_height - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8 + } else { + BURN_BLOCK_MINED_AT_MODULUS as u8 - 1 + }, new_seed: new_seed.clone(), key_block_ptr: paired_key.block_height as u32, key_vtxindex: paired_key.vtxindex as u16, @@ -125,10 +132,22 @@ impl LeaderBlockCommitOp { txid: Txid([0u8; 32]), vtxindex: 0, block_height: 0, + burn_parent_modulus: BURN_BLOCK_MINED_AT_MODULUS as u8 - 1, + burn_header_hash: BurnchainHeaderHash::zero(), } } + #[cfg(test)] + pub fn set_burn_height(&mut self, height: u64) { + self.block_height = height; + self.burn_parent_modulus = if height > 0 { + (height - 1) % BURN_BLOCK_MINED_AT_MODULUS + } else { + BURN_BLOCK_MINED_AT_MODULUS - 1 + } as u8; + } + pub fn expected_chained_utxo(sunset_finished: bool) -> u32 { if sunset_finished { 2 // if sunset has occurred, chained commits should spend the output after the burn commit @@ -138,12 +157,16 @@ impl LeaderBlockCommitOp { } } + fn burn_block_mined_at(&self) -> u64 { + self.burn_parent_modulus as u64 % BURN_BLOCK_MINED_AT_MODULUS + } + fn parse_data(data: &Vec) -> Option { /* Wire format: 0 2 3 35 67 71 73 77 79 80 |------|--|-------------|---------------|------|------|-----|-----|-----| - magic op block hash new seed parent parent key key memo + magic op block hash new seed parent parent key key burn_block_parent modulus block txoff block txoff Note that `data` is missing the first 3 bytes -- the magic and op have been stripped @@ -168,7 +191,12 @@ impl LeaderBlockCommitOp { let parent_vtxindex = parse_u16_from_be(&data[68..70]).unwrap(); let key_block_ptr = parse_u32_from_be(&data[70..74]).unwrap(); let key_vtxindex = parse_u16_from_be(&data[74..76]).unwrap(); - let memo = data[76..77].to_vec(); + + let burn_parent_modulus_and_memo_byte = data[76]; + + let burn_parent_modulus = ((burn_parent_modulus_and_memo_byte & 0b111) as u64 + % BURN_BLOCK_MINED_AT_MODULUS) as u8; + let memo = (burn_parent_modulus_and_memo_byte >> 3) & 0x1f; Some(ParsedData { block_header_hash, @@ -177,30 +205,31 @@ impl LeaderBlockCommitOp { parent_vtxindex, key_block_ptr, key_vtxindex, + burn_parent_modulus, memo, }) } pub fn from_tx( + burnchain: &Burnchain, block_header: &BurnchainBlockHeader, tx: &BurnchainTransaction, - pox_sunset_ht: u64, ) -> Result { LeaderBlockCommitOp::parse_from_tx( + burnchain, block_header.block_height, &block_header.block_hash, tx, - pox_sunset_ht, ) } /// parse a LeaderBlockCommitOp /// `pox_sunset_ht` is the height at which PoX *disables* pub fn parse_from_tx( + burnchain: &Burnchain, block_height: u64, block_hash: &BurnchainHeaderHash, tx: &BurnchainTransaction, - pox_sunset_ht: u64, ) -> Result { // can't be too careful... let mut outputs = tx.get_recipients(); @@ -265,7 +294,18 @@ impl LeaderBlockCommitOp { } // check if we've reached PoX disable - let (commit_outs, sunset_burn, burn_fee) = if block_height >= pox_sunset_ht { + let (commit_outs, sunset_burn, burn_fee) = if block_height + >= burnchain.pox_constants.sunset_end + { + // should be only one burn output + if !outputs[0].address.is_burn() { + return Err(op_error::BlockCommitBadOutputs); + } + let BurnchainRecipient { address, amount } = outputs.remove(0); + (vec![address], 0, amount) + // check if we're in a prepare phase + } else if burnchain.is_in_prepare_phase(block_height) { + // should be only one burn output if !outputs[0].address.is_burn() { return Err(op_error::BlockCommitBadOutputs); } @@ -330,7 +370,8 @@ impl LeaderBlockCommitOp { parent_vtxindex: data.parent_vtxindex, key_block_ptr: data.key_block_ptr, key_vtxindex: data.key_vtxindex, - memo: data.memo, + memo: vec![data.memo], + burn_parent_modulus: data.burn_parent_modulus, commit_outs, sunset_burn, @@ -361,7 +402,7 @@ impl StacksMessageCodec for LeaderBlockCommitOp { 0 2 3 35 67 71 73 77 79 80 |------|--|-------------|---------------|------|------|-----|-----|-----| - magic op block hash new seed parent parent key key memo + magic op block hash new seed parent parent key key burn parent modulus block txoff block txoff */ fn consensus_serialize(&self, fd: &mut W) -> Result<(), net_error> { @@ -373,11 +414,9 @@ impl StacksMessageCodec for LeaderBlockCommitOp { write_next(fd, &self.parent_vtxindex)?; write_next(fd, &self.key_block_ptr)?; write_next(fd, &self.key_vtxindex)?; - if self.memo.len() > 0 { - write_next(fd, &self.memo[0])?; - } else { - write_next(fd, &0u8)?; - } + let memo_burn_parent_modulus = + (self.memo.get(0).copied().unwrap_or(0x00) << 3) + (self.burn_parent_modulus & 0b111); + write_next(fd, &memo_burn_parent_modulus)?; Ok(()) } @@ -393,6 +432,13 @@ pub struct RewardSetInfo { pub recipients: Vec<(StacksAddress, u16)>, } +#[derive(Debug, Clone)] +pub struct MissedBlockCommit { + pub txid: Txid, + pub input: (Txid, u32), + pub intended_sortition: SortitionId, +} + impl RewardSetInfo { /// Takes an Option and produces the commit_outs /// for a corresponding LeaderBlockCommitOp. If RewardSetInfo is none, @@ -442,9 +488,11 @@ impl LeaderBlockCommitOp { ///////////////////////////////////////////////////////////////////////////////////// // This tx must have the expected commit or burn outputs: // * if there is a known anchor block for the current reward cycle, and this - // block commit descends from that block - // the commit outputs must = the expected set of commit outputs - // * otherwise, there must be no block commits + // block commit descends from that block, and this block commit is not in the + // prepare phase of the reward cycle, and there are still reward addresses + // left in this reward cycle to pay out to, then + // the commit outputs must = the expected set of commit outputs. + // * otherwise, the commit outputs must be burn outputs. ///////////////////////////////////////////////////////////////////////////////////// if let Some(reward_set_info) = reward_set_info { // we do some check-inversion here so that we check the commit_outs _before_ @@ -453,70 +501,84 @@ impl LeaderBlockCommitOp { // we want to make sure that any TX that forces us to perform the check // has either burned BTC or sent BTC to the PoX recipients - // first, handle a corner case: - // all of the commitment outputs are _burns_ - // _and_ the reward set chose two burn addresses as reward addresses. - // then, don't need to do a pox descendant check. - let recipient_set_all_burns = reward_set_info - .recipients - .iter() - .fold(true, |prior_is_burn, (addr, _)| { - prior_is_burn && addr.is_burn() - }); - - if recipient_set_all_burns { - if !self.all_outputs_burn() { - warn!("Invalid block commit: recipient set should be all burns"); + // if we're in the prepare phase, then this block-commit _must_ burn. + // No PoX descent check needs to be performed -- prepare-phase block commits + // stand alone. + if burnchain.is_in_prepare_phase(self.block_height) { + if let Err(e) = self.check_prepare_commit_burn() { + warn!("Invalid block commit: in block {} which is in the prepare phase, but did not burn to a single output as expected ({:?})", self.block_height, &e); return Err(op_error::BlockCommitBadOutputs); } } else { - let expect_pox_descendant = if self.all_outputs_burn() { - false - } else { - if self.commit_outs.len() != reward_set_info.recipients.len() { - warn!( - "Invalid block commit: expected {} PoX transfers, but commit has {}", - reward_set_info.recipients.len(), - self.commit_outs.len() - ); + // Not in prepare phase, so this can be either PoB or PoX (a descent check from the + // anchor block will be necessary if the block-commit is well-formed). + // + // first, handle a corner case: + // all of the commitment outputs are _burns_ + // _and_ the reward set chose two burn addresses as reward addresses. + // then, don't need to do a pox descendant check. + let recipient_set_all_burns = reward_set_info + .recipients + .iter() + .fold(true, |prior_is_burn, (addr, _)| { + prior_is_burn && addr.is_burn() + }); + + if recipient_set_all_burns { + if !self.all_outputs_burn() { + warn!("Invalid block commit: recipient set should be all burns"); return Err(op_error::BlockCommitBadOutputs); } - - // sort check_recipients and commit_outs so that we can perform an - // iterative equality check - let mut check_recipients: Vec<_> = reward_set_info - .recipients - .iter() - .map(|(addr, _)| addr.clone()) - .collect(); - check_recipients.sort(); - let mut commit_outs = self.commit_outs.clone(); - commit_outs.sort(); - for (expected_commit, found_commit) in commit_outs.iter().zip(check_recipients) - { - if expected_commit != &found_commit { - warn!("Invalid block commit: committed output {} does not match expected {}", - found_commit, expected_commit); + } else { + let expect_pox_descendant = if self.all_outputs_burn() { + false + } else { + if self.commit_outs.len() != reward_set_info.recipients.len() { + warn!( + "Invalid block commit: expected {} PoX transfers, but commit has {}", + reward_set_info.recipients.len(), + self.commit_outs.len() + ); return Err(op_error::BlockCommitBadOutputs); } - } - true - }; - let descended_from_anchor = tx.descended_from(parent_block_height, &reward_set_info.anchor_block) - .map_err(|e| { - error!("Failed to check whether parent (height={}) is descendent of anchor block={}: {}", - parent_block_height, &reward_set_info.anchor_block, e); - op_error::BlockCommitAnchorCheck})?; - if descended_from_anchor != expect_pox_descendant { - if descended_from_anchor { - warn!("Invalid block commit: descended from PoX anchor, but used burn outputs"); - } else { - warn!( - "Invalid block commit: not descended from PoX anchor, but used PoX outputs" - ); + // sort check_recipients and commit_outs so that we can perform an + // iterative equality check + let mut check_recipients: Vec<_> = reward_set_info + .recipients + .iter() + .map(|(addr, _)| addr.clone()) + .collect(); + check_recipients.sort(); + let mut commit_outs = self.commit_outs.clone(); + commit_outs.sort(); + for (expected_commit, found_commit) in + commit_outs.iter().zip(check_recipients) + { + if expected_commit != &found_commit { + warn!("Invalid block commit: committed output {} does not match expected {}", + found_commit, expected_commit); + return Err(op_error::BlockCommitBadOutputs); + } + } + true + }; + + let descended_from_anchor = tx.descended_from(parent_block_height, &reward_set_info.anchor_block) + .map_err(|e| { + error!("Failed to check whether parent (height={}) is descendent of anchor block={}: {}", + parent_block_height, &reward_set_info.anchor_block, e); + op_error::BlockCommitAnchorCheck})?; + if descended_from_anchor != expect_pox_descendant { + if descended_from_anchor { + warn!("Invalid block commit: descended from PoX anchor, but used burn outputs"); + } else { + warn!( + "Invalid block commit: not descended from PoX anchor, but used PoX outputs" + ); + } + return Err(op_error::BlockCommitBadOutputs); } - return Err(op_error::BlockCommitBadOutputs); } } } else { @@ -529,7 +591,7 @@ impl LeaderBlockCommitOp { Ok(()) } - fn check_after_pox_sunset(&self) -> Result<(), op_error> { + fn check_single_burn_output(&self) -> Result<(), op_error> { if self.commit_outs.len() != 1 { warn!("Invalid post-sunset block commit, should have 1 commit out"); return Err(op_error::BlockCommitBadOutputs); @@ -541,6 +603,14 @@ impl LeaderBlockCommitOp { Ok(()) } + fn check_after_pox_sunset(&self) -> Result<(), op_error> { + self.check_single_burn_output() + } + + fn check_prepare_commit_burn(&self) -> Result<(), op_error> { + self.check_single_burn_output() + } + pub fn check( &self, burnchain: &Burnchain, @@ -561,6 +631,37 @@ impl LeaderBlockCommitOp { return Err(op_error::BlockCommitBadInput); } + let intended_modulus = (self.burn_block_mined_at() + 1) % BURN_BLOCK_MINED_AT_MODULUS; + let actual_modulus = self.block_height % BURN_BLOCK_MINED_AT_MODULUS; + if actual_modulus != intended_modulus { + warn!("Invalid block commit: missed target block"; + "intended_modulus" => intended_modulus, + "actual_modulus" => actual_modulus, + "block_height" => self.block_height); + // This transaction "missed" its target burn block, the transaction + // is not valid, but we should allow this UTXO to "chain" to valid + // UTXOs to allow the miner windowing to work in the face of missed + // blocks. + let miss_distance = if actual_modulus > intended_modulus { + actual_modulus - intended_modulus + } else { + BURN_BLOCK_MINED_AT_MODULUS + actual_modulus - intended_modulus + }; + if miss_distance > self.block_height { + return Err(op_error::BlockCommitBadModulus); + } + let intended_sortition = tx + .get_ancestor_block_hash(self.block_height - miss_distance, &tx_tip)? + .ok_or_else(|| op_error::BlockCommitNoParent)?; + let missed_data = MissedBlockCommit { + input: self.input.clone(), + txid: self.txid.clone(), + intended_sortition, + }; + + return Err(op_error::MissedBlockCommit(missed_data)); + } + if self.block_height >= burnchain.pox_constants.sunset_end { self.check_after_pox_sunset()?; } else { @@ -731,11 +832,15 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843021; + burnchain.pox_constants.sunset_end = 16843022; + let err = LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843022, &BurnchainHeaderHash([0; 32]), &tx, - 16843022, ) .unwrap_err(); @@ -785,11 +890,15 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843021; + burnchain.pox_constants.sunset_end = 16843022; + let op = LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843022, &BurnchainHeaderHash([0; 32]), &tx, - 16843022, ) .unwrap(); @@ -840,11 +949,15 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843019; + burnchain.pox_constants.sunset_end = 16843020; + let op = LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843019, &BurnchainHeaderHash([0; 32]), &tx, - 16843020, ) .unwrap(); @@ -886,12 +999,16 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843019; + burnchain.pox_constants.sunset_end = 16843020; + // burn amount should have been 10, not 9 match LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843019, &BurnchainHeaderHash([0; 32]), &tx, - 16843020, ) .unwrap_err() { @@ -955,11 +1072,15 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843019; + burnchain.pox_constants.sunset_end = 16843020; + let op = LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843019, &BurnchainHeaderHash([0; 32]), &tx, - 16843020, ) .unwrap(); @@ -991,12 +1112,16 @@ mod tests { }], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843019; + burnchain.pox_constants.sunset_end = 16843020; + // not enough PoX outputs match LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843019, &BurnchainHeaderHash([0; 32]), &tx, - 16843020, ) .unwrap_err() { @@ -1036,12 +1161,16 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843019; + burnchain.pox_constants.sunset_end = 16843020; + // unequal PoX outputs match LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843019, &BurnchainHeaderHash([0; 32]), &tx, - 16843020, ) .unwrap_err() { @@ -1105,12 +1234,16 @@ mod tests { ], }); + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = 16843019; + burnchain.pox_constants.sunset_end = 16843020; + // 0 total burn match LeaderBlockCommitOp::parse_from_tx( + &burnchain, 16843019, &BurnchainHeaderHash([0; 32]), &tx, - 16843020, ) .unwrap_err() { @@ -1131,8 +1264,8 @@ mod tests { let tx_fixtures = vec![ OpFixture { // valid - txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006b483045022100eba8c0a57c1eb71cdfba0874de63cf37b3aace1e56dcbd61701548194a79af34022041dd191256f3f8a45562e5d60956bb871421ba69db605716250554b23b08277b012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000040000000000000000536a4c5069645b222222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333334041424350516061626370718039300000000000001976a914000000000000000000000000000000000000000088ac39300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".into(), - opstr: "69645b2222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333340414243505160616263707180".to_string(), + txstr: "01000000011111111111111111111111111111111111111111111111111111111111111111000000006b483045022100eba8c0a57c1eb71cdfba0874de63cf37b3aace1e56dcbd61701548194a79af34022041dd191256f3f8a45562e5d60956bb871421ba69db605716250554b23b08277b012102d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d000000000040000000000000000536a4c5069645b22222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333404142435051606162637071fa39300000000000001976a914000000000000000000000000000000000000000088ac39300000000000001976a914000000000000000000000000000000000000000088aca05b0000000000001976a9140be3e286a15ea85882761618e366586b5574100d88ac00000000".into(), + opstr: "69645b22222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333404142435051606162637071fa".to_string(), result: Some(LeaderBlockCommitOp { sunset_burn: 0, block_header_hash: BlockHeaderHash::from_bytes(&hex_bytes("2222222222222222222222222222222222222222222222222222222222222222").unwrap()).unwrap(), @@ -1141,7 +1274,7 @@ mod tests { parent_vtxindex: 0x5051, key_block_ptr: 0x60616263, key_vtxindex: 0x7071, - memo: vec![0x80], + memo: vec![0x1f], commit_outs: vec![ StacksAddress { version: 26, bytes: Hash160::empty() }, @@ -1158,9 +1291,10 @@ mod tests { hash_mode: AddressHashMode::SerializeP2PKH }, - txid: Txid::from_hex("b08d5d1bc81049a3957e9ff9a5882463811735fd5de985e6d894e9b3d5c49501").unwrap(), + txid: Txid::from_hex("502f3e5756de7e1bdba8c713cd2daab44adb5337d14ff668fdc57cc27d67f0d4").unwrap(), vtxindex: vtxindex, block_height: block_height, + burn_parent_modulus: ((block_height - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: burn_header_hash, }) }, @@ -1218,7 +1352,12 @@ mod tests { }; let burnchain_tx = BurnchainTransaction::Bitcoin(parser.parse_tx(&tx, vtxindex as usize).unwrap()); - let op = LeaderBlockCommitOp::from_tx(&header, &burnchain_tx, block_height + 1); + + let mut burnchain = Burnchain::regtest("nope"); + burnchain.pox_constants.sunset_start = block_height; + burnchain.pox_constants.sunset_end = block_height + 1; + + let op = LeaderBlockCommitOp::from_tx(&burnchain, &header, &burnchain_tx); match (op, tx_fixture.result) { (Ok(parsed_tx), Some(result)) => { @@ -1281,12 +1420,12 @@ mod tests { block_122_hash.clone(), block_123_hash.clone(), block_124_hash.clone(), - block_125_hash.clone(), - block_126_hash.clone(), + block_125_hash.clone(), // prepare phase + block_126_hash.clone(), // prepare phase ]; let burnchain = Burnchain { - pox_constants: PoxConstants::test_default(), + pox_constants: PoxConstants::new(6, 2, 2, 25, 5, 5000, 10000), peer_version: 0x012345678, network_id: 0x9abcdef0, chain_name: "bitcoin".to_string(), @@ -1395,6 +1534,7 @@ mod tests { .unwrap(), vtxindex: 444, block_height: 125, + burn_parent_modulus: (124 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_125_hash.clone(), }; @@ -1496,6 +1636,7 @@ mod tests { &prev_snapshot, &snapshot_row, &block_ops[i], + &vec![], None, None, None, @@ -1559,6 +1700,7 @@ mod tests { .unwrap(), vtxindex: 444, block_height: 80, + burn_parent_modulus: (79 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Err(op_error::BlockCommitPredatesGenesis), @@ -1608,6 +1750,7 @@ mod tests { .unwrap(), vtxindex: 444, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Err(op_error::BlockCommitNoLeaderKey), @@ -1657,6 +1800,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Err(op_error::BlockCommitNoParent), @@ -1706,6 +1850,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Err(op_error::BlockCommitNoParent), @@ -1755,6 +1900,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Ok(()), @@ -1804,6 +1950,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Err(op_error::BlockCommitBadInput), @@ -1853,6 +2000,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Ok(()), @@ -1902,6 +2050,7 @@ mod tests { .unwrap(), vtxindex: 445, block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, burn_header_hash: block_126_hash.clone(), }, res: Ok(()), diff --git a/src/chainstate/burn/operations/leader_key_register.rs b/src/chainstate/burn/operations/leader_key_register.rs index 08d8ca131b..f9bc7ae23a 100644 --- a/src/chainstate/burn/operations/leader_key_register.rs +++ b/src/chainstate/burn/operations/leader_key_register.rs @@ -640,6 +640,7 @@ pub mod tests { &prev_snapshot, &snapshot_row, &block_ops[i as usize], + &vec![], None, None, None, diff --git a/src/chainstate/burn/operations/mod.rs b/src/chainstate/burn/operations/mod.rs index 63887f10c2..8a16cd80fc 100644 --- a/src/chainstate/burn/operations/mod.rs +++ b/src/chainstate/burn/operations/mod.rs @@ -44,6 +44,9 @@ use util::hash::Sha512Trunc256Sum; use burnchains::BurnchainBlockHeader; use burnchains::Error as BurnchainError; +use chainstate::burn::operations::leader_block_commit::{ + MissedBlockCommit, BURN_BLOCK_MINED_AT_MODULUS, +}; use chainstate::burn::Opcodes; use chainstate::burn::VRFSeed; use chainstate::stacks::index::TrieHash; @@ -69,8 +72,9 @@ pub enum Error { BlockCommitNoParent, BlockCommitBadInput, BlockCommitBadOutputs, - BlockCommitAnchorCheck, + BlockCommitBadModulus, + MissedBlockCommit(MissedBlockCommit), // all the things that can go wrong with leader key register LeaderKeyAlreadyRegistered, @@ -111,7 +115,15 @@ impl fmt::Display for Error { Error::BlockCommitBadOutputs => { write!(f, "Block commit included a bad commitment output") } - + Error::BlockCommitBadModulus => { + write!(f, "Block commit included a bad burn block height modulus") + } + Error::MissedBlockCommit(_) => { + write!( + f, + "Block commit included in a burn block that was not intended" + ) + } Error::LeaderKeyAlreadyRegistered => { write!(f, "Leader key has already been registered") } @@ -213,6 +225,8 @@ pub struct LeaderBlockCommitOp { /// the input transaction, used in mining commitment smoothing pub input: (Txid, u32), + pub burn_parent_modulus: u8, + /// the apparent sender of the transaction. note: this /// is *not* authenticated, and should be used only /// for informational purposes (e.g., log messages) @@ -335,7 +349,9 @@ impl BlockstackOperationType { pub fn set_block_height(&mut self, height: u64) { match self { BlockstackOperationType::LeaderKeyRegister(ref mut data) => data.block_height = height, - BlockstackOperationType::LeaderBlockCommit(ref mut data) => data.block_height = height, + BlockstackOperationType::LeaderBlockCommit(ref mut data) => { + data.set_burn_height(height) + } BlockstackOperationType::UserBurnSupport(ref mut data) => data.block_height = height, BlockstackOperationType::StackStx(ref mut data) => data.block_height = height, BlockstackOperationType::PreStx(ref mut data) => data.block_height = height, diff --git a/src/chainstate/burn/operations/user_burn_support.rs b/src/chainstate/burn/operations/user_burn_support.rs index 76659635b9..3a3780b873 100644 --- a/src/chainstate/burn/operations/user_burn_support.rs +++ b/src/chainstate/burn/operations/user_burn_support.rs @@ -639,6 +639,7 @@ mod tests { &prev_snapshot, &snapshot_row, &block_ops[i as usize], + &vec![], None, None, None, diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index e8865ae3cf..2a6975222f 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -224,6 +224,8 @@ impl RewardSetProvider for OnChainRewardSetProvider { liquid_ustx, ); + test_debug!("PoX reward cycle threshold: {}, participation: {}, liquid_ustx: {}, num registered addrs: {}", threshold, participation, liquid_ustx, registered_addrs.len()); + if !burnchain .pox_constants .enough_participation(participation, liquid_ustx) @@ -378,11 +380,7 @@ pub fn get_next_recipients( provider, )?; sort_db - .get_next_block_recipients( - sortition_tip, - reward_cycle_info.as_ref(), - burnchain.pox_constants.sunset_end, - ) + .get_next_block_recipients(burnchain, sortition_tip, reward_cycle_info.as_ref()) .map_err(|e| Error::from(e)) } @@ -407,7 +405,11 @@ pub fn get_reward_cycle_info( })); } - info!("Beginning reward cycle. block_height={}", burn_height); + info!("Beginning reward cycle"; + "burn_height" => burn_height, + "reward_cycle_length" => burnchain.pox_constants.reward_cycle_length, + "prepare_phase_length" => burnchain.pox_constants.prepare_length); + let reward_cycle_info = { let ic = sort_db.index_handle(sortition_tip); ic.get_chosen_pox_anchor(&parent_bhh, &burnchain.pox_constants) diff --git a/src/chainstate/coordinator/tests.rs b/src/chainstate/coordinator/tests.rs index fb11c46247..29176b7a77 100644 --- a/src/chainstate/coordinator/tests.rs +++ b/src/chainstate/coordinator/tests.rs @@ -30,8 +30,10 @@ use chainstate::stacks::db::{ use chainstate::stacks::index::TrieHash; use core; use core::*; +use std::cmp; use monitoring::increment_stx_blocks_processed_counter; +use rusqlite::Connection; use std::collections::HashSet; use std::sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, @@ -86,6 +88,38 @@ pub fn produce_burn_block<'a, I: Iterator>( par: &BurnchainHeaderHash, mut ops: Vec, others: I, +) -> BurnchainHeaderHash { + let BurnchainBlockData { + header: par_header, .. + } = burnchain_db.get_burnchain_block(par).unwrap(); + assert_eq!(&par_header.block_hash, par); + let block_height = par_header.block_height + 1; + for op in ops.iter_mut() { + op.set_block_height(block_height); + } + + produce_burn_block_do_not_set_height(burnchain_db, par, ops, others) +} + +use chainstate::burn::distribution::BurnSamplePoint; + +fn get_burn_distribution(conn: &Connection, sortition: &SortitionId) -> Vec { + conn.query_row( + "SELECT data FROM snapshot_burn_distributions WHERE sortition_id = ?", + &[sortition], + |row| { + let data_str: String = row.get(0); + serde_json::from_str(&data_str).unwrap() + }, + ) + .unwrap() +} + +fn produce_burn_block_do_not_set_height<'a, I: Iterator>( + burnchain_db: &mut BurnchainDB, + par: &BurnchainHeaderHash, + mut ops: Vec, + others: I, ) -> BurnchainHeaderHash { let BurnchainBlockData { header: par_header, .. @@ -104,7 +138,6 @@ pub fn produce_burn_block<'a, I: Iterator>( }; for op in ops.iter_mut() { - op.set_block_height(block_height); op.set_burn_header_hash(block_hash.clone()); } @@ -461,6 +494,7 @@ fn make_genesis_block_with_recipients( vtxindex: 1, block_height: 0, + burn_parent_modulus: (BURN_BLOCK_MINED_AT_MODULUS - 1) as u8, burn_header_hash: BurnchainHeaderHash([0; 32]), }; @@ -529,6 +563,37 @@ fn make_stacks_block_with_recipients_and_sunset_burn( recipients: Option<&RewardSetInfo>, sunset_burn: u64, post_sunset_burn: bool, +) -> (BlockstackOperationType, StacksBlock) { + make_stacks_block_with_input( + sort_db, + state, + parent_block, + miner, + my_burn, + vrf_key, + key_index, + recipients, + sunset_burn, + post_sunset_burn, + (Txid([0; 32]), 0), + ) +} + +/// build a stacks block with just the coinbase off of +/// parent_block, in the canonical sortition fork of SortitionDB. +/// parent_block _must_ be included in the StacksChainState +fn make_stacks_block_with_input( + sort_db: &SortitionDB, + state: &mut StacksChainState, + parent_block: &BlockHeaderHash, + miner: &StacksPrivateKey, + my_burn: u64, + vrf_key: &VRFPrivateKey, + key_index: u32, + recipients: Option<&RewardSetInfo>, + sunset_burn: u64, + post_sunset_burn: bool, + input: (Txid, u32), ) -> (BlockstackOperationType, StacksBlock) { let tx_auth = TransactionAuth::from_p2pkh(miner).unwrap(); @@ -604,7 +669,7 @@ fn make_stacks_block_with_recipients_and_sunset_burn( sunset_burn, block_header_hash: block.block_hash(), burn_fee: my_burn, - input: (Txid([0; 32]), 0), + input, apparent_sender: BurnchainSigner { num_sigs: 1, hash_mode: address::AddressHashMode::SerializeP2PKH, @@ -622,12 +687,263 @@ fn make_stacks_block_with_recipients_and_sunset_burn( txid: next_txid(), vtxindex: (1 + key_index) as u32, block_height: 0, + burn_parent_modulus: (BURN_BLOCK_MINED_AT_MODULUS - 1) as u8, burn_header_hash: BurnchainHeaderHash([0; 32]), }; (BlockstackOperationType::LeaderBlockCommit(commit_op), block) } +#[test] +fn missed_block_commits() { + let path = "/tmp/stacks-blockchain-missed_block_commits"; + let _r = std::fs::remove_dir_all(path); + + let sunset_ht = 8000; + let pox_consts = Some(PoxConstants::new(5, 3, 3, 25, 5, 7010, sunset_ht)); + let burnchain_conf = get_burnchain(path, pox_consts.clone()); + + let vrf_keys: Vec<_> = (0..50).map(|_| VRFPrivateKey::new()).collect(); + let committers: Vec<_> = (0..50).map(|_| StacksPrivateKey::new()).collect(); + + let stacker = p2pkh_from(&StacksPrivateKey::new()); + let rewards = p2pkh_from(&StacksPrivateKey::new()); + let balance = 6_000_000_000 * (core::MICROSTACKS_PER_STACKS as u64); + let stacked_amt = 1_000_000_000 * (core::MICROSTACKS_PER_STACKS as u128); + let initial_balances = vec![(stacker.clone().into(), balance)]; + + setup_states( + &[path], + &vrf_keys, + &committers, + pox_consts.clone(), + Some(initial_balances), + ); + + let mut coord = make_coordinator(path, Some(burnchain_conf)); + + coord.handle_new_burnchain_block().unwrap(); + + let sort_db = get_sortition_db(path, pox_consts.clone()); + + let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); + assert_eq!(tip.block_height, 1); + assert_eq!(tip.sortition, false); + let (_, ops) = sort_db + .get_sortition_result(&tip.sortition_id) + .unwrap() + .unwrap(); + + // we should have all the VRF registrations accepted + assert_eq!(ops.accepted_ops.len(), vrf_keys.len()); + assert_eq!(ops.consumed_leader_keys.len(), 0); + + // process sequential blocks, and their sortitions... + let mut stacks_blocks: Vec<(SortitionId, StacksBlock)> = vec![]; + + let mut last_txid: Option = None; + let b = get_burnchain(path, None); + + for ix in 0..vrf_keys.len() { + let vrf_key = &vrf_keys[ix]; + let miner = &committers[ix]; + + let mut burnchain = get_burnchain_db(path, pox_consts.clone()); + let mut chainstate = get_chainstate(path); + + let parent = if ix == 0 { + BlockHeaderHash([0; 32]) + } else { + stacks_blocks[ix - 1].1.header.block_hash() + }; + + let burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); + let next_mock_header = BurnchainBlockHeader { + block_height: burnchain_tip.block_height + 1, + block_hash: BurnchainHeaderHash([0; 32]), + parent_block_hash: burnchain_tip.block_hash, + num_txs: 0, + timestamp: 1, + }; + + let reward_cycle_info = coord.get_reward_cycle_info(&next_mock_header).unwrap(); + let next_block_recipients = get_rw_sortdb(path, pox_consts.clone()) + .test_get_next_block_recipients(&b, reward_cycle_info.as_ref()) + .unwrap(); + + let mut ops = vec![]; + if ix % 6 == 4 { + let (mut bad_op, _) = make_stacks_block_with_input( + &sort_db, + &mut chainstate, + &parent, + miner, + 10000, + vrf_key, + ix as u32, + next_block_recipients.as_ref(), + 0, + false, + (last_txid.as_ref().unwrap().clone(), 3), + ); + last_txid = Some(bad_op.txid()); + bad_op.set_block_height(next_mock_header.block_height); + if let BlockstackOperationType::LeaderBlockCommit(ref mut op) = bad_op { + op.burn_parent_modulus = + ((next_mock_header.block_height - 2) % BURN_BLOCK_MINED_AT_MODULUS) as u8; + op.vtxindex = 3; + } else { + panic!("Should be leader block commit"); + } + ops.push(bad_op); + } + + let (mut good_op, block) = if ix == 0 { + make_genesis_block_with_recipients( + &sort_db, + &mut chainstate, + &parent, + miner, + 10000, + vrf_key, + ix as u32, + next_block_recipients.as_ref(), + ) + } else { + make_stacks_block_with_input( + &sort_db, + &mut chainstate, + &parent, + miner, + 10000, + vrf_key, + ix as u32, + next_block_recipients.as_ref(), + 0, + false, + (last_txid.as_ref().unwrap().clone(), 3), + ) + }; + + good_op.set_block_height(next_mock_header.block_height); + + let expected_winner = good_op.txid(); + ops.push(good_op); + + let burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); + + if ix % 6 == 3 { + // produce an empty block! + produce_burn_block( + &mut burnchain, + &burnchain_tip.block_hash, + vec![], + vec![].iter_mut(), + ); + } else { + // produce a block with one good op, + last_txid = Some(expected_winner.clone()); + produce_burn_block_do_not_set_height( + &mut burnchain, + &burnchain_tip.block_hash, + ops, + vec![].iter_mut(), + ); + } + // handle the sortition + coord.handle_new_burnchain_block().unwrap(); + + let b = get_burnchain(path, pox_consts.clone()); + + let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); + let burn_distribution = get_burn_distribution(sort_db.conn(), &tip.sortition_id); + eprintln!("{}", ix); + if ix % 6 == 3 { + assert!( + !tip.sortition, + "Sortition should not have occurred because the only block commit was invalid" + ); + // duplicate the last stacks_block + stacks_blocks.push(stacks_blocks[ix - 1].clone()); + } else { + // how many commit do we expect to see counted in the current window? + let expected_window_commits = if ix >= 6 { + 5 + } else { + if ix >= 3 { + ix + } else { + ix + 1 + } + }; + // there were 2 burn blocks before we started mining + let expected_window_size = cmp::min(6, ix + 3); + + let min_burn = 1; + let median_burn = if expected_window_commits > expected_window_size / 2 { + 10000 + } else if expected_window_size % 2 == 0 + && expected_window_commits == expected_window_size / 2 + { + (10000 + 1) / 2 + } else { + 1 + }; + let last_burn = if ix % 6 == 3 { 0 } else { 10000 }; + + assert_eq!( + burn_distribution[0].burns, + cmp::min(last_burn, median_burn), + "Burn distribution should match at ix = {}", + ix + ); + + assert_eq!(&tip.winning_block_txid, &expected_winner); + + // load the block into staging + let block_hash = block.header.block_hash(); + + assert_eq!(&tip.winning_stacks_block_hash, &block_hash); + stacks_blocks.push((tip.sortition_id.clone(), block.clone())); + + preprocess_block(&mut chainstate, &sort_db, &tip, block); + + // handle the stacks block + coord.handle_new_stacks_block().unwrap(); + } + } + + let stacks_tip = SortitionDB::get_canonical_stacks_chain_tip_hash(sort_db.conn()).unwrap(); + let mut chainstate = get_chainstate(path); + // 1 block of every 6 is missed + let missed_blocks = vrf_keys.len() / 6; + let expected_height = vrf_keys.len() - missed_blocks; + assert_eq!( + chainstate + .with_read_only_clarity_tx( + &sort_db.index_conn(), + &StacksBlockId::new(&stacks_tip.0, &stacks_tip.1), + |conn| conn + .with_readonly_clarity_env( + PrincipalData::parse("SP3Q4A5WWZ80REGBN0ZXNE540ECJ9JZ4A765Q5K2Q").unwrap(), + LimitedCostTracker::new_free(), + |env| env.eval_raw("block-height") + ) + .unwrap() + ) + .unwrap(), + Value::UInt(expected_height as u128), + ); + + { + let ic = sort_db.index_handle_at_tip(); + let pox_id = ic.get_pox_id().unwrap(); + assert_eq!(&pox_id.to_string(), + "11111111111", + "PoX ID should reflect the 5 reward cycles _with_ a known anchor block, plus the 'initial' known reward cycle at genesis"); + } +} + #[test] fn test_simple_setup() { let path = "/tmp/stacks-blockchain-simple-setup"; @@ -832,7 +1148,7 @@ fn test_sortition_with_reward_set() { let mut vrf_keys: Vec<_> = (0..150).map(|_| VRFPrivateKey::new()).collect(); let mut committers: Vec<_> = (0..150).map(|_| StacksPrivateKey::new()).collect(); - let reward_set_size = 10; + let reward_set_size = 4; let reward_set: Vec<_> = (0..reward_set_size) .map(|_| p2pkh_from(&StacksPrivateKey::new())) .collect(); @@ -873,6 +1189,8 @@ fn test_sortition_with_reward_set() { let vrf_key_wrong_outs = vrf_key_burners.split_off(50); let miner_wrong_outs = miner_burners.split_off(50); + let b = get_burnchain(path, None); + // track the reward set consumption let mut reward_recipients = HashSet::new(); for ix in 0..vrf_keys.len() { @@ -918,7 +1236,7 @@ fn test_sortition_with_reward_set() { reward_recipients.clear(); } let next_block_recipients = get_rw_sortdb(path, None) - .test_get_next_block_recipients(reward_cycle_info.as_ref(), 5000) + .test_get_next_block_recipients(&b, reward_cycle_info.as_ref()) .unwrap(); if let Some(ref next_block_recipients) = next_block_recipients { for (addr, _) in next_block_recipients.recipients.iter() { @@ -1081,7 +1399,7 @@ fn test_sortition_with_burner_reward_set() { let mut vrf_keys: Vec<_> = (0..150).map(|_| VRFPrivateKey::new()).collect(); let mut committers: Vec<_> = (0..150).map(|_| StacksPrivateKey::new()).collect(); - let reward_set_size = 9; + let reward_set_size = 3; let mut reward_set: Vec<_> = (0..reward_set_size - 1) .map(|_| StacksAddress::burn_address(false)) .collect(); @@ -1123,6 +1441,8 @@ fn test_sortition_with_burner_reward_set() { let vrf_key_wrong_outs = vrf_key_burners.split_off(50); let miner_wrong_outs = miner_burners.split_off(50); + let b = get_burnchain(path, None); + // track the reward set consumption let mut reward_recipients = HashSet::new(); for ix in 0..vrf_keys.len() { @@ -1165,7 +1485,7 @@ fn test_sortition_with_burner_reward_set() { reward_recipients.clear(); } let next_block_recipients = get_rw_sortdb(path, None) - .test_get_next_block_recipients(reward_cycle_info.as_ref(), 5000) + .test_get_next_block_recipients(&b, reward_cycle_info.as_ref()) .unwrap(); if let Some(ref next_block_recipients) = next_block_recipients { for (addr, _) in next_block_recipients.recipients.iter() { @@ -1324,7 +1644,7 @@ fn test_pox_btc_ops() { Some(initial_balances), ); - let mut coord = make_coordinator(path, Some(burnchain_conf)); + let mut coord = make_coordinator(path, Some(burnchain_conf.clone())); coord.handle_new_burnchain_block().unwrap(); @@ -1385,7 +1705,7 @@ fn test_pox_btc_ops() { reward_recipients.clear(); } let next_block_recipients = get_rw_sortdb(path, pox_consts.clone()) - .test_get_next_block_recipients(reward_cycle_info.as_ref(), sunset_ht) + .test_get_next_block_recipients(&burnchain_conf, reward_cycle_info.as_ref()) .unwrap(); if next_mock_header.block_height >= sunset_ht { assert!(next_block_recipients.is_none()); @@ -1581,7 +1901,7 @@ fn test_stx_transfer_btc_ops() { Some(initial_balances), ); - let mut coord = make_coordinator(path, Some(burnchain_conf)); + let mut coord = make_coordinator(path, Some(burnchain_conf.clone())); coord.handle_new_burnchain_block().unwrap(); @@ -1638,7 +1958,7 @@ fn test_stx_transfer_btc_ops() { reward_recipients.clear(); } let next_block_recipients = get_rw_sortdb(path, pox_consts.clone()) - .test_get_next_block_recipients(reward_cycle_info.as_ref(), sunset_ht) + .test_get_next_block_recipients(&burnchain_conf, reward_cycle_info.as_ref()) .unwrap(); if next_mock_header.block_height >= sunset_ht { assert!(next_block_recipients.is_none()); @@ -2069,13 +2389,14 @@ fn test_sortition_with_sunset() { let _r = std::fs::remove_dir_all(path); let sunset_ht = 80; - let pox_consts = Some(PoxConstants::new(5, 3, 3, 25, 5, 10, sunset_ht)); + let pox_consts = Some(PoxConstants::new(6, 3, 3, 25, 5, 10, sunset_ht)); let burnchain_conf = get_burnchain(path, pox_consts.clone()); let mut vrf_keys: Vec<_> = (0..200).map(|_| VRFPrivateKey::new()).collect(); let mut committers: Vec<_> = (0..200).map(|_| StacksPrivateKey::new()).collect(); - let reward_set_size = 10; + let reward_set_size = pox_consts.as_ref().unwrap().reward_slots() as usize; + assert_eq!(reward_set_size, 6); let reward_set: Vec<_> = (0..reward_set_size) .map(|_| p2pkh_from(&StacksPrivateKey::new())) .collect(); @@ -2144,13 +2465,35 @@ fn test_sortition_with_sunset() { // did we process a reward set last cycle? check if the // recipient set size matches our expectation if started_first_reward_cycle { - if burnchain_tip.block_height == sunset_ht { - // sunset finished in the last reward cycle, - // the last two slots were left unfilled. - assert_eq!(reward_recipients.len(), reward_set_size - 2); - } else if burnchain_tip.block_height > sunset_ht { + let last_reward_cycle_block = (sunset_ht + / (pox_consts.as_ref().unwrap().reward_cycle_length as u64)) + * (pox_consts.as_ref().unwrap().reward_cycle_length as u64); + if burnchain_tip.block_height == last_reward_cycle_block { + eprintln!( + "End of PoX (at sunset height {}): reward set size is {}", + burnchain_tip.block_height, + reward_recipients.len() + ); + assert_eq!(reward_recipients.len(), 6); // still hasn't cleared yet, so still 6 + } else if burnchain_tip.block_height + > last_reward_cycle_block + + (pox_consts.as_ref().unwrap().reward_cycle_length as u64) + { + eprintln!("End of PoX (beyond sunset height {} and in next reward cycle): reward set size is {}", burnchain_tip.block_height, reward_recipients.len()); assert_eq!(reward_recipients.len(), 0); + } else if burnchain_tip.block_height > last_reward_cycle_block { + eprintln!( + "End of PoX (beyond sunset height {}): reward set size is {}", + burnchain_tip.block_height, + reward_recipients.len() + ); + assert_eq!(reward_recipients.len(), 2); // still haven't cleared this yet, so still 2 } else { + eprintln!( + "End of PoX (before sunset height {}): reward set size is {}", + burnchain_tip.block_height, + reward_recipients.len() + ); assert_eq!(reward_recipients.len(), reward_set_size); } } @@ -2159,13 +2502,15 @@ fn test_sortition_with_sunset() { reward_recipients.clear(); } let next_block_recipients = get_rw_sortdb(path, pox_consts.clone()) - .test_get_next_block_recipients(reward_cycle_info.as_ref(), sunset_ht) + .test_get_next_block_recipients(&burnchain_conf, reward_cycle_info.as_ref()) .unwrap(); if next_mock_header.block_height >= sunset_ht { assert!(next_block_recipients.is_none()); } if let Some(ref next_block_recipients) = next_block_recipients { + // this is only Some(..) if we're pre-sunset + assert!(burnchain_tip.block_height <= sunset_ht); for (addr, _) in next_block_recipients.recipients.iter() { if !addr.is_burn() { assert!( @@ -2176,6 +2521,12 @@ fn test_sortition_with_sunset() { } reward_recipients.insert(addr.clone()); } + eprintln!( + "at {}: reward_recipients ({}) = {:?}", + burnchain_tip.block_height, + reward_recipients.len(), + reward_recipients + ); } let sunset_burn = burnchain_conf.expected_sunset_burn(next_mock_header.block_height, 10000); @@ -2207,6 +2558,7 @@ fn test_sortition_with_sunset() { ) }; + eprintln!("good op: {:?}", &good_op); let expected_winner = good_op.txid(); let mut ops = vec![good_op]; @@ -2293,7 +2645,7 @@ fn test_sortition_with_sunset() { let ic = sort_db.index_handle_at_tip(); let pox_id = ic.get_pox_id().unwrap(); assert_eq!(&pox_id.to_string(), - "111111111111111111111", + "11111111111111111", "PoX ID should reflect the 10 reward cycles _with_ a known anchor block, plus the 'initial' known reward cycle at genesis"); } } diff --git a/src/chainstate/stacks/auth.rs b/src/chainstate/stacks/auth.rs index e88e7894e4..9ae6ef881f 100644 --- a/src/chainstate/stacks/auth.rs +++ b/src/chainstate/stacks/auth.rs @@ -127,7 +127,7 @@ impl StacksMessageCodec for MultisigSpendingCondition { write_next(fd, &(self.hash_mode.clone() as u8))?; write_next(fd, &self.signer)?; write_next(fd, &self.nonce)?; - write_next(fd, &self.fee_rate)?; + write_next(fd, &self.tx_fee)?; write_next(fd, &self.fields)?; write_next(fd, &self.signatures_required)?; Ok(()) @@ -143,7 +143,7 @@ impl StacksMessageCodec for MultisigSpendingCondition { let signer: Hash160 = read_next(fd)?; let nonce: u64 = read_next(fd)?; - let fee_rate: u64 = read_next(fd)?; + let tx_fee: u64 = read_next(fd)?; let fields: Vec = { let mut bound_read = BoundReader::from_reader(fd, MAX_MESSAGE_LEN as u64); read_next(&mut bound_read) @@ -203,7 +203,7 @@ impl StacksMessageCodec for MultisigSpendingCondition { Ok(MultisigSpendingCondition { signer, nonce, - fee_rate, + tx_fee, hash_mode, fields, signatures_required, @@ -272,7 +272,7 @@ impl MultisigSpendingCondition { let (pubkey, next_sighash) = TransactionSpendingCondition::next_verification( &cur_sighash, cond_code, - self.fee_rate, + self.tx_fee, self.nonce, pubkey_encoding, sigbuf, @@ -329,7 +329,7 @@ impl StacksMessageCodec for SinglesigSpendingCondition { write_next(fd, &(self.hash_mode.clone() as u8))?; write_next(fd, &self.signer)?; write_next(fd, &self.nonce)?; - write_next(fd, &self.fee_rate)?; + write_next(fd, &self.tx_fee)?; write_next(fd, &(self.key_encoding.clone() as u8))?; write_next(fd, &self.signature)?; Ok(()) @@ -346,7 +346,7 @@ impl StacksMessageCodec for SinglesigSpendingCondition { let signer: Hash160 = read_next(fd)?; let nonce: u64 = read_next(fd)?; - let fee_rate: u64 = read_next(fd)?; + let tx_fee: u64 = read_next(fd)?; let key_encoding_u8: u8 = read_next(fd)?; let key_encoding = TransactionPublicKeyEncoding::from_u8(key_encoding_u8).ok_or( @@ -369,7 +369,7 @@ impl StacksMessageCodec for SinglesigSpendingCondition { Ok(SinglesigSpendingCondition { signer: signer, nonce: nonce, - fee_rate: fee_rate, + tx_fee: tx_fee, hash_mode: hash_mode, key_encoding: key_encoding, signature: signature, @@ -430,7 +430,7 @@ impl SinglesigSpendingCondition { let (pubkey, next_sighash) = TransactionSpendingCondition::next_verification( initial_sighash, cond_code, - self.fee_rate, + self.tx_fee, self.nonce, &self.key_encoding, &self.signature, @@ -514,7 +514,7 @@ impl TransactionSpendingCondition { SinglesigSpendingCondition { signer: signer_addr.bytes.clone(), nonce: 0, - fee_rate: 0, + tx_fee: 0, hash_mode: SinglesigHashMode::P2PKH, key_encoding: key_encoding, signature: MessageSignature::empty(), @@ -534,7 +534,7 @@ impl TransactionSpendingCondition { SinglesigSpendingCondition { signer: signer_addr.bytes.clone(), nonce: 0, - fee_rate: 0, + tx_fee: 0, hash_mode: SinglesigHashMode::P2WPKH, key_encoding: TransactionPublicKeyEncoding::Compressed, signature: MessageSignature::empty(), @@ -557,7 +557,7 @@ impl TransactionSpendingCondition { MultisigSpendingCondition { signer: signer_addr.bytes.clone(), nonce: 0, - fee_rate: 0, + tx_fee: 0, hash_mode: MultisigHashMode::P2SH, fields: vec![], signatures_required: num_sigs, @@ -580,7 +580,7 @@ impl TransactionSpendingCondition { MultisigSpendingCondition { signer: signer_addr.bytes.clone(), nonce: 0, - fee_rate: 0, + tx_fee: 0, hash_mode: MultisigHashMode::P2WSH, fields: vec![], signatures_required: num_sigs, @@ -595,7 +595,7 @@ impl TransactionSpendingCondition { TransactionSpendingCondition::Singlesig(SinglesigSpendingCondition { signer: Hash160([0u8; 20]), nonce: 0, - fee_rate: 0, + tx_fee: 0, hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Compressed, signature: MessageSignature::empty(), @@ -641,10 +641,10 @@ impl TransactionSpendingCondition { } } - pub fn fee_rate(&self) -> u64 { + pub fn tx_fee(&self) -> u64 { match *self { - TransactionSpendingCondition::Singlesig(ref data) => data.fee_rate, - TransactionSpendingCondition::Multisig(ref data) => data.fee_rate, + TransactionSpendingCondition::Singlesig(ref data) => data.tx_fee, + TransactionSpendingCondition::Multisig(ref data) => data.tx_fee, } } @@ -659,21 +659,21 @@ impl TransactionSpendingCondition { } } - pub fn set_fee_rate(&mut self, fee_rate: u64) -> () { + pub fn set_tx_fee(&mut self, tx_fee: u64) -> () { match *self { TransactionSpendingCondition::Singlesig(ref mut singlesig_data) => { - singlesig_data.fee_rate = fee_rate; + singlesig_data.tx_fee = tx_fee; } TransactionSpendingCondition::Multisig(ref mut multisig_data) => { - multisig_data.fee_rate = fee_rate; + multisig_data.tx_fee = tx_fee; } } } - pub fn get_fee_rate(&self) -> u64 { + pub fn get_tx_fee(&self) -> u64 { match *self { - TransactionSpendingCondition::Singlesig(ref singlesig_data) => singlesig_data.fee_rate, - TransactionSpendingCondition::Multisig(ref multisig_data) => multisig_data.fee_rate, + TransactionSpendingCondition::Singlesig(ref singlesig_data) => singlesig_data.tx_fee, + TransactionSpendingCondition::Multisig(ref multisig_data) => multisig_data.tx_fee, } } @@ -697,12 +697,12 @@ impl TransactionSpendingCondition { pub fn clear(&mut self) -> () { match *self { TransactionSpendingCondition::Singlesig(ref mut singlesig_data) => { - singlesig_data.fee_rate = 0; + singlesig_data.tx_fee = 0; singlesig_data.nonce = 0; singlesig_data.signature = MessageSignature::empty(); } TransactionSpendingCondition::Multisig(ref mut multisig_data) => { - multisig_data.fee_rate = 0; + multisig_data.tx_fee = 0; multisig_data.nonce = 0; multisig_data.fields.clear(); } @@ -712,7 +712,7 @@ impl TransactionSpendingCondition { pub fn make_sighash_presign( cur_sighash: &Txid, cond_code: &TransactionAuthFlags, - fee_rate: u64, + tx_fee: u64, nonce: u64, ) -> Txid { // new hash combines the previous hash and all the new data this signature will add. This @@ -726,7 +726,7 @@ impl TransactionSpendingCondition { new_tx_hash_bits.extend_from_slice(cur_sighash.as_bytes()); new_tx_hash_bits.extend_from_slice(&[*cond_code as u8]); - new_tx_hash_bits.extend_from_slice(&fee_rate.to_be_bytes()); + new_tx_hash_bits.extend_from_slice(&tx_fee.to_be_bytes()); new_tx_hash_bits.extend_from_slice(&nonce.to_be_bytes()); assert!(new_tx_hash_bits.len() == new_tx_hash_bits_len as usize); @@ -771,14 +771,14 @@ impl TransactionSpendingCondition { pub fn next_signature( cur_sighash: &Txid, cond_code: &TransactionAuthFlags, - fee_rate: u64, + tx_fee: u64, nonce: u64, privk: &StacksPrivateKey, ) -> Result<(MessageSignature, Txid), net_error> { let sighash_presign = TransactionSpendingCondition::make_sighash_presign( cur_sighash, cond_code, - fee_rate, + tx_fee, nonce, ); @@ -801,7 +801,7 @@ impl TransactionSpendingCondition { pub fn next_verification( cur_sighash: &Txid, cond_code: &TransactionAuthFlags, - fee_rate: u64, + tx_fee: u64, nonce: u64, key_encoding: &TransactionPublicKeyEncoding, sig: &MessageSignature, @@ -809,7 +809,7 @@ impl TransactionSpendingCondition { let sighash_presign = TransactionSpendingCondition::make_sighash_presign( cur_sighash, cond_code, - fee_rate, + tx_fee, nonce, ); @@ -1028,17 +1028,17 @@ impl TransactionAuth { } } - pub fn set_fee_rate(&mut self, fee_rate: u64) -> () { + pub fn set_tx_fee(&mut self, tx_fee: u64) -> () { match *self { - TransactionAuth::Standard(ref mut s) => s.set_fee_rate(fee_rate), - TransactionAuth::Sponsored(_, ref mut s) => s.set_fee_rate(fee_rate), + TransactionAuth::Standard(ref mut s) => s.set_tx_fee(tx_fee), + TransactionAuth::Sponsored(_, ref mut s) => s.set_tx_fee(tx_fee), } } - pub fn get_fee_rate(&self) -> u64 { + pub fn get_tx_fee(&self) -> u64 { match *self { - TransactionAuth::Standard(ref s) => s.get_fee_rate(), - TransactionAuth::Sponsored(_, ref s) => s.get_fee_rate(), + TransactionAuth::Standard(ref s) => s.get_tx_fee(), + TransactionAuth::Sponsored(_, ref s) => s.get_tx_fee(), } } @@ -1094,7 +1094,7 @@ mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Uncompressed, nonce: 123, - fee_rate: 456, + tx_fee: 456, signature: MessageSignature::from_raw(&vec![0xff; 65]), }; @@ -1215,7 +1215,7 @@ mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 345, - fee_rate: 456, + tx_fee: 456, signature: MessageSignature::from_raw(&vec![0xfe; 65]), }; @@ -1355,7 +1355,7 @@ mod test { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2SH, nonce: 123, - fee_rate: 456, + tx_fee: 456, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -1592,7 +1592,7 @@ mod test { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2SH, nonce: 456, - fee_rate: 567, + tx_fee: 567, fields: vec![ TransactionAuthField::Signature( TransactionPublicKeyEncoding::Compressed, @@ -1860,7 +1860,7 @@ mod test { hash_mode: SinglesigHashMode::P2WPKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 345, - fee_rate: 567, + tx_fee: 567, signature: MessageSignature::from_raw(&vec![0xfe; 65]), }; @@ -1993,7 +1993,7 @@ mod test { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2WSH, nonce: 456, - fee_rate: 567, + tx_fee: 567, fields: vec![ TransactionAuthField::Signature( TransactionPublicKeyEncoding::Compressed, @@ -2257,7 +2257,7 @@ mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Uncompressed, nonce: 123, - fee_rate: 567, + tx_fee: 567, signature: MessageSignature::from_raw(&vec![0xff; 65]) }), TransactionSpendingCondition::Singlesig(SinglesigSpendingCondition { @@ -2265,14 +2265,14 @@ mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 345, - fee_rate: 567, + tx_fee: 567, signature: MessageSignature::from_raw(&vec![0xff; 65]) }), TransactionSpendingCondition::Multisig(MultisigSpendingCondition { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2SH, nonce: 123, - fee_rate: 567, + tx_fee: 567, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -2284,7 +2284,7 @@ mod test { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2SH, nonce: 456, - fee_rate: 567, + tx_fee: 567, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -2297,14 +2297,14 @@ mod test { hash_mode: SinglesigHashMode::P2WPKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 345, - fee_rate: 567, + tx_fee: 567, signature: MessageSignature::from_raw(&vec![0xfe; 65]), }), TransactionSpendingCondition::Multisig(MultisigSpendingCondition { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2WSH, nonce: 456, - fee_rate: 567, + tx_fee: 567, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -3150,7 +3150,7 @@ mod test { signer: Hash160([0x11; 20]), hash_mode: SinglesigHashMode::P2WPKH, nonce: 123, - fee_rate: 567, + tx_fee: 567, key_encoding: TransactionPublicKeyEncoding::Uncompressed, signature: MessageSignature::from_raw(&vec![0xff; 65]), }); @@ -3272,7 +3272,7 @@ mod test { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2WSH, nonce: 456, - fee_rate: 567, + tx_fee: 567, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -3581,7 +3581,7 @@ mod test { TransactionAuthFlags::AuthSponsored, ]; - let fee_rates = vec![123, 456, 123, 456]; + let tx_fees = vec![123, 456, 123, 456]; let nonces: Vec = vec![1, 2, 3, 4]; @@ -3589,7 +3589,7 @@ mod test { let (sig, next_sighash) = TransactionSpendingCondition::next_signature( &cur_sighash, &auth_flags[i], - fee_rates[i], + tx_fees[i], nonces[i], &keys[i], ) @@ -3600,7 +3600,7 @@ mod test { expected_sighash_bytes.clear(); expected_sighash_bytes.extend_from_slice(cur_sighash.as_bytes()); expected_sighash_bytes.extend_from_slice(&[auth_flags[i] as u8]); - expected_sighash_bytes.extend_from_slice(&fee_rates[i].to_be_bytes()); + expected_sighash_bytes.extend_from_slice(&tx_fees[i].to_be_bytes()); expected_sighash_bytes.extend_from_slice(&nonces[i].to_be_bytes()); let expected_sighash_presign = Txid::from_sighash_bytes(&expected_sighash_bytes[..]); @@ -3622,7 +3622,7 @@ mod test { TransactionSpendingCondition::next_verification( &cur_sighash, &auth_flags[i], - fee_rates[i], + tx_fees[i], nonces[i], &key_encoding, &sig, diff --git a/src/chainstate/stacks/block.rs b/src/chainstate/stacks/block.rs index 41082032c4..727ba8c4af 100644 --- a/src/chainstate/stacks/block.rs +++ b/src/chainstate/stacks/block.rs @@ -931,6 +931,7 @@ impl StacksMicroblock { #[cfg(test)] mod test { use super::*; + use chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use chainstate::stacks::test::*; use chainstate::stacks::*; use net::codec::test::*; @@ -1397,6 +1398,8 @@ mod test { .unwrap(), vtxindex: 444, block_height: 125, + burn_parent_modulus: (124 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: BurnchainHeaderHash([0xff; 32]), }; diff --git a/src/chainstate/stacks/boot/bns.clar b/src/chainstate/stacks/boot/bns.clar index c004fb57b3..2143cb8e19 100644 --- a/src/chainstate/stacks/boot/bns.clar +++ b/src/chainstate/stacks/boot/bns.clar @@ -101,14 +101,6 @@ (define-private (max (a uint) (b uint)) (if (> a b) a b)) -(define-read-only (compute-namespace-price? (namespace (buff 20))) - (let ((namespace-len (len namespace))) - (asserts! - (> namespace-len u0) - (err ERR_NAMESPACE_BLANK)) - (ok (unwrap-panic - (element-at NAMESPACE_PRICE_TIERS (min u7 (- namespace-len u1))))))) - (define-private (get-exp-at-index (buckets (list 16 uint)) (index uint)) (unwrap-panic (element-at buckets index))) @@ -301,6 +293,24 @@ (> block-height (+ (get revealed-at namespace-props) NAMESPACE_LAUNCHABILITY_TTL)))) ;; Is the namespace expired? true)) +(define-private (compute-name-price (name (buff 32)) + (price-function (tuple (buckets (list 16 uint)) + (base uint) + (coeff uint) + (nonalpha-discount uint) + (no-vowel-discount uint)))) + (let ( + (exponent (get-exp-at-index (get buckets price-function) (min u15 (- (len name) u1)))) + (no-vowel-discount (if (not (has-vowels-chars name)) (get no-vowel-discount price-function) u1)) + (nonalpha-discount (if (has-nonalpha-chars name) (get nonalpha-discount price-function) u1))) + (* + (/ + (* + (get coeff price-function) + (pow (get base price-function) exponent)) + (max nonalpha-discount no-vowel-discount)) + u10))) + ;;;; NAMESPACES ;; NAMESPACE_PREORDER ;; This step registers the salted hash of the namespace with BNS nodes, and burns the requisite amount of cryptocurrency. @@ -384,14 +394,11 @@ (base p-func-base) (coeff p-func-coeff) (nonalpha-discount p-func-non-alpha-discount) - (no-vowel-discount p-func-no-vowel-discount)))) - (let ( - (preorder (unwrap! - (map-get? namespace-preorders { hashed-salted-namespace: hashed-salted-namespace, buyer: tx-sender }) - (err ERR_NAMESPACE_PREORDER_NOT_FOUND))) - (namespace-price (unwrap! - (compute-namespace-price? namespace) - (err ERR_NAMESPACE_BLANK)))) + (no-vowel-discount p-func-no-vowel-discount))) + (preorder (unwrap! + (map-get? namespace-preorders { hashed-salted-namespace: hashed-salted-namespace, buyer: tx-sender }) + (err ERR_NAMESPACE_PREORDER_NOT_FOUND))) + (namespace-price (try! (get-namespace-price namespace)))) ;; The namespace must only have valid chars (asserts! (not (has-invalid-chars namespace)) @@ -421,7 +428,7 @@ launched-at: none, lifetime: lifetime, price-function: price-function }) - (ok true)))) + (ok true))) ;; NAME_IMPORT ;; Once a namespace is revealed, the user has the option to populate it with a set of names. Each imported name is given @@ -434,6 +441,10 @@ (namespace-props (unwrap! (map-get? namespaces { namespace: namespace }) (err ERR_NAMESPACE_NOT_FOUND)))) + ;; The name must only have valid chars + (asserts! + (not (has-invalid-chars name)) + (err ERR_NAME_CHARSET_INVALID)) ;; The sender principal must match the namespace's import principal (asserts! (is-eq (get namespace-import namespace-props) tx-sender) @@ -527,15 +538,13 @@ (hashed-salted-fqn (hash160 (concat (concat (concat name 0x2e) namespace) salt))) (namespace-props (unwrap! (map-get? namespaces { namespace: namespace }) - (err ERR_NAMESPACE_NOT_FOUND)))) - (let ( - (preorder (unwrap! - (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: tx-sender }) - (err ERR_NAME_PREORDER_NOT_FOUND)))) - ;; The name's namespace must be launched - (asserts! - (is-some (get launched-at namespace-props)) - (err ERR_NAMESPACE_NOT_LAUNCHED)) + (err ERR_NAMESPACE_NOT_FOUND))) + (preorder (unwrap! + (map-get? name-preorders { hashed-salted-fqn: hashed-salted-fqn, buyer: tx-sender }) + (err ERR_NAME_PREORDER_NOT_FOUND)))) + ;; The name can be registered + (asserts! (try! (can-name-be-registered namespace name)) + (err ERR_NAME_UNAVAILABLE)) ;; The preorder must have been created after the launch of the namespace (asserts! (> (get created-at preorder) (unwrap-panic (get launched-at namespace-props))) @@ -550,7 +559,7 @@ (err ERR_NAME_CLAIMABILITY_EXPIRED)) ;; The amount burnt must be equal to or greater than the cost of the name (asserts! - (>= (get stx-burned preorder) (unwrap-panic (compute-name-price name (get price-function namespace-props)))) + (>= (get stx-burned preorder) (compute-name-price name (get price-function namespace-props))) (err ERR_NAME_STX_BURNT_INSUFFICIENT)) ;; Mint the name if new, transfer the name otherwise. (try! (mint-or-transfer-name? namespace name tx-sender)) @@ -563,7 +572,7 @@ none zonefile-hash "name-register") - (ok true)))) + (ok true))) ;; NAME_UPDATE ;; A NAME_UPDATE transaction changes the name's zone file hash. You would send one of these transactions @@ -682,7 +691,7 @@ true) ;; The amount burnt must be equal to or greater than the cost of the namespace (asserts! - (>= stx-to-burn (unwrap-panic (compute-name-price name (get price-function namespace-props)))) + (>= stx-to-burn (compute-name-price name (get price-function namespace-props))) (err ERR_NAME_STX_BURNT_INSUFFICIENT)) ;; The name must not be revoked (asserts! @@ -711,6 +720,22 @@ (ok true))) ;; Additionals public methods + +(define-read-only (get-namespace-price (namespace (buff 20))) + (let ((namespace-len (len namespace))) + (asserts! + (> namespace-len u0) + (err ERR_NAMESPACE_BLANK)) + (ok (unwrap-panic + (element-at NAMESPACE_PRICE_TIERS (min u7 (- namespace-len u1))))))) + +(define-read-only (get-name-price (namespace (buff 20)) (name (buff 32))) + (let ( + (namespace-props (unwrap! + (map-get? namespaces { namespace: namespace }) + (err ERR_NAMESPACE_NOT_FOUND)))) + (ok (compute-name-price name (get price-function namespace-props))))) + (define-read-only (check-name-ops-preconditions (namespace (buff 20)) (name (buff 32))) (let ( (owner (unwrap! @@ -744,24 +769,6 @@ (err ERR_NAME_REVOKED)) (ok { namespace-props: namespace-props, name-props: name-props, owner: owner }))) -(define-read-only (compute-name-price (name (buff 32)) - (price-function (tuple (buckets (list 16 uint)) - (base uint) - (coeff uint) - (nonalpha-discount uint) - (no-vowel-discount uint)))) - (let ( - (exponent (get-exp-at-index (get buckets price-function) (min u15 (- (len name) u1)))) - (no-vowel-discount (if (not (has-vowels-chars name)) (get no-vowel-discount price-function) u1)) - (nonalpha-discount (if (has-nonalpha-chars name) (get nonalpha-discount price-function) u1))) - (ok (* - (/ - (* - (get coeff price-function) - (pow (get base price-function) exponent)) - (max nonalpha-discount no-vowel-discount)) - u10)))) - (define-read-only (can-namespace-be-registered (namespace (buff 20))) (ok (is-namespace-available namespace))) @@ -772,12 +779,12 @@ (err ERR_NAMESPACE_NOT_FOUND))) (name-props (unwrap! (map-get? name-properties { name: name, namespace: namespace }) - (err ERR_NAME_NOT_FOUND)))) - (let ((lease-started-at (try! (name-lease-started-at? (get launched-at namespace-props) (get revealed-at namespace-props) name-props))) - (lifetime (get lifetime namespace-props))) - (if (is-eq lifetime u0) - (ok false) - (ok (> block-height (+ lifetime lease-started-at))))))) + (err ERR_NAME_NOT_FOUND))) + (lease-started-at (try! (name-lease-started-at? (get launched-at namespace-props) (get revealed-at namespace-props) name-props))) + (lifetime (get lifetime namespace-props))) + (if (is-eq lifetime u0) + (ok false) + (ok (> block-height (+ lifetime lease-started-at)))))) (define-read-only (is-name-in-grace-period (namespace (buff 20)) (name (buff 32))) (let ( @@ -786,14 +793,14 @@ (err ERR_NAMESPACE_NOT_FOUND))) (name-props (unwrap! (map-get? name-properties { name: name, namespace: namespace }) - (err ERR_NAME_NOT_FOUND)))) - (let ((lease-started-at (try! (name-lease-started-at? (get launched-at namespace-props) (get revealed-at namespace-props) name-props))) - (lifetime (get lifetime namespace-props))) - (if (is-eq lifetime u0) - (ok false) - (ok (and - (> block-height (+ lifetime lease-started-at)) - (<= block-height (+ (+ lifetime lease-started-at) NAME_GRACE_PERIOD_DURATION)))))))) + (err ERR_NAME_NOT_FOUND))) + (lease-started-at (try! (name-lease-started-at? (get launched-at namespace-props) (get revealed-at namespace-props) name-props))) + (lifetime (get lifetime namespace-props))) + (if (is-eq lifetime u0) + (ok false) + (ok (and + (> block-height (+ lifetime lease-started-at)) + (<= block-height (+ (+ lifetime lease-started-at) NAME_GRACE_PERIOD_DURATION))))))) (define-read-only (can-receive-name (owner principal)) (let ((current-owned-name (map-get? owner-name { owner: owner }))) diff --git a/src/chainstate/stacks/boot/contract_tests.rs b/src/chainstate/stacks/boot/contract_tests.rs index dc30c49bcb..a286fa00d5 100644 --- a/src/chainstate/stacks/boot/contract_tests.rs +++ b/src/chainstate/stacks/boot/contract_tests.rs @@ -491,7 +491,7 @@ fn delegation_tests() { "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", Value::from(&USER_KEYS[0]), Value::UInt(*MIN_THRESHOLD - 1), - Value::UInt(360) + Value::UInt(450) )) ); @@ -599,7 +599,7 @@ fn delegation_tests() { "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", Value::from(&USER_KEYS[2]), Value::UInt(*MIN_THRESHOLD - 1), - Value::UInt(240) + Value::UInt(300) )) ); @@ -703,7 +703,7 @@ fn delegation_tests() { "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", Value::from(&USER_KEYS[3]), Value::UInt(*MIN_THRESHOLD), - Value::UInt(360) + Value::UInt(450) )) ); @@ -814,7 +814,7 @@ fn delegation_tests() { "(ok {{ stacker: '{}, lock-amount: {}, unlock-burn-height: {} }})", Value::from(&USER_KEYS[1]), Value::UInt(*MIN_THRESHOLD), - Value::UInt(360) + Value::UInt(450) )) ); @@ -1331,3 +1331,134 @@ fn test_vote_confirm() { ); }); } + +#[test] +fn test_vote_too_many_confirms() { + let mut sim = ClarityTestSim::new(); + + let MAX_CONFIRMATIONS_PER_BLOCK = 10; + sim.execute_next_block(|env| { + env.initialize_contract(COST_VOTING_CONTRACT.clone(), &BOOT_CODE_COST_VOTING) + .unwrap(); + + // Submit a proposal + for i in 0..(MAX_CONFIRMATIONS_PER_BLOCK + 1) { + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + COST_VOTING_CONTRACT.clone(), + "submit-proposal", + &symbols_from_values(vec![ + Value::Principal( + PrincipalData::parse_qualified_contract_principal( + "ST000000000000000000002AMW42H.function-name2" + ) + .unwrap() + ), + Value::string_ascii_from_bytes("function-name2".into()).unwrap(), + Value::Principal( + PrincipalData::parse_qualified_contract_principal( + "ST000000000000000000002AMW42H.cost-function-name2" + ) + .unwrap() + ), + Value::string_ascii_from_bytes("cost-function-name2".into()).unwrap(), + ]) + ) + .unwrap() + .0, + Value::Response(ResponseData { + committed: true, + data: Value::UInt(i as u128).into() + }) + ); + } + + for i in 0..(MAX_CONFIRMATIONS_PER_BLOCK + 1) { + // Commit all liquid stacks to vote + for user in USER_KEYS.iter() { + assert_eq!( + env.execute_transaction( + user.into(), + COST_VOTING_CONTRACT.clone(), + "vote-proposal", + &symbols_from_values(vec![ + Value::UInt(i as u128), + Value::UInt(USTX_PER_HOLDER) + ]), + ) + .unwrap() + .0, + Value::okay_true() + ); + } + + // Assert confirmation returns true + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + COST_VOTING_CONTRACT.clone(), + "confirm-votes", + &symbols_from_values(vec![Value::UInt(i as u128)]) + ) + .unwrap() + .0, + Value::okay_true(), + ); + + // withdraw + for user in USER_KEYS.iter() { + env.execute_transaction( + user.into(), + COST_VOTING_CONTRACT.clone(), + "withdraw-votes", + &symbols_from_values(vec![ + Value::UInt(i as u128), + Value::UInt(USTX_PER_HOLDER), + ]), + ) + .unwrap() + .0; + } + } + }); + + // Fast forward to proposal expiration + for _ in 0..2016 { + sim.execute_next_block(|_| {}); + } + + for _ in 0..1007 { + sim.execute_next_block(|_| {}); + } + + sim.execute_next_block(|env| { + for i in 0..MAX_CONFIRMATIONS_PER_BLOCK { + // Assert confirmation passes + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + COST_VOTING_CONTRACT.clone(), + "confirm-miners", + &symbols_from_values(vec![Value::UInt(i as u128)]) + ) + .unwrap() + .0, + Value::okay_true(), + ); + } + + // Assert next confirmation fails + assert_eq!( + env.execute_transaction( + (&USER_KEYS[0]).into(), + COST_VOTING_CONTRACT.clone(), + "confirm-miners", + &symbols_from_values(vec![Value::UInt(MAX_CONFIRMATIONS_PER_BLOCK)]) + ) + .unwrap() + .0, + Value::error(Value::Int(17)).unwrap() + ); + }); +} diff --git a/src/chainstate/stacks/boot/cost-voting.clar b/src/chainstate/stacks/boot/cost-voting.clar index 9b9353b550..a9ccc846cd 100644 --- a/src/chainstate/stacks/boot/cost-voting.clar +++ b/src/chainstate/stacks/boot/cost-voting.clar @@ -17,6 +17,7 @@ (define-constant ERR_PROPOSAL_VETOED 14) (define-constant ERR_PROPOSAL_CONFIRMED 15) (define-constant ERR_FETCHING_BLOCK_INFO 16) +(define-constant ERR_TOO_MANY_CONFIRMED 17) (define-constant ERR_UNREACHABLE 255) (define-constant VOTE_LENGTH u2016) @@ -24,6 +25,8 @@ (define-constant REQUIRED_PERCENT_STX_VOTE u20) (define-constant REQUIRED_VETOES u500) +(define-constant MAX_CONFIRMED_PER_BLOCK u10) + ;; cost vote token (define-fungible-token cost-vote-token) @@ -61,6 +64,11 @@ } ) +;; limit the number of miner confirmed-proposals +;; that can be introduced per block +;; track the # of proposals confirmed at a given block-height +(define-map confirmed-count-at-block uint uint) + (define-map proposal-confirmed-id { proposal-id: uint } { confirmed-id: uint } @@ -230,17 +238,18 @@ ;; Confirm proposal hasn't been vetoed (define-public (confirm-miners (proposal-id uint)) - (let ( - (vetos (default-to u0 (get vetos (map-get? proposal-vetos { proposal-id: proposal-id })))) - (vote-confirmed-proposal (unwrap! (map-get? vote-confirmed-proposals + (let ((vetos (default-to u0 (get vetos (map-get? proposal-vetos { proposal-id: proposal-id })))) + (vote-confirmed-proposal (unwrap! (map-get? vote-confirmed-proposals { proposal-id: proposal-id }) (err ERR_NO_SUCH_PROPOSAL))) - (proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) + (proposal (unwrap! (map-get? proposals { proposal-id: proposal-id }) (err ERR_NO_SUCH_PROPOSAL))) - (confirmed-count (var-get confirmed-proposal-count)) - ) - (let ( - (expiration-block-height (get expiration-block-height vote-confirmed-proposal)) - ) + (confirmed-count (var-get confirmed-proposal-count)) + (expiration-block-height (get expiration-block-height vote-confirmed-proposal)) + (confirmed-this-block (default-to u0 (map-get? confirmed-count-at-block block-height)))) + + ;; have we already confirmed too many proposals in this block + (asserts! (< confirmed-this-block MAX_CONFIRMED_PER_BLOCK) (err ERR_TOO_MANY_CONFIRMED)) + (map-set confirmed-count-at-block block-height (+ u1 confirmed-this-block)) ;; miner confirmation will fail if invoked before the expiration (asserts! (>= burn-block-height expiration-block-height) (err ERR_VETO_PERIOD_NOT_OVER)) @@ -260,4 +269,3 @@ (map-insert proposal-confirmed-id { proposal-id: proposal-id } { confirmed-id: confirmed-count }) (var-set confirmed-proposal-count (+ confirmed-count u1)) (ok true))) -) \ No newline at end of file diff --git a/src/chainstate/stacks/boot/costs.clar b/src/chainstate/stacks/boot/costs.clar index 03ee3eea33..eed7fb4b59 100644 --- a/src/chainstate/stacks/boot/costs.clar +++ b/src/chainstate/stacks/boot/costs.clar @@ -507,6 +507,33 @@ read_length: u1 }) +(define-read-only (cost_ft_get_supply (n uint)) + { + runtime: u1, + write_length: u0, + write_count: u0, + read_count: u1, + read_length: u1 + }) + +(define-read-only (cost_ft_burn (n uint)) + { + runtime: u1, + write_length: u1, + write_count: u2, + read_count: u2, + read_length: u1 + }) + +(define-read-only (cost_nft_burn (n uint)) + { + runtime: (linear n u1 u1), + write_length: u1, + write_count: u1, + read_count: u1, + read_length: u1 + }) + (define-read-only (poison_microblock (n uint)) { runtime: u1, diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 41774cbec3..06659bcca9 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -18,6 +18,7 @@ use chainstate::stacks::db::StacksChainState; use chainstate::stacks::Error; use chainstate::stacks::StacksAddress; use chainstate::stacks::StacksBlockHeader; +use vm::database::ClarityDatabase; use address::AddressHashMode; use burnchains::bitcoin::address::BitcoinAddress; @@ -26,9 +27,13 @@ use burnchains::{Address, PoxConstants}; use chainstate::burn::db::sortdb::SortitionDB; use core::{POX_MAXIMAL_SCALING, POX_THRESHOLD_STEPS_USTX}; +use vm::costs::{ + cost_functions::ClarityCostFunction, ClarityCostFunctionReference, CostStateSummary, +}; +use vm::representations::ClarityName; use vm::types::{ PrincipalData, QualifiedContractIdentifier, SequenceData, StandardPrincipalData, TupleData, - Value, + TypeSignature, Value, }; use chainstate::stacks::index::marf::MarfConnection; @@ -55,6 +60,7 @@ const BOOT_CODE_POX_TESTNET_CONSTS: &'static str = std::include_str!("pox-testne const BOOT_CODE_POX_MAINNET_CONSTS: &'static str = std::include_str!("pox-mainnet.clar"); const BOOT_CODE_LOCKUP: &'static str = std::include_str!("lockup.clar"); pub const BOOT_CODE_COSTS: &'static str = std::include_str!("costs.clar"); +pub const BOOT_CODE_COST_VOTING: &'static str = std::include_str!("cost-voting.clar"); const BOOT_CODE_BNS: &'static str = std::include_str!("bns.clar"); lazy_static! { @@ -64,19 +70,23 @@ lazy_static! { format!("{}\n{}", BOOT_CODE_POX_MAINNET_CONSTS, BOOT_CODE_POX_BODY); static ref BOOT_CODE_POX_TESTNET: String = format!("{}\n{}", BOOT_CODE_POX_TESTNET_CONSTS, BOOT_CODE_POX_BODY); - pub static ref STACKS_BOOT_CODE_MAINNET: [(&'static str, &'static str); 4] = [ + pub static ref STACKS_BOOT_CODE_MAINNET: [(&'static str, &'static str); 5] = [ ("pox", &BOOT_CODE_POX_MAINNET), ("lockup", BOOT_CODE_LOCKUP), + ("costs", BOOT_CODE_COSTS), + ("cost-voting", BOOT_CODE_COST_VOTING), ("bns", &BOOT_CODE_BNS), - ("costs", BOOT_CODE_COSTS) ]; - pub static ref STACKS_BOOT_CODE_TESTNET: [(&'static str, &'static str); 4] = [ + pub static ref STACKS_BOOT_CODE_TESTNET: [(&'static str, &'static str); 5] = [ ("pox", &BOOT_CODE_POX_TESTNET), ("lockup", BOOT_CODE_LOCKUP), + ("costs", BOOT_CODE_COSTS), + ("cost-voting", BOOT_CODE_COST_VOTING), ("bns", &BOOT_CODE_BNS), - ("costs", BOOT_CODE_COSTS) ]; pub static ref STACKS_BOOT_COST_CONTRACT: QualifiedContractIdentifier = boot_code_id("costs"); + pub static ref STACKS_BOOT_COST_VOTE_CONTRACT: QualifiedContractIdentifier = + boot_code_id("cost-voting"); } pub fn boot_code_addr() -> StacksAddress { @@ -156,12 +166,6 @@ impl StacksChainState { .map_err(Error::ClarityError) } - /// Determine which reward cycle this particular block lives in. - pub fn get_reward_cycle(&mut self, burnchain: &Burnchain, burn_block_height: u64) -> u128 { - ((burn_block_height - burnchain.first_block_height) - / burnchain.pox_constants.reward_cycle_length as u64) as u128 - } - /// Determine the minimum amount of STX per reward address required to stack in the _next_ /// reward cycle #[cfg(test)] @@ -237,10 +241,11 @@ impl StacksChainState { let slots_taken = u32::try_from(stacked_amt / threshold) .expect("CORRUPTION: Stacker claimed > u32::max() reward slots"); info!( - "Slots taken by {} = {}, on stacked_amt = {}", - &address, slots_taken, stacked_amt + "Slots taken by {} = {}, on stacked_amt = {}, threshold = {}", + &address, slots_taken, stacked_amt, threshold ); for _i in 0..slots_taken { + test_debug!("Add to PoX reward set: {:?}", &address); reward_set.push(address.clone()); } } @@ -288,8 +293,11 @@ impl StacksChainState { current_burn_height: u64, block_id: &StacksBlockId, ) -> Result, Error> { - let reward_cycle = self.get_reward_cycle(burnchain, current_burn_height); - if !self.is_pox_active(sortdb, block_id, reward_cycle)? { + let reward_cycle = burnchain + .block_height_to_reward_cycle(current_burn_height) + .ok_or(Error::PoxNoRewardCycle)?; + + if !self.is_pox_active(sortdb, block_id, reward_cycle as u128)? { debug!( "PoX was voted disabled in block {} (reward cycle {})", block_id, reward_cycle @@ -349,6 +357,11 @@ impl StacksChainState { false => hash_mode.to_version_testnet(), }; + test_debug!( + "PoX reward address (for {} ustx): {:?}", + total_ustx, + &StacksAddress::new(version, hash) + ); ret.push((StacksAddress::new(version, hash), total_ustx)); } @@ -363,6 +376,7 @@ mod contract_tests; pub mod test { use chainstate::burn::db::sortdb::*; use chainstate::burn::db::*; + use chainstate::burn::operations::BlockstackOperationType; use chainstate::burn::*; use chainstate::stacks::db::test::*; use chainstate::stacks::db::*; @@ -384,7 +398,7 @@ pub mod test { use vm::contracts::Contract; use vm::types::*; - use std::collections::HashMap; + use std::collections::{HashMap, HashSet}; use std::convert::From; use std::fs; @@ -419,7 +433,7 @@ pub mod test { #[test] fn get_reward_threshold_units() { - let test_pox_constants = PoxConstants::new(500, 1, 1, 1, 5, 5000, 10000); + let test_pox_constants = PoxConstants::new(501, 1, 1, 1, 5, 5000, 10000); // when the liquid amount = the threshold step, // the threshold should always be the step size. let liquid = POX_THRESHOLD_STEPS_USTX; @@ -558,7 +572,7 @@ pub mod test { let balances: Vec<(PrincipalData, u64)> = addrs .clone() .into_iter() - .map(|addr| (addr.into(), 1024 * 1000000)) + .map(|addr| (addr.into(), (1024 * POX_THRESHOLD_STEPS_USTX) as u64)) .collect(); peer_config.initial_balances = balances; @@ -747,7 +761,7 @@ pub mod test { fn make_tx( key: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, payload: TransactionPayload, ) -> StacksTransaction { let auth = TransactionAuth::from_p2pkh(key).unwrap(); @@ -756,7 +770,7 @@ pub mod test { tx.chain_id = 0x80000000; tx.auth.set_origin_nonce(nonce); tx.set_post_condition_mode(TransactionPostConditionMode::Allow); - tx.set_fee_rate(fee_rate); + tx.set_tx_fee(tx_fee); let mut tx_signer = StacksTransactionSigner::new(&tx); tx_signer.sign_origin(key).unwrap(); @@ -845,23 +859,23 @@ pub mod test { fn make_bare_contract( key: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, name: &str, code: &str, ) -> StacksTransaction { let payload = TransactionPayload::new_smart_contract(name, code).unwrap(); - make_tx(key, nonce, fee_rate, payload) + make_tx(key, nonce, tx_fee, payload) } fn make_token_transfer( key: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, dest: PrincipalData, amount: u64, ) -> StacksTransaction { let payload = TransactionPayload::TokenTransfer(dest, amount, TokenTransferMemo([0u8; 34])); - make_tx(key, nonce, fee_rate, payload) + make_tx(key, nonce, tx_fee, payload) } fn make_pox_lockup_contract( @@ -999,11 +1013,12 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, keys) = instantiate_pox_peer(&burnchain, "test-liquid-ustx", 6000); let num_blocks = 10; - let mut expected_liquid_ustx = 1024 * 1000000 * (keys.len() as u128); + let mut expected_liquid_ustx = 1024 * POX_THRESHOLD_STEPS_USTX * (keys.len() as u128); let mut missed_initial_blocks = 0; for tenure_id in 0..num_blocks { @@ -1183,6 +1198,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 3; burnchain.pox_constants.prepare_length = 1; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-hook-special-contract-call", 6007); @@ -1208,18 +1224,18 @@ pub mod test { ]; if tenure_id == 1 { - let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, tip.block_height); + let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, tip.block_height); block_txs.push(alice_lockup_1); } if tenure_id == 2 { let alice_test_tx = make_bare_contract(&alice, 1, 0, "nested-stacker", &format!( "(define-public (nested-stack-stx) - (contract-call? '{}.pox stack-stx u512000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) burn-block-height u1))", STACKS_BOOT_CODE_CONTRACT_ADDRESS_STR)); + (contract-call? '{}.pox stack-stx u5120000000000 (tuple (version 0x00) (hashbytes 0xffffffffffffffffffffffffffffffffffffffff)) burn-block-height u1))", STACKS_BOOT_CODE_CONTRACT_ADDRESS_STR)); block_txs.push(alice_test_tx); } if tenure_id == 8 { - // alice locks 512_000_000 STX through her contract + // alice locks 512 * 10_000 * POX_THRESHOLD_STEPS_USTX uSTX through her contract let cc_payload = TransactionPayload::new_contract_call(key_to_stacks_addr(&alice), "nested-stacker", "nested-stack-stx", @@ -1258,34 +1274,34 @@ pub mod test { // before/after alice's tokens lock if tenure_id == 0 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id == 1 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); } // before/after alice's tokens unlock else if tenure_id == 4 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id == 5 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } // before/after contract lockup else if tenure_id == 7 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id == 8 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); } // before/after contract-locked tokens unlock else if tenure_id == 13 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id == 14 { let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } } } @@ -1295,11 +1311,12 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-liquid-ustx", 6026); let num_blocks = 10; - let mut expected_liquid_ustx = 1024 * 1000000 * (keys.len() as u128); + let mut expected_liquid_ustx = 1024 * POX_THRESHOLD_STEPS_USTX * (keys.len() as u128); let mut missed_initial_blocks = 0; let alice = keys.pop().unwrap(); @@ -1396,6 +1413,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-single-tx-sender", 6002); @@ -1433,7 +1451,7 @@ pub mod test { let alice_lockup = make_pox_lockup( &alice, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, @@ -1474,10 +1492,13 @@ pub mod test { if tenure_id < 1 { // Alice has not locked up STX let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_account.stx_balance.amount_unlocked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_unlocked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!(alice_account.stx_balance.amount_locked, 0); assert_eq!(alice_account.stx_balance.unlock_height, 0); } @@ -1502,12 +1523,12 @@ pub mod test { // record the first reward cycle when Alice's tokens get stacked let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - alice_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + alice_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nalice reward cycle: {}\ncur reward cycle: {}\n", @@ -1517,9 +1538,9 @@ pub mod test { // Alice's address is locked as of the next reward cycle let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; // Alice has locked up STX no matter what let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); @@ -1550,11 +1571,11 @@ pub mod test { if tenure_id >= (MINER_REWARD_MATURITY + 1) as usize { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. - assert!(total_liquid_ustx > 4 * 1024 * 1000000); + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(min_ustx, total_liquid_ustx / 480); } else { // still at 25% or more locked - assert!(total_liquid_ustx <= 4 * 1024 * 1000000); + assert!(total_liquid_ustx <= 4 * 1024 * POX_THRESHOLD_STEPS_USTX); } let (amount_ustx, pox_addr, lock_period, first_reward_cycle) = @@ -1569,12 +1590,15 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[0].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[0].1, 1024 * 1000000); + assert_eq!(reward_addrs[0].1, 1024 * POX_THRESHOLD_STEPS_USTX); // Lock-up is consistent with stacker state let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_account.stx_balance.amount_unlocked, 0); - assert_eq!(alice_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (first_reward_cycle + lock_period) @@ -1589,11 +1613,267 @@ pub mod test { } } + #[test] + fn test_pox_lockup_single_tx_sender_100() { + let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); + burnchain.pox_constants.reward_cycle_length = 4; // 4 reward slots + burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; + assert_eq!(burnchain.pox_constants.reward_slots(), 4); + + let (mut peer, keys) = + instantiate_pox_peer(&burnchain, "test-pox-lockup-single-tx-sender-100", 6026); + + let num_blocks = 20; + + let mut lockup_reward_cycle = 0; + let mut prepared = false; + let mut rewarded = false; + + for tenure_id in 0..num_blocks { + let microblock_privkey = StacksPrivateKey::new(); + let microblock_pubkeyhash = + Hash160::from_node_public_key(&StacksPublicKey::from_private(µblock_privkey)); + let tip = + SortitionDB::get_canonical_burn_chain_tip(&peer.sortdb.as_ref().unwrap().conn()) + .unwrap(); + + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip.block_height) + .unwrap() as u128; + + let (burn_ops, stacks_block, microblocks) = peer.make_tenure( + |ref mut miner, + ref mut sortdb, + ref mut chainstate, + vrf_proof, + ref parent_opt, + ref parent_microblock_header_opt| { + let parent_tip = get_parent_tip(parent_opt, chainstate, sortdb); + let coinbase_tx = make_coinbase(miner, tenure_id); + + let mut block_txs = vec![coinbase_tx]; + + if tenure_id == 1 { + // all peers lock at the same time + for key in keys.iter() { + let lockup = make_pox_lockup( + key, + 0, + 1024 * POX_THRESHOLD_STEPS_USTX, + AddressHashMode::SerializeP2PKH, + key_to_stacks_addr(key).bytes, + 12, + tip.block_height, + ); + block_txs.push(lockup); + } + } + + let block_builder = StacksBlockBuilder::make_block_builder( + &parent_tip, + vrf_proof, + tip.total_burn, + microblock_pubkeyhash, + ) + .unwrap(); + let (anchored_block, _size, _cost) = + StacksBlockBuilder::make_anchored_block_from_txs( + block_builder, + chainstate, + &sortdb.index_conn(), + block_txs, + ) + .unwrap(); + (anchored_block, vec![]) + }, + ); + + let (burn_height, _, consensus_hash) = peer.next_burnchain_block(burn_ops.clone()); + peer.process_stacks_epoch_at_tip(&stacks_block, µblocks); + + if burnchain.is_in_prepare_phase(burn_height) { + // make sure we burn! + for op in burn_ops.iter() { + if let BlockstackOperationType::LeaderBlockCommit(ref opdata) = &op { + eprintln!("prepare phase {}: {:?}", burn_height, opdata); + assert!(opdata.all_outputs_burn()); + assert!(opdata.burn_fee > 0); + + if tenure_id > 1 && cur_reward_cycle > lockup_reward_cycle { + prepared = true; + } + } + } + } else { + // no burns -- 100% commitment + for op in burn_ops.iter() { + if let BlockstackOperationType::LeaderBlockCommit(ref opdata) = &op { + eprintln!("reward phase {}: {:?}", burn_height, opdata); + if tenure_id > 1 && cur_reward_cycle > lockup_reward_cycle { + assert!(!opdata.all_outputs_burn()); + rewarded = true; + } else { + // lockup hasn't happened yet + assert!(opdata.all_outputs_burn()); + } + + assert!(opdata.burn_fee > 0); + } + } + } + + let total_liquid_ustx = get_liquid_ustx(&mut peer); + let tip_index_block = StacksBlockHeader::make_index_block_hash( + &consensus_hash, + &stacks_block.block_hash(), + ); + + if tenure_id <= 1 { + if tenure_id < 1 { + // No locks have taken place + for key in keys.iter() { + // has not locked up STX + let balance = get_balance(&mut peer, &key_to_stacks_addr(&key).into()); + assert_eq!(balance, 1024 * POX_THRESHOLD_STEPS_USTX); + + let account = get_account(&mut peer, &key_to_stacks_addr(&key).into()); + assert_eq!( + account.stx_balance.amount_unlocked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); + assert_eq!(account.stx_balance.amount_locked, 0); + assert_eq!(account.stx_balance.unlock_height, 0); + } + } + let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { + chainstate.get_stacking_minimum(sortdb, &tip_index_block) + }) + .unwrap(); + assert_eq!(min_ustx, total_liquid_ustx / 480); + + // no reward addresses + let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { + get_reward_addresses_with_par_tip( + chainstate, + &burnchain, + sortdb, + &tip_index_block, + ) + }) + .unwrap(); + assert_eq!(reward_addrs.len(), 0); + + // record the first reward cycle when tokens get stacked + let tip_burn_block_height = + get_par_burn_block_height(peer.chainstate(), &tip_index_block); + lockup_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + + eprintln!( + "\nlockup reward cycle: {}\ncur reward cycle: {}\n", + lockup_reward_cycle, cur_reward_cycle + ); + } else { + // all addresses are locked as of the next reward cycle + let tip_burn_block_height = + get_par_burn_block_height(peer.chainstate(), &tip_index_block); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + + // all keys locked up STX no matter what + for key in keys.iter() { + let balance = get_balance(&mut peer, &key_to_stacks_addr(key).into()); + assert_eq!(balance, 0); + } + + let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { + chainstate.get_stacking_minimum(sortdb, &tip_index_block) + }) + .unwrap(); + let reward_addrs = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { + get_reward_addresses_with_par_tip( + chainstate, + &burnchain, + sortdb, + &tip_index_block, + ) + }) + .unwrap(); + let total_stacked = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { + chainstate.get_total_ustx_stacked(sortdb, &tip_index_block, cur_reward_cycle) + }) + .unwrap(); + + eprintln!("\ntenure: {}\nreward cycle: {}\nmin-uSTX: {}\naddrs: {:?}\ntotal_liquid_ustx: {}\ntotal-stacked: {}\n", tenure_id, cur_reward_cycle, min_ustx, &reward_addrs, total_liquid_ustx, total_stacked); + + if cur_reward_cycle >= lockup_reward_cycle { + // this will grow as more miner rewards are unlocked, so be wary + if tenure_id >= (MINER_REWARD_MATURITY + 1) as usize { + // miner rewards increased liquid supply, so less than 25% is locked. + // minimum participation decreases. + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); + assert_eq!(min_ustx, total_liquid_ustx / 480); + } else { + // still at 25% or more locked + assert!(total_liquid_ustx <= 4 * 1024 * POX_THRESHOLD_STEPS_USTX); + } + + assert_eq!(reward_addrs.len(), 4); + let mut all_addrbytes = HashSet::new(); + for key in keys.iter() { + all_addrbytes.insert(key_to_stacks_addr(&key).bytes); + } + + for key in keys.iter() { + let (amount_ustx, pox_addr, lock_period, first_reward_cycle) = + get_stacker_info(&mut peer, &key_to_stacks_addr(&key).into()).unwrap(); + eprintln!("\n{}: {} uSTX stacked for {} cycle(s); addr is {:?}; first reward cycle is {}\n", key.to_hex(), amount_ustx, lock_period, &pox_addr, first_reward_cycle); + + assert_eq!( + (reward_addrs[0].0).version, + AddressHashMode::SerializeP2PKH.to_version_testnet() + ); + assert!(all_addrbytes.contains(&key_to_stacks_addr(&key).bytes)); + all_addrbytes.remove(&key_to_stacks_addr(&key).bytes); + assert_eq!(reward_addrs[0].1, 1024 * POX_THRESHOLD_STEPS_USTX); + + // Lock-up is consistent with stacker state + let account = get_account(&mut peer, &key_to_stacks_addr(&key).into()); + assert_eq!(account.stx_balance.amount_unlocked, 0); + assert_eq!( + account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); + assert_eq!( + account.stx_balance.unlock_height as u128, + (first_reward_cycle + lock_period) + * (burnchain.pox_constants.reward_cycle_length as u128) + + (burnchain.first_block_height as u128) + ); + } + + assert_eq!(all_addrbytes.len(), 0); + } else { + // no reward addresses + assert_eq!(reward_addrs.len(), 0); + } + } + } + assert!(prepared && rewarded); + } + #[test] fn test_pox_lockup_contract() { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-contract", 6018); @@ -1636,7 +1916,7 @@ pub mod test { 0, &key_to_stacks_addr(&bob), "do-lockup", - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, @@ -1676,7 +1956,7 @@ pub mod test { if tenure_id < 1 { // Alice has not locked up STX let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) @@ -1699,12 +1979,12 @@ pub mod test { // record the first reward cycle when Alice's tokens get stacked let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - alice_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + alice_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nalice reward cycle: {}\ncur reward cycle: {}\n", @@ -1713,9 +1993,9 @@ pub mod test { } else { let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; // Alice's tokens got sent to the contract, so her balance is 0 let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); @@ -1749,11 +2029,11 @@ pub mod test { // height at which earliest miner rewards mature. // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. - assert!(total_liquid_ustx > 4 * 1024 * 1000000); + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(min_ustx, total_liquid_ustx / 480); } else { // still at 25% or more locked - assert!(total_liquid_ustx <= 4 * 1024 * 1000000); + assert!(total_liquid_ustx <= 4 * 1024 * POX_THRESHOLD_STEPS_USTX); } // Alice is _not_ a stacker -- Bob's contract is! @@ -1778,7 +2058,7 @@ pub mod test { // should be consistent with the API call assert_eq!(lock_period, 1); assert_eq!(first_reward_cycle, alice_reward_cycle); - assert_eq!(amount_ustx, 1024 * 1000000); + assert_eq!(amount_ustx, 1024 * POX_THRESHOLD_STEPS_USTX); // one reward address, and it's Alice's // either way, there's a single reward address @@ -1788,7 +2068,7 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[0].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[0].1, 1024 * 1000000); + assert_eq!(reward_addrs[0].1, 1024 * POX_THRESHOLD_STEPS_USTX); // contract's address's tokens are locked let contract_balance = get_balance( @@ -1803,7 +2083,10 @@ pub mod test { &make_contract_id(&key_to_stacks_addr(&bob), "do-lockup").into(), ); assert_eq!(contract_account.stx_balance.amount_unlocked, 0); - assert_eq!(contract_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + contract_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( contract_account.stx_balance.unlock_height as u128, (first_reward_cycle + lock_period) @@ -1816,7 +2099,7 @@ pub mod test { &mut peer, &make_contract_id(&key_to_stacks_addr(&bob), "do-lockup").into(), ); - assert_eq!(contract_balance, 1024 * 1000000); + assert_eq!(contract_balance, 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(reward_addrs.len(), 0); @@ -1826,7 +2109,10 @@ pub mod test { &make_contract_id(&key_to_stacks_addr(&bob), "do-lockup").into(), ); assert_eq!(contract_account.stx_balance.amount_unlocked, 0); - assert_eq!(contract_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + contract_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( contract_account.stx_balance.unlock_height as u128, (alice_reward_cycle + 1) @@ -1847,6 +2133,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-multi-tx-sender", 6004); @@ -1884,7 +2171,7 @@ pub mod test { let alice_lockup = make_pox_lockup( &alice, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, @@ -1896,7 +2183,7 @@ pub mod test { let bob_lockup = make_pox_lockup( &bob, 0, - (4 * 1024 * 1000000) / 5, + (4 * 1024 * POX_THRESHOLD_STEPS_USTX) / 5, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&bob).bytes, 12, @@ -1937,11 +2224,11 @@ pub mod test { if tenure_id < 1 { // Alice has not locked up STX let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); // Bob has not locked up STX let bob_balance = get_balance(&mut peer, &key_to_stacks_addr(&bob).into()); - assert_eq!(bob_balance, 1024 * 1000000); + assert_eq!(bob_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -1965,12 +2252,12 @@ pub mod test { // record the first reward cycle when Alice's tokens get stacked let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - first_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + first_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nalice reward cycle: {}\ncur reward cycle: {}\n", @@ -1980,16 +2267,19 @@ pub mod test { // Alice's and Bob's addresses are locked as of the next reward cycle let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; // Alice and Bob have locked up STX no matter what let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_balance, 0); let bob_balance = get_balance(&mut peer, &key_to_stacks_addr(&bob).into()); - assert_eq!(bob_balance, 1024 * 1000000 - (4 * 1024 * 1000000) / 5); + assert_eq!( + bob_balance, + 1024 * POX_THRESHOLD_STEPS_USTX - (4 * 1024 * POX_THRESHOLD_STEPS_USTX) / 5 + ); let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { chainstate.get_stacking_minimum(sortdb, &tip_index_block) @@ -2015,10 +2305,10 @@ pub mod test { if tenure_id >= (MINER_REWARD_MATURITY + 1) as usize { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. - assert!(total_liquid_ustx > 4 * 1024 * 1000000); + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); } else { // still at 25% or more locked - assert!(total_liquid_ustx <= 4 * 1024 * 1000000); + assert!(total_liquid_ustx <= 4 * 1024 * POX_THRESHOLD_STEPS_USTX); } // well over 25% locked, so this is always true @@ -2032,14 +2322,14 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[1].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[1].1, 1024 * 1000000); + assert_eq!(reward_addrs[1].1, 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!( (reward_addrs[0].0).version, AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[0].0).bytes, key_to_stacks_addr(&bob).bytes); - assert_eq!(reward_addrs[0].1, (4 * 1024 * 1000000) / 5); + assert_eq!(reward_addrs[0].1, (4 * 1024 * POX_THRESHOLD_STEPS_USTX) / 5); } else { // no reward addresses assert_eq!(reward_addrs.len(), 0); @@ -2053,6 +2343,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-no-double-stacking", 6006); @@ -2084,11 +2375,11 @@ pub mod test { if tenure_id == 1 { // Alice locks up exactly 12.5% of the liquid STX supply, twice. // Only the first one succeeds. - let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); + let alice_lockup_1 = make_pox_lockup(&alice, 0, 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); block_txs.push(alice_lockup_1); // will be rejected - let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); + let alice_lockup_2 = make_pox_lockup(&alice, 1, 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); block_txs.push(alice_lockup_2); // let's make some allowances for contract-calls through smart contracts @@ -2140,7 +2431,7 @@ pub mod test { "(define-data-var test-run bool false) (define-data-var test-result int -1) (let ((result - (contract-call? '{}.pox stack-stx u1024000000000 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) burn-block-height u12))) + (contract-call? '{}.pox stack-stx u10240000000001 (tuple (version 0x00) (hashbytes 0xfefefefefefefefefefefefefefefefefefefefe)) burn-block-height u12))) (var-set test-result (match result ok_value -1 err_value err_value)) (var-set test-run true)) @@ -2166,15 +2457,15 @@ pub mod test { if tenure_id == 0 { // Alice has not locked up half of her STX let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id == 1 { // only half locked let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id > 1 { // only half locked, still let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); } if tenure_id <= 1 { @@ -2194,12 +2485,12 @@ pub mod test { let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - first_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + first_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nalice reward cycle: {}\ncur reward cycle: {}\n", @@ -2265,6 +2556,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-single-tx-sender-unlock", 6012); @@ -2302,7 +2594,7 @@ pub mod test { let alice_lockup = make_pox_lockup( &alice, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, @@ -2343,7 +2635,7 @@ pub mod test { if tenure_id < 1 { // Alice has not locked up STX let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -2367,12 +2659,12 @@ pub mod test { // record the first reward cycle when Alice's tokens get stacked let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - alice_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + alice_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nalice reward cycle: {}\ncur reward cycle: {}\n", @@ -2382,9 +2674,9 @@ pub mod test { // Alice's address is locked as of the next reward cycle let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); @@ -2413,7 +2705,7 @@ pub mod test { if tenure_id >= (MINER_REWARD_MATURITY + 1) as usize { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. - assert!(total_liquid_ustx > 4 * 1024 * 1000000); + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(min_ustx, total_liquid_ustx / 480); } @@ -2434,7 +2726,7 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[0].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[0].1, 1024 * 1000000); + assert_eq!(reward_addrs[0].1, 1024 * POX_THRESHOLD_STEPS_USTX); // All of Alice's tokens are locked assert_eq!(alice_balance, 0); @@ -2443,7 +2735,10 @@ pub mod test { let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_account.stx_balance.amount_unlocked, 0); - assert_eq!(alice_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (first_reward_cycle + lock_period) @@ -2452,7 +2747,7 @@ pub mod test { ); } else { // unlock should have happened - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); // alice shouldn't be a stacker let info = get_stacker_info(&mut peer, &key_to_stacks_addr(&alice).into()); @@ -2471,7 +2766,10 @@ pub mod test { let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_account.stx_balance.amount_unlocked, 0); - assert_eq!(alice_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (alice_reward_cycle + 1) @@ -2492,6 +2790,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-unlock-relock", 6014); @@ -2537,7 +2836,7 @@ pub mod test { let alice_lockup = make_pox_lockup( &alice, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, @@ -2554,7 +2853,7 @@ pub mod test { 0, &key_to_stacks_addr(&bob), "do-lockup", - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&charlie).bytes, 1, @@ -2566,7 +2865,7 @@ pub mod test { 1, &key_to_stacks_addr(&bob), "do-lockup", - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, ); block_txs.push(charlie_withdraw); } else if tenure_id == 11 { @@ -2574,7 +2873,7 @@ pub mod test { let alice_lockup = make_pox_lockup( &alice, 1, - 512 * 1000000, + 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, @@ -2588,7 +2887,7 @@ pub mod test { 2, &key_to_stacks_addr(&bob), "do-lockup", - 512 * 1000000, + 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&charlie).bytes, 1, @@ -2625,9 +2924,9 @@ pub mod test { ); let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); let charlie_contract_balance = get_balance( @@ -2652,7 +2951,7 @@ pub mod test { if tenure_id <= 1 { if tenure_id < 1 { // Alice has not locked up STX - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); // Charlie's contract has not locked up STX assert_eq!(charlie_contract_balance, 0); @@ -2668,9 +2967,12 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // record the first reward cycle when Alice's tokens get stacked - first_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + first_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nfirst reward cycle: {}\ncur reward cycle: {}\n", first_reward_cycle, cur_reward_cycle @@ -2680,13 +2982,13 @@ pub mod test { test_before_first_reward_cycle = true; } else if tenure_id == 10 { // Alice has unlocked - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); // Charlie's contract was unlocked and wiped assert_eq!(charlie_contract_balance, 0); // Charlie's balance - assert_eq!(charlie_balance, 1024 * 1000000); + assert_eq!(charlie_balance, 1024 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id == 11 { // should have just re-locked // stacking minimum should be minimum, since we haven't @@ -2701,9 +3003,9 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // record the first reward cycle when Alice's tokens get stacked - second_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + second_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; assert!(second_reward_cycle > cur_reward_cycle); eprintln!( "\nsecond reward cycle: {}\ncur reward cycle: {}\n", @@ -2717,11 +3019,11 @@ pub mod test { if tenure_id >= (MINER_REWARD_MATURITY + 1) as usize { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. - assert!(total_liquid_ustx > 4 * 1024 * 1000000); + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(min_ustx, total_liquid_ustx / 480); } else if tenure_id >= 1 && cur_reward_cycle < first_reward_cycle { // still at 25% or more locked - assert!(total_liquid_ustx <= 4 * 1024 * 1000000); + assert!(total_liquid_ustx <= 4 * 1024 * POX_THRESHOLD_STEPS_USTX); } else if tenure_id < 1 { // nothing locked yet assert_eq!(min_ustx, total_liquid_ustx / 480); @@ -2758,7 +3060,7 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[1].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[1].1, 1024 * 1000000); + assert_eq!(reward_addrs[1].1, 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!( (reward_addrs[0].0).version, @@ -2768,7 +3070,7 @@ pub mod test { (reward_addrs[0].0).bytes, key_to_stacks_addr(&charlie).bytes ); - assert_eq!(reward_addrs[0].1, 1024 * 1000000); + assert_eq!(reward_addrs[0].1, 1024 * POX_THRESHOLD_STEPS_USTX); // All of Alice's and Charlie's tokens are locked assert_eq!(alice_balance, 0); @@ -2777,7 +3079,10 @@ pub mod test { // Lock-up is consistent with stacker state let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_account.stx_balance.amount_unlocked, 0); - assert_eq!(alice_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (first_reward_cycle + lock_period) @@ -2791,7 +3096,10 @@ pub mod test { &make_contract_id(&key_to_stacks_addr(&bob), "do-lockup").into(), ); assert_eq!(charlie_account.stx_balance.amount_unlocked, 0); - assert_eq!(charlie_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + charlie_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( charlie_account.stx_balance.unlock_height as u128, (first_reward_cycle + lock_period) @@ -2803,7 +3111,7 @@ pub mod test { // After Alice's first reward cycle, but before her second. // unlock should have happened - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(charlie_contract_balance, 0); // alice shouldn't be a stacker @@ -2825,7 +3133,10 @@ pub mod test { // Unlock is lazy let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_account.stx_balance.amount_unlocked, 0); - assert_eq!(alice_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (first_reward_cycle + 1) @@ -2874,7 +3185,7 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[1].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[1].1, 512 * 1000000); + assert_eq!(reward_addrs[1].1, 512 * POX_THRESHOLD_STEPS_USTX); assert_eq!( (reward_addrs[0].0).version, @@ -2884,17 +3195,23 @@ pub mod test { (reward_addrs[0].0).bytes, key_to_stacks_addr(&charlie).bytes ); - assert_eq!(reward_addrs[0].1, 512 * 1000000); + assert_eq!(reward_addrs[0].1, 512 * POX_THRESHOLD_STEPS_USTX); // Half of Alice's tokens are locked - assert_eq!(alice_balance, 512 * 1000000); + assert_eq!(alice_balance, 512 * POX_THRESHOLD_STEPS_USTX); assert_eq!(charlie_contract_balance, 0); - assert_eq!(charlie_balance, 512 * 1000000); + assert_eq!(charlie_balance, 512 * POX_THRESHOLD_STEPS_USTX); // Lock-up is consistent with stacker state let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_account.stx_balance.amount_unlocked, 512 * 1000000); - assert_eq!(alice_account.stx_balance.amount_locked, 512 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_unlocked, + 512 * POX_THRESHOLD_STEPS_USTX + ); + assert_eq!( + alice_account.stx_balance.amount_locked, + 512 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (second_reward_cycle + lock_period) @@ -2908,7 +3225,10 @@ pub mod test { &make_contract_id(&key_to_stacks_addr(&bob), "do-lockup").into(), ); assert_eq!(charlie_account.stx_balance.amount_unlocked, 0); - assert_eq!(charlie_account.stx_balance.amount_locked, 512 * 1000000); + assert_eq!( + charlie_account.stx_balance.amount_locked, + 512 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( charlie_account.stx_balance.unlock_height as u128, (second_reward_cycle + lock_period) @@ -2920,9 +3240,9 @@ pub mod test { // After Alice's second reward cycle // unlock should have happened - assert_eq!(alice_balance, 1024 * 1000000); - assert_eq!(charlie_contract_balance, 512 * 1000000); - assert_eq!(charlie_balance, 512 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); + assert_eq!(charlie_contract_balance, 512 * POX_THRESHOLD_STEPS_USTX); + assert_eq!(charlie_balance, 512 * POX_THRESHOLD_STEPS_USTX); // alice and charlie shouldn't be stackers assert!( @@ -2942,8 +3262,14 @@ pub mod test { // Unlock is lazy let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_account.stx_balance.amount_unlocked, 512 * 1000000); - assert_eq!(alice_account.stx_balance.amount_locked, 512 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_unlocked, + 512 * POX_THRESHOLD_STEPS_USTX + ); + assert_eq!( + alice_account.stx_balance.amount_locked, + 512 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (second_reward_cycle + 1) @@ -2957,7 +3283,10 @@ pub mod test { &make_contract_id(&key_to_stacks_addr(&bob), "do-lockup").into(), ); assert_eq!(charlie_account.stx_balance.amount_unlocked, 0); - assert_eq!(charlie_account.stx_balance.amount_locked, 512 * 1000000); + assert_eq!( + charlie_account.stx_balance.amount_locked, + 512 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( charlie_account.stx_balance.unlock_height as u128, (second_reward_cycle + 1) @@ -2980,6 +3309,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; let (mut peer, mut keys) = instantiate_pox_peer(&burnchain, "test-pox-lockup-unlock-on-spend", 6016); @@ -3022,7 +3352,7 @@ pub mod test { let alice_lockup = make_pox_lockup( &alice, 0, - 512 * 1000000, + 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 1, @@ -3033,7 +3363,7 @@ pub mod test { let bob_lockup = make_pox_lockup( &bob, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&bob).bytes, 1, @@ -3044,7 +3374,7 @@ pub mod test { let charlie_lockup = make_pox_lockup( &charlie, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&charlie).bytes, 1, @@ -3055,7 +3385,7 @@ pub mod test { let danielle_lockup = make_pox_lockup( &danielle, 0, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&danielle).bytes, 1, @@ -3071,7 +3401,7 @@ pub mod test { 1, &key_to_stacks_addr(&bob), "do-lockup", - 512 * 1000000, + 512 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2SH, key_to_stacks_addr(&alice).bytes, 1, @@ -3170,9 +3500,9 @@ pub mod test { let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; let stacker_addrs: Vec = vec![ key_to_stacks_addr(&alice).into(), @@ -3211,37 +3541,37 @@ pub mod test { .collect(); let balances_before_stacking: Vec = vec![ - 1024 * 1000000, - 1024 * 1000000, - 1024 * 1000000, - 1024 * 1000000, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, 0, ]; let balances_during_stacking: Vec = vec![0, 0, 0, 0, 0]; let balances_stacked: Vec = vec![ - 512 * 1000000, - 1024 * 1000000, - 1024 * 1000000, - 1024 * 1000000, - 512 * 1000000, + 512 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 512 * POX_THRESHOLD_STEPS_USTX, ]; let balances_after_stacking: Vec = vec![ - 512 * 1000000, - 1024 * 1000000, - 1024 * 1000000, - 1024 * 1000000, - 512 * 1000000, + 512 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 1024 * POX_THRESHOLD_STEPS_USTX, + 512 * POX_THRESHOLD_STEPS_USTX, ]; let balances_after_spending: Vec = vec![ - 512 * 1000000 + 2, - 1024 * 1000000 - 1, - 1024 * 1000000 - 1, - 1024 * 1000000 - 1, - 512 * 1000000 - 1, + 512 * POX_THRESHOLD_STEPS_USTX + 2, + 1024 * POX_THRESHOLD_STEPS_USTX - 1, + 1024 * POX_THRESHOLD_STEPS_USTX - 1, + 1024 * POX_THRESHOLD_STEPS_USTX - 1, + 512 * POX_THRESHOLD_STEPS_USTX - 1, ]; let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -3283,9 +3613,9 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // record the first reward cycle when Alice's tokens get stacked - reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "first reward cycle: {}\ncur reward cycle: {}\n", reward_cycle, cur_reward_cycle @@ -3426,6 +3756,7 @@ pub mod test { let mut burnchain = Burnchain::default_unittest(0, &BurnchainHeaderHash::zero()); burnchain.pox_constants.reward_cycle_length = 5; burnchain.pox_constants.prepare_length = 2; + burnchain.pox_constants.anchor_threshold = 1; // used to be set to 25, but test at 5 here, because the increased coinbase // and, to a lesser extent, the initial block bonus altered the relative fraction // owned by charlie. @@ -3459,7 +3790,7 @@ pub mod test { if tenure_id == 1 { // Alice locks up exactly 25% of the liquid STX supply, so this should succeed. - let alice_lockup = make_pox_lockup(&alice, 0, 1024 * 1000000, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); + let alice_lockup = make_pox_lockup(&alice, 0, 1024 * POX_THRESHOLD_STEPS_USTX, AddressHashMode::SerializeP2PKH, key_to_stacks_addr(&alice).bytes, 12, tip.block_height); block_txs.push(alice_lockup); // Bob rejects with exactly 25% of the liquid STX supply (shouldn't affect @@ -3534,9 +3865,9 @@ pub mod test { let tip_burn_block_height = get_par_burn_block_height(peer.chainstate(), &tip_index_block); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; let alice_balance = get_balance(&mut peer, &key_to_stacks_addr(&alice).into()); let min_ustx = with_sortdb(&mut peer, |ref mut chainstate, ref sortdb| { @@ -3562,10 +3893,13 @@ pub mod test { if tenure_id <= 1 { if tenure_id < 1 { // Alice has not locked up STX - assert_eq!(alice_balance, 1024 * 1000000); + assert_eq!(alice_balance, 1024 * POX_THRESHOLD_STEPS_USTX); let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); - assert_eq!(alice_account.stx_balance.amount_unlocked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_unlocked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!(alice_account.stx_balance.amount_locked, 0); assert_eq!(alice_account.stx_balance.unlock_height, 0); } @@ -3576,12 +3910,12 @@ pub mod test { assert_eq!(reward_addrs.len(), 0); // record the first reward cycle when Alice's tokens get stacked - alice_reward_cycle = 1 + peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); - let cur_reward_cycle = peer - .chainstate() - .get_reward_cycle(&burnchain, tip_burn_block_height); + alice_reward_cycle = 1 + burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; + let cur_reward_cycle = burnchain + .block_height_to_reward_cycle(tip_burn_block_height) + .unwrap() as u128; eprintln!( "\nalice reward cycle: {}\ncur reward cycle: {}\n", @@ -3629,11 +3963,11 @@ pub mod test { if tenure_id >= (MINER_REWARD_MATURITY + 1) as usize { // miner rewards increased liquid supply, so less than 25% is locked. // minimum participation decreases. - assert!(total_liquid_ustx > 4 * 1024 * 1000000); + assert!(total_liquid_ustx > 4 * 1024 * POX_THRESHOLD_STEPS_USTX); assert_eq!(min_ustx, total_liquid_ustx / 480); } else { // still at 25% or more locked - assert!(total_liquid_ustx <= 4 * 1024 * 1000000); + assert!(total_liquid_ustx <= 4 * 1024 * POX_THRESHOLD_STEPS_USTX); } let (amount_ustx, pox_addr, lock_period, first_reward_cycle) = @@ -3655,13 +3989,16 @@ pub mod test { AddressHashMode::SerializeP2PKH.to_version_testnet() ); assert_eq!((reward_addrs[0].0).bytes, key_to_stacks_addr(&alice).bytes); - assert_eq!(reward_addrs[0].1, 1024 * 1000000); + assert_eq!(reward_addrs[0].1, 1024 * POX_THRESHOLD_STEPS_USTX); } // Lock-up is consistent with stacker state let alice_account = get_account(&mut peer, &key_to_stacks_addr(&alice).into()); assert_eq!(alice_account.stx_balance.amount_unlocked, 0); - assert_eq!(alice_account.stx_balance.amount_locked, 1024 * 1000000); + assert_eq!( + alice_account.stx_balance.amount_locked, + 1024 * POX_THRESHOLD_STEPS_USTX + ); assert_eq!( alice_account.stx_balance.unlock_height as u128, (first_reward_cycle + lock_period) diff --git a/src/chainstate/stacks/boot/pox-mainnet.clar b/src/chainstate/stacks/boot/pox-mainnet.clar index f5e581db24..0d40e8c17a 100644 --- a/src/chainstate/stacks/boot/pox-mainnet.clar +++ b/src/chainstate/stacks/boot/pox-mainnet.clar @@ -4,10 +4,10 @@ (define-constant MAX_POX_REWARD_CYCLES u12) ;; Default length of the PoX registration window, in burnchain blocks. -(define-constant PREPARE_CYCLE_LENGTH u250) +(define-constant PREPARE_CYCLE_LENGTH u100) ;; Default length of the PoX reward cycle, in burnchain blocks. -(define-constant REWARD_CYCLE_LENGTH u2000) +(define-constant REWARD_CYCLE_LENGTH u2100) ;; Valid values for burnchain address versions. ;; These correspond to address hash modes in Stacks 2.0. diff --git a/src/chainstate/stacks/boot/pox-testnet.clar b/src/chainstate/stacks/boot/pox-testnet.clar index a9ccc34c7b..92eb2e6231 100644 --- a/src/chainstate/stacks/boot/pox-testnet.clar +++ b/src/chainstate/stacks/boot/pox-testnet.clar @@ -7,7 +7,7 @@ (define-constant PREPARE_CYCLE_LENGTH u30) ;; Default length of the PoX reward cycle, in burnchain blocks. -(define-constant REWARD_CYCLE_LENGTH u120) +(define-constant REWARD_CYCLE_LENGTH u150) ;; Valid values for burnchain address versions. ;; These correspond to address hash modes in Stacks 2.0. diff --git a/src/chainstate/stacks/db/accounts.rs b/src/chainstate/stacks/db/accounts.rs index c7db1d85cc..4fbb210757 100644 --- a/src/chainstate/stacks/db/accounts.rs +++ b/src/chainstate/stacks/db/accounts.rs @@ -57,7 +57,6 @@ impl FromRow for MinerPaymentSchedule { let burns_text: String = row.get("stx_burns"); let burnchain_commit_burn = u64::from_column(row, "burnchain_commit_burn")?; let burnchain_sortition_burn = u64::from_column(row, "burnchain_sortition_burn")?; - let fill_text: String = row.get("fill"); let miner: bool = row.get("miner"); let stacks_block_height = u64::from_column(row, "stacks_block_height")?; let vtxindex: u32 = row.get("vtxindex"); @@ -74,9 +73,6 @@ impl FromRow for MinerPaymentSchedule { let stx_burns = burns_text .parse::() .map_err(|_e| db_error::ParseError)?; - let fill = fill_text - .parse::() - .map_err(|_e| db_error::ParseError)?; let payment_data = MinerPaymentSchedule { address, @@ -90,7 +86,6 @@ impl FromRow for MinerPaymentSchedule { stx_burns, burnchain_commit_burn, burnchain_sortition_burn, - fill, miner, stacks_block_height, vtxindex, @@ -108,6 +103,40 @@ impl MinerReward { } } +impl MinerPaymentSchedule { + /// If this is a MinerPaymentSchedule for a miner who _confirmed_ a microblock stream, then + /// this calculates the percentage of that stream this miner is entitled to + pub fn streamed_tx_fees_confirmed(&self) -> u128 { + (self.tx_fees_streamed * 3) / 5 + } + + /// If this is a MinerPaymentSchedule for a miner who _produced_ a microblock stream, then + /// this calculates the percentage of that stream this miner is entitled to + pub fn streamed_tx_fees_produced(&self) -> u128 { + (self.tx_fees_streamed * 2) / 5 + } + + /// Empty miner payment schedule -- i.e. for the genesis block + pub fn genesis(mainnet: bool) -> MinerPaymentSchedule { + MinerPaymentSchedule { + address: StacksAddress::burn_address(mainnet), + block_hash: FIRST_STACKS_BLOCK_HASH.clone(), + consensus_hash: FIRST_BURNCHAIN_CONSENSUS_HASH.clone(), + parent_block_hash: FIRST_STACKS_BLOCK_HASH.clone(), + parent_consensus_hash: FIRST_BURNCHAIN_CONSENSUS_HASH.clone(), + coinbase: 0, + tx_fees_anchored: 0, + tx_fees_streamed: 0, + stx_burns: 0, + burnchain_commit_burn: 0, + burnchain_sortition_burn: 0, + miner: true, + stacks_block_height: 0, + vtxindex: 0, + } + } +} + impl StacksChainState { pub fn get_account( clarity_tx: &mut T, @@ -304,7 +333,6 @@ impl StacksChainState { &format!("{}", block_reward.stx_burns), &u64_to_sql(block_reward.burnchain_commit_burn)?, &u64_to_sql(block_reward.burnchain_sortition_burn)?, - &format!("{}", block_reward.fill), &u64_to_sql(block_reward.stacks_block_height)?, &true, &0i64, @@ -324,12 +352,11 @@ impl StacksChainState { stx_burns, burnchain_commit_burn, burnchain_sortition_burn, - fill, stacks_block_height, miner, vtxindex, index_block_hash) \ - VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16)", + VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)", args, ) .map_err(|e| Error::DBError(db_error::SqliteError(e)))?; @@ -349,7 +376,6 @@ impl StacksChainState { &"0".to_string(), &u64_to_sql(user_support.burn_amount)?, &u64_to_sql(block_reward.burnchain_sortition_burn)?, - &format!("{}", block_reward.fill), &u64_to_sql(block_reward.stacks_block_height)?, &false, &user_support.vtxindex, @@ -369,12 +395,11 @@ impl StacksChainState { stx_burns, burnchain_commit_burn, burnchain_sortition_burn, - fill, stacks_block_height, miner, vtxindex, index_block_hash) \ - VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16)", + VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15)", args, ) .map_err(|e| Error::DBError(db_error::SqliteError(e)))?; @@ -398,6 +423,19 @@ impl StacksChainState { Ok(principal_seq_opt.map(|(principal, seq)| (principal.into(), seq))) } + /// Get the scheduled miner rewards at a particular index hash + pub fn get_scheduled_block_rewards_at_block<'a>( + tx: &mut StacksDBTx<'a>, + index_block_hash: &StacksBlockId, + ) -> Result, Error> { + let qry = + "SELECT * FROM payments WHERE index_block_hash = ?1 ORDER BY vtxindex ASC".to_string(); + let args: &[&dyn ToSql] = &[index_block_hash]; + let rows = query_rows::(tx, &qry, args).map_err(Error::DBError)?; + test_debug!("{} rewards in {}", rows.len(), index_block_hash); + Ok(rows) + } + /// Get the scheduled miner rewards in a particular Stacks fork at a particular height. pub fn get_scheduled_block_rewards_in_fork_at_height<'a>( tx: &mut StacksDBTx<'a>, @@ -485,20 +523,17 @@ impl StacksChainState { /// /// If poison_reporter_opt is not None, then the returned MinerReward will reward the _poison reporter_, /// not the miner, for reporting the microblock stream fork. - /// - /// TODO: this is incomplete -- it does not calculate transaction fees. This is just stubbed - /// out for now -- it only grants miners and user burn supports their coinbases. - /// fn calculate_miner_reward( mainnet: bool, participant: &MinerPaymentSchedule, miner: &MinerPaymentSchedule, users: &Vec, + parent: &MinerPaymentSchedule, poison_reporter_opt: Option<&StacksAddress>, - ) -> MinerReward { + ) -> (MinerReward, MinerReward) { ////////////////////// coinbase reward total ///////////////////////////////// let (this_burn_total, other_burn_total) = { - if participant.address == miner.address { + if participant.miner { // we're calculating the miner's reward let mut total_user: u128 = 0; for user_support in users.iter() { @@ -547,50 +582,80 @@ impl StacksChainState { // process poison -- someone can steal a fraction of the total coinbase if they can present // evidence that the miner forked the microblock stream. The remainder of the coinbase is // destroyed if this happens. - let (recipient, coinbase_reward) = if let Some(reporter_address) = poison_reporter_opt { - if participant.address == miner.address { - // the poison-reporter, not the miner, gets a (fraction of the) reward - debug!( - "{:?} will recieve poison-microblock commission {}", - &reporter_address.to_string(), - StacksChainState::poison_microblock_commission(coinbase_reward) - ); + let (recipient, coinbase_reward, punished) = + if let Some(reporter_address) = poison_reporter_opt { + if participant.miner { + // the poison-reporter, not the miner, gets a (fraction of the) reward + debug!( + "{:?} will recieve poison-microblock commission {}", + &reporter_address.to_string(), + StacksChainState::poison_microblock_commission(coinbase_reward) + ); + ( + reporter_address.clone(), + StacksChainState::poison_microblock_commission(coinbase_reward), + true, + ) + } else { + // users that helped a miner that reported a poison-microblock get nothing + (StacksAddress::burn_address(mainnet), coinbase_reward, false) + } + } else { + // no poison microblock reported + (participant.address, coinbase_reward, false) + }; + + let (tx_fees_anchored, parent_tx_fees_streamed_produced, tx_fees_streamed_confirmed) = + if participant.miner { + // only award tx fees to the miner, and only if the miner was not punished. + // parent gets its produced tx fees regardless of punishment. ( - reporter_address.clone(), - StacksChainState::poison_microblock_commission(coinbase_reward), + if !punished { + participant.tx_fees_anchored + } else { + 0 + }, + parent.streamed_tx_fees_produced(), + if !punished { + participant.streamed_tx_fees_confirmed() + } else { + 0 + }, ) } else { - // users that helped a miner that reported a poison-microblock get nothing - (StacksAddress::burn_address(mainnet), coinbase_reward) - } - } else { - // no poison microblock reported - (participant.address, coinbase_reward) - }; + // users get no tx fees + (0, 0, 0) + }; - // TODO: missing transaction fee calculation - let tx_fees_anchored = 0; - let tx_fees_streamed_produced = 0; - let tx_fees_streamed_confirmed = 0; debug!( - "{}: {} coinbase, {} anchored fees, {} streamed fees, {} confirmed fees", + "{}: {} coinbase, {} anchored fees, {} streamed fees confirmed; {} has produced {} fees", &recipient.to_string(), coinbase_reward, tx_fees_anchored, - tx_fees_streamed_produced, - tx_fees_streamed_confirmed + tx_fees_streamed_confirmed, + &parent.address.to_string(), + parent_tx_fees_streamed_produced, ); - // ("bad miner! no coinbase!") + let parent_miner_reward = MinerReward { + address: parent.address.clone(), + coinbase: 0, + tx_fees_anchored: 0, + tx_fees_streamed_produced: parent_tx_fees_streamed_produced, + tx_fees_streamed_confirmed: 0, + vtxindex: parent.vtxindex, + }; + let miner_reward = MinerReward { address: recipient, coinbase: coinbase_reward, tx_fees_anchored: tx_fees_anchored, - tx_fees_streamed_produced: tx_fees_streamed_produced, + tx_fees_streamed_produced: 0, tx_fees_streamed_confirmed: tx_fees_streamed_confirmed, vtxindex: miner.vtxindex, }; - miner_reward + + (parent_miner_reward, miner_reward) } /// Find the latest miner reward to mature, assuming that there are mature rewards. @@ -599,7 +664,8 @@ impl StacksChainState { clarity_tx: &mut ClarityTx<'a>, tip: &StacksHeaderInfo, mut latest_matured_miners: Vec, - ) -> Result, MinerRewardInfo)>, Error> { + parent_miner: MinerPaymentSchedule, + ) -> Result, MinerReward, MinerRewardInfo)>, Error> { let mainnet = clarity_tx.config.mainnet; if tip.block_height <= MINER_REWARD_MATURITY { // no mature rewards exist @@ -639,28 +705,36 @@ impl StacksChainState { } // calculate miner reward - let miner_reward = StacksChainState::calculate_miner_reward( + let (parent_miner_reward, miner_reward) = StacksChainState::calculate_miner_reward( mainnet, &miner, &miner, &users, + &parent_miner, poison_recipient_opt.as_ref(), ); // calculate reward for each user-support-burn let mut user_rewards = vec![]; for user_reward in users.iter() { - let reward = StacksChainState::calculate_miner_reward( + let (parent_reward, reward) = StacksChainState::calculate_miner_reward( mainnet, user_reward, &miner, &users, + &parent_miner, poison_recipient_opt.as_ref(), ); + assert_eq!(parent_reward.total(), 0); user_rewards.push(reward); } - Ok(Some((miner_reward, user_rewards, reward_info))) + Ok(Some(( + miner_reward, + user_rewards, + parent_miner_reward, + reward_info, + ))) } } @@ -696,7 +770,6 @@ mod test { stx_burns: 0, burnchain_commit_burn: commit_burn, burnchain_sortition_burn: sortition_burn, - fill: 0xffffffffffffffff, miner: true, stacks_block_height: 0, vtxindex: 0, @@ -935,101 +1008,6 @@ mod test { }; } - /* - #[test] - fn find_mature_miner_rewards() { - let mut chainstate = instantiate_chainstate(false, 0x80000000, "find_mature_miner_rewards"); - let miner_1 = - StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()) - .unwrap(); - let user_1 = - StacksAddress::from_string(&"SP2837ZMC89J40K4YTS64B00M7065C6X46JX6ARG0".to_string()) - .unwrap(); - - let mut parent_tip = StacksHeaderInfo::regtest_genesis(0); - - let mut matured_miners = (make_dummy_miner_payment_schedule(&miner_1, 0, 0, 0, 0, 0), vec![]); - - for i in 0..(MINER_REWARD_MATURITY + 1) { - let mut miner_reward = make_dummy_miner_payment_schedule( - &miner_1, - 500, - (2 * i) as u128, - (2 * i) as u128, - 1000, - 1000, - ); - let user_reward = make_dummy_user_payment_schedule( - &user_1, - 500, - (2 * i) as u128, - (2 * i) as u128, - 100, - 100, - 1, - ); - let user_support = StagingUserBurnSupport::from_miner_payment_schedule(&user_reward); - - if i == 0 { - matured_miners = (miner_reward.clone(), vec![user_reward.clone()]); - } - - let mut user_supports = vec![user_support]; - - let next_tip = advance_tip( - &mut chainstate, - &parent_tip, - &mut miner_reward, - &mut user_supports, - ); - - if i < MINER_REWARD_MATURITY { - let mut tx = chainstate.chainstate_tx_begin().unwrap(); - let rewards_opt = - StacksChainState::find_mature_miner_rewards(&mut tx, &parent_tip, vec![]) - .unwrap(); - assert!(rewards_opt.is_none()); // not mature yet - } - test_debug!("next tip = {:?}", &next_tip); - parent_tip = next_tip; - } - - let mut tx = chainstate.chainstate_tx_begin().unwrap().0; - - test_debug!("parent tip = {:?}", &parent_tip); - let miner_reward = StacksChainState::calculate_miner_reward( - false, - &matured_miners.0, - &matured_miners.0, - &matured_miners.1, - None - ); - let mut user_rewards = vec![]; - for user_reward in matured_miners.1.iter() { - let rw = StacksChainState::calculate_miner_reward( - false, - user_reward, - &matured_miners.0, - &matured_miners.1, - None - ); - user_rewards.push(rw); - } - - let mut matured_rewards = vec![]; - matured_rewards.push(miner_reward.clone()); - matured_rewards.append(&mut user_rewards.clone()); - - let rewards_opt = - StacksChainState::find_mature_miner_rewards(&mut tx, &parent_tip, &matured_rewards).unwrap(); - assert!(rewards_opt.is_some()); - - let rewards = rewards_opt.unwrap(); - assert_eq!(rewards.0, miner_reward); - assert_eq!(rewards.1, user_rewards); - } - */ - #[test] fn miner_reward_one_miner_no_tx_fees_no_users() { let miner_1 = @@ -1037,11 +1015,12 @@ mod test { .unwrap(); let participant = make_dummy_miner_payment_schedule(&miner_1, 500, 0, 0, 1000, 1000); - let miner_reward = StacksChainState::calculate_miner_reward( + let (parent_reward, miner_reward) = StacksChainState::calculate_miner_reward( false, &participant, &participant, &vec![], + &MinerPaymentSchedule::genesis(true), None, ); @@ -1050,6 +1029,12 @@ mod test { assert_eq!(miner_reward.tx_fees_anchored, 0); assert_eq!(miner_reward.tx_fees_streamed_produced, 0); assert_eq!(miner_reward.tx_fees_streamed_confirmed, 0); + + // parent gets nothing -- no tx fees + assert_eq!(parent_reward.coinbase, 0); + assert_eq!(parent_reward.tx_fees_anchored, 0); + assert_eq!(parent_reward.tx_fees_streamed_produced, 0); + assert_eq!(parent_reward.tx_fees_streamed_confirmed, 0); } #[test] @@ -1064,18 +1049,20 @@ mod test { let miner = make_dummy_miner_payment_schedule(&miner_1, 500, 0, 0, 250, 1000); let user = make_dummy_user_payment_schedule(&user_1, 500, 0, 0, 750, 1000, 1); - let reward_miner_1 = StacksChainState::calculate_miner_reward( + let (parent_miner_1, reward_miner_1) = StacksChainState::calculate_miner_reward( false, &miner, &miner, &vec![user.clone()], + &MinerPaymentSchedule::genesis(true), None, ); - let reward_user_1 = StacksChainState::calculate_miner_reward( + let (parent_user_1, reward_user_1) = StacksChainState::calculate_miner_reward( false, &user, &miner, &vec![user.clone()], + &MinerPaymentSchedule::genesis(true), None, ); @@ -1085,260 +1072,50 @@ mod test { assert_eq!(reward_miner_1.tx_fees_streamed_produced, 0); assert_eq!(reward_miner_1.tx_fees_streamed_confirmed, 0); - // user should have received 3/4 the coinbase - assert_eq!(reward_user_1.coinbase, 375); - assert_eq!(reward_user_1.tx_fees_anchored, 0); - assert_eq!(reward_user_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_user_1.tx_fees_streamed_confirmed, 0); - } - - /* - // TODO: broken; needs to be rewritten once transaction fee processing is added - #[test] - fn miner_reward_one_miner_one_user_anchored_tx_fees_unfull() { - let mut sample = vec![]; - let miner_1 = StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()).unwrap(); - let user_1 = StacksAddress::from_string(&"SP2837ZMC89J40K4YTS64B00M7065C6X46JX6ARG0".to_string()).unwrap(); - let mut participant = make_dummy_miner_payment_schedule(&miner_1, 500, 100, 0, 250, 1000); - let mut user = make_dummy_user_payment_schedule(&user_1, 500, 0, 0, 750, 1000, 1); - - // blocks are NOT full - let fill_cutoff = (((MINER_FEE_MINIMUM_BLOCK_USAGE as u128) << 64) / 100u128) as u64; - participant.fill = fill_cutoff - 1; - user.fill = fill_cutoff - 1; - - sample.push((participant.clone(), vec![user.clone()])); - - for i in 0..9 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_1, 250, 100, 0, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); + assert_eq!(parent_miner_1.total(), 0); - next_participant.fill = fill_cutoff - 1; - next_user.fill = fill_cutoff - 1; - - sample.push((next_participant, vec![next_user])); - } - - let reward_miner_1 = StacksChainState::calculate_miner_reward(&participant, &sample); - let reward_user_1 = StacksChainState::calculate_miner_reward(&user, &sample); - - // miner should have received 1/4 the coinbase, and miner should have received only shared - assert_eq!(reward_miner_1.coinbase, 125); - assert_eq!(reward_miner_1.tx_fees_anchored, 1000); // same miner, so they get everything - assert_eq!(reward_miner_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_miner_1.tx_fees_streamed_confirmed, 0); - - // user should have received 3/4 the coinbase, but no tx fees - assert_eq!(reward_user_1.coinbase, 375); - assert_eq!(reward_user_1.tx_fees_anchored, 0); - assert_eq!(reward_user_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_user_1.tx_fees_streamed_confirmed, 0); - } - - // TODO: broken; needs to be rewritten once transaction fee processing is added - #[test] - fn miner_reward_two_miners_one_user_anchored_tx_fees_unfull() { - let mut sample = vec![]; - let miner_1 = StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()).unwrap(); - let miner_2 = StacksAddress::from_string(&"SP8WWTGMNCCSB88QF4VYWN69PAMQRMF34FCT498G".to_string()).unwrap(); - let user_1 = StacksAddress::from_string(&"SP2837ZMC89J40K4YTS64B00M7065C6X46JX6ARG0".to_string()).unwrap(); - let mut participant_1 = make_dummy_miner_payment_schedule(&miner_1, 500, 100, 0, 250, 1000); - let mut participant_2 = make_dummy_miner_payment_schedule(&miner_2, 500, 0, 0, 1000, 1000); - let mut user = make_dummy_user_payment_schedule(&user_1, 500, 0, 0, 750, 1000, 1); - - // blocks are NOT full - let fill_cutoff = (((MINER_FEE_MINIMUM_BLOCK_USAGE as u128) << 64) / 100u128) as u64; - participant_1.fill = fill_cutoff - 1; - participant_2.fill = fill_cutoff - 1; - user.fill = fill_cutoff - 1; - - sample.push((participant_1.clone(), vec![user.clone()])); - - for i in 0..4 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_1, 250, 100, 0, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); - - next_participant.fill = fill_cutoff - 1; - next_user.fill = fill_cutoff - 1; - - sample.push((next_participant, vec![next_user])); - } - - for i in 0..5 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_2, 250, 100, 0, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); - - next_participant.fill = fill_cutoff - 1; - next_user.fill = fill_cutoff - 1; - - sample.push((next_participant, vec![next_user])); - } - - let reward_miner_1 = StacksChainState::calculate_miner_reward(&participant_1, &sample); - let reward_miner_2 = StacksChainState::calculate_miner_reward(&participant_2, &sample); - let reward_user_1 = StacksChainState::calculate_miner_reward(&user, &sample); - - // if miner 1 won, then it should have received 1/4 the coinbase, and miner should have received only shared tx fees - assert_eq!(reward_miner_1.coinbase, 125); - assert_eq!(reward_miner_1.tx_fees_anchored, 500); // did half the work over the sample - assert_eq!(reward_miner_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_miner_1.tx_fees_streamed_confirmed, 0); - - // miner 2 didn't mine this block - assert_eq!(reward_miner_2.coinbase, 0); - assert_eq!(reward_miner_2.tx_fees_anchored, 500); // did half the work over the sample - assert_eq!(reward_miner_2.tx_fees_streamed_produced, 0); - assert_eq!(reward_miner_2.tx_fees_streamed_confirmed, 0); - - // user should have received 3/4 the coinbase, but no tx fees + // user should have received 3/4 the coinbase assert_eq!(reward_user_1.coinbase, 375); assert_eq!(reward_user_1.tx_fees_anchored, 0); assert_eq!(reward_user_1.tx_fees_streamed_produced, 0); assert_eq!(reward_user_1.tx_fees_streamed_confirmed, 0); - } - - // TODO: broken; needs to be rewritten once transaction fee processing is added - #[test] - fn miner_reward_two_miners_one_user_anchored_tx_fees_full() { - let mut sample = vec![]; - let miner_1 = StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()).unwrap(); - let miner_2 = StacksAddress::from_string(&"SP8WWTGMNCCSB88QF4VYWN69PAMQRMF34FCT498G".to_string()).unwrap(); - let user_1 = StacksAddress::from_string(&"SP2837ZMC89J40K4YTS64B00M7065C6X46JX6ARG0".to_string()).unwrap(); - let mut participant_1 = make_dummy_miner_payment_schedule(&miner_1, 500, 100, 0, 250, 1000); - let mut participant_2 = make_dummy_miner_payment_schedule(&miner_2, 500, 0, 0, 1000, 1000); - let mut user = make_dummy_user_payment_schedule(&user_1, 500, 0, 0, 750, 1000, 1); - - let fill_cutoff = ((MINER_FEE_MINIMUM_BLOCK_USAGE as u128) << 64) / 100u128; - - // blocks are full to 90% - let fill = ((90u128 << 64) / (100u128)) as u64; - participant_1.fill = fill; - participant_2.fill = fill; - user.fill = fill; - - sample.push((participant_1.clone(), vec![user.clone()])); - - for i in 0..4 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_1, 250, 100, 0, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); - - next_participant.fill = fill; - next_user.fill = fill; - - sample.push((next_participant, vec![next_user])); - } - - for i in 0..5 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_2, 250, 100, 0, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); - - next_participant.fill = fill; - next_user.fill = fill; - - sample.push((next_participant, vec![next_user])); - } - - let reward_miner_1 = StacksChainState::calculate_miner_reward(&participant_1, &sample); - let reward_miner_2 = StacksChainState::calculate_miner_reward(&participant_2, &sample); - let reward_user_1 = StacksChainState::calculate_miner_reward(&user, &sample); - let expected_shared = - if (500 * fill_cutoff) & 0x0000000000000000ffffffffffffffff > 0x00000000000000007fffffffffffffff { - ((500 * fill_cutoff) >> 64) + 1 - } - else { - (500 * fill_cutoff) >> 64 - }; - - // miner 1 should have received 1/4 the coinbase - assert_eq!(reward_miner_1.coinbase, 125); - assert_eq!(reward_miner_1.tx_fees_anchored, expected_shared); // did half the work over the sample - assert_eq!(reward_miner_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_miner_1.tx_fees_streamed_confirmed, 0); - - // miner 2 didn't win this block, so it gets no coinbase - assert_eq!(reward_miner_2.coinbase, 0); - assert_eq!(reward_miner_2.tx_fees_anchored, expected_shared); // did half the work over the sample - assert_eq!(reward_miner_2.tx_fees_streamed_produced, 0); - assert_eq!(reward_miner_2.tx_fees_streamed_confirmed, 0); - - // user should have received 3/4 the coinbase, but no tx fees - assert_eq!(reward_user_1.coinbase, 375); - assert_eq!(reward_user_1.tx_fees_anchored, 0); - assert_eq!(reward_user_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_user_1.tx_fees_streamed_confirmed, 0); + assert_eq!(parent_user_1.total(), 0); } - // TODO: broken; needs to be rewritten once transaction fee processing is added #[test] - fn miner_reward_two_miners_one_user_anchored_tx_fees_full_streamed() { - let mut sample = vec![]; - let miner_1 = StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()).unwrap(); - let miner_2 = StacksAddress::from_string(&"SP8WWTGMNCCSB88QF4VYWN69PAMQRMF34FCT498G".to_string()).unwrap(); - let user_1 = StacksAddress::from_string(&"SP2837ZMC89J40K4YTS64B00M7065C6X46JX6ARG0".to_string()).unwrap(); - let mut participant_1 = make_dummy_miner_payment_schedule(&miner_1, 500, 100, 100, 250, 1000); - let mut participant_2 = make_dummy_miner_payment_schedule(&miner_2, 500, 0, 0, 1000, 1000); - let mut user = make_dummy_user_payment_schedule(&user_1, 500, 0, 0, 750, 1000, 1); - - let fill_cutoff = ((MINER_FEE_MINIMUM_BLOCK_USAGE as u128) << 64) / 100u128; - - // blocks are full to 90% - let fill = ((90u128 << 64) / (100u128)) as u64; - participant_1.fill = fill; - participant_2.fill = fill; - user.fill = fill; - - sample.push((participant_1.clone(), vec![user.clone()])); - - for i in 0..4 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_1, 250, 100, 100, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); - - next_participant.fill = fill; - next_user.fill = fill; - - sample.push((next_participant, vec![next_user])); - } - - for i in 0..5 { - let mut next_participant = make_dummy_miner_payment_schedule(&miner_2, 250, 100, 100, 250, 1000); - let mut next_user = make_dummy_user_payment_schedule(&user_1, 750, 0, 0, 750, 1000, 1); - - next_participant.fill = fill; - next_user.fill = fill; - - sample.push((next_participant, vec![next_user])); - } - - let reward_miner_1 = StacksChainState::calculate_miner_reward(&participant_1, &sample); - let reward_miner_2 = StacksChainState::calculate_miner_reward(&participant_2, &sample); - let reward_user_1 = StacksChainState::calculate_miner_reward(&user, &sample); + fn miner_reward_tx_fees() { + let miner_1 = + StacksAddress::from_string(&"SP1A2K3ENNA6QQ7G8DVJXM24T6QMBDVS7D0TRTAR5".to_string()) + .unwrap(); - let expected_shared = - if (500 * fill_cutoff) & 0x0000000000000000ffffffffffffffff > 0x00000000000000007fffffffffffffff { - ((500 * fill_cutoff) >> 64) + 1 - } - else { - (500 * fill_cutoff) >> 64 - }; + let parent_miner_1 = + StacksAddress::from_string(&"SP2QDF700V0FWXVNQJJ4XFGBWE6R2Y4APTSFQNBVE".to_string()) + .unwrap(); - // miner 1 produced this block with the help from users 3x as interested, so it gets 1/4 of the coinbase - assert_eq!(reward_miner_1.coinbase, 125); - assert_eq!(reward_miner_1.tx_fees_anchored, expected_shared); // did half the work over the sample - assert_eq!(reward_miner_1.tx_fees_streamed_produced, 240); // produced half of the microblocks, should get half the 2/5 of the reward - assert_eq!(reward_miner_1.tx_fees_streamed_confirmed, 240); // confirmed half of the microblocks, should get half of the 3/5 reward + let participant = make_dummy_miner_payment_schedule(&miner_1, 500, 100, 105, 1000, 1000); + let parent_participant = + make_dummy_miner_payment_schedule(&parent_miner_1, 500, 100, 395, 1000, 1000); - // miner 2 didn't produce this block, so no coinbase - assert_eq!(reward_miner_2.coinbase, 0); - assert_eq!(reward_miner_2.tx_fees_anchored, expected_shared); // did half the work over the sample - assert_eq!(reward_miner_2.tx_fees_streamed_produced, 200); - assert_eq!(reward_miner_2.tx_fees_streamed_confirmed, 300); + let (parent_reward, miner_reward) = StacksChainState::calculate_miner_reward( + false, + &participant, + &participant, + &vec![], + &parent_participant, + None, + ); - // user should have received 3/4 the coinbase, but no tx fees - assert_eq!(reward_user_1.coinbase, 375); - assert_eq!(reward_user_1.tx_fees_anchored, 0); - assert_eq!(reward_user_1.tx_fees_streamed_produced, 0); - assert_eq!(reward_user_1.tx_fees_streamed_confirmed, 0); + // miner should have received the entire coinbase + assert_eq!(miner_reward.coinbase, 500); + assert_eq!(miner_reward.tx_fees_anchored, 100); + assert_eq!(miner_reward.tx_fees_streamed_produced, 0); // not rewarded yet + assert_eq!(miner_reward.tx_fees_streamed_confirmed, (105 * 3) / 5); + + // parent gets produced stream fees + assert_eq!(parent_reward.coinbase, 0); + assert_eq!(parent_reward.tx_fees_anchored, 0); + assert_eq!(parent_reward.tx_fees_streamed_produced, (395 * 2) / 5); + assert_eq!(parent_reward.tx_fees_streamed_confirmed, 0); } - */ } diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index fa46ef92c8..b4d546f5b7 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -3350,9 +3350,6 @@ impl StacksChainState { /// Create the block reward. /// `coinbase_reward_ustx` is the total coinbase reward for this block, including any /// accumulated rewards from missed sortitions or initial mining rewards. - // TODO: calculate how full the block was. - // TODO: tx_fees needs to be normalized _a priori_ to be equal to the block-determined fee - // rate, times the fraction of the block's total utilization. fn make_scheduled_miner_reward( mainnet: bool, parent_block_hash: &BlockHeaderHash, @@ -3360,12 +3357,11 @@ impl StacksChainState { block: &StacksBlock, block_consensus_hash: &ConsensusHash, block_height: u64, - tx_fees: u128, + anchored_fees: u128, streamed_fees: u128, stx_burns: u128, burnchain_commit_burn: u64, burnchain_sortition_burn: u64, - fill: u64, coinbase_reward_ustx: u128, ) -> Result { let coinbase_tx = block.get_coinbase_tx().ok_or(Error::InvalidStacksBlock( @@ -3385,12 +3381,11 @@ impl StacksChainState { parent_block_hash: parent_block_hash.clone(), parent_consensus_hash: parent_consensus_hash.clone(), coinbase: coinbase_reward_ustx, - tx_fees_anchored: tx_fees, + tx_fees_anchored: anchored_fees, tx_fees_streamed: streamed_fees, stx_burns: stx_burns, burnchain_commit_burn: burnchain_commit_burn, burnchain_sortition_burn: burnchain_sortition_burn, - fill: fill, miner: true, stacks_block_height: block_height, vtxindex: 0, @@ -3924,6 +3919,7 @@ impl StacksChainState { clarity_tx: &mut ClarityTx<'a>, miner_share: &MinerReward, users_share: &Vec, + parent_share: &MinerReward, ) -> Result { let mut coinbase_reward = miner_share.coinbase; StacksChainState::process_matured_miner_reward(clarity_tx, miner_share)?; @@ -3931,6 +3927,10 @@ impl StacksChainState { coinbase_reward += reward.coinbase; StacksChainState::process_matured_miner_reward(clarity_tx, reward)?; } + + // give the parent its confirmed share of the streamed microblocks + assert_eq!(parent_share.total(), parent_share.tx_fees_streamed_produced); + StacksChainState::process_matured_miner_reward(clarity_tx, parent_share)?; Ok(coinbase_reward) } @@ -3981,6 +3981,44 @@ impl StacksChainState { .map_err(Error::ClarityError) } + /// Given the list of matured miners, find the miner reward schedule that produced the parent + /// of the block whose coinbase just matured. + pub fn get_parent_matured_miner( + stacks_tx: &mut StacksDBTx, + mainnet: bool, + latest_matured_miners: &Vec, + ) -> Result { + let parent_miner = if let Some(ref miner) = latest_matured_miners.first().as_ref() { + StacksChainState::get_scheduled_block_rewards_at_block( + stacks_tx, + &StacksBlockHeader::make_index_block_hash( + &miner.parent_consensus_hash, + &miner.parent_block_hash, + ), + )? + .pop() + .unwrap_or_else(|| { + if miner.parent_consensus_hash == FIRST_BURNCHAIN_CONSENSUS_HASH + && miner.parent_block_hash == FIRST_STACKS_BLOCK_HASH + { + MinerPaymentSchedule::genesis(mainnet) + } else { + panic!( + "CORRUPTION: parent {}/{} of {}/{} not found in DB", + &miner.parent_consensus_hash, + &miner.parent_block_hash, + &miner.consensus_hash, + &miner.block_hash + ); + } + }) + } else { + MinerPaymentSchedule::genesis(mainnet) + }; + + Ok(parent_miner) + } + /// Process the next pre-processed staging block. /// We've already processed parent_chain_tip. chain_tip refers to a block we have _not_ /// processed yet. @@ -4021,6 +4059,12 @@ impl StacksChainState { &parent_chain_tip, )?; + let matured_miner_parent = StacksChainState::get_parent_matured_miner( + chainstate_tx.deref_mut(), + mainnet, + &latest_matured_miners, + )?; + let ( scheduled_miner_reward, tx_receipts, @@ -4117,6 +4161,7 @@ impl StacksChainState { &mut clarity_tx, parent_chain_tip, latest_matured_miners, + matured_miner_parent, ) { Ok(miner_rewards_opt) => miner_rewards_opt, Err(e) => { @@ -4235,27 +4280,30 @@ impl StacksChainState { let block_cost = clarity_tx.cost_so_far(); // grant matured miner rewards - let new_liquid_miner_ustx = if let Some((ref miner_reward, ref user_rewards, _)) = - matured_miner_rewards_opt.as_ref() - { - // grant in order by miner, then users - StacksChainState::process_matured_miner_rewards( - &mut clarity_tx, - miner_reward, - user_rewards, - )? - } else { - 0 - }; + let new_liquid_miner_ustx = + if let Some((ref miner_reward, ref user_rewards, ref parent_miner_reward, _)) = + matured_miner_rewards_opt.as_ref() + { + // grant in order by miner, then users + StacksChainState::process_matured_miner_rewards( + &mut clarity_tx, + miner_reward, + user_rewards, + parent_miner_reward, + )? + } else { + 0 + }; // obtain reward info for receipt let (matured_rewards, matured_rewards_info) = - if let Some((miner_reward, mut user_rewards, reward_ptr)) = + if let Some((miner_reward, mut user_rewards, parent_reward, reward_ptr)) = matured_miner_rewards_opt { let mut ret = vec![]; ret.push(miner_reward); ret.append(&mut user_rewards); + ret.push(parent_reward); (ret, Some(reward_ptr)) } else { (vec![], None) @@ -4347,12 +4395,11 @@ impl StacksChainState { &block, chain_tip_consensus_hash, next_block_height, - block_fees, // TODO: calculate (STX/compute unit) * (compute used) + block_fees, microblock_fees, total_burnt, burnchain_commit_burn, burnchain_sortition_burn, - 0xffffffffffffffff, total_coinbase, ) // TODO: calculate total compute budget and scale up .expect("FATAL: parsed and processed a block without a coinbase"); @@ -5048,7 +5095,7 @@ impl StacksChainState { .map_err(|e| MemPoolRejection::FailedToValidate(e))?; // 3: it must pay a tx fee - let fee = tx.get_fee_rate(); + let fee = tx.get_tx_fee(); if fee < MINIMUM_TX_FEE || fee / tx_size < MINIMUM_TX_FEE_RATE_PER_BYTE { return Err(MemPoolRejection::FeeTooLow( diff --git a/src/chainstate/stacks/db/mod.rs b/src/chainstate/stacks/db/mod.rs index b66b263b7c..207fa79920 100644 --- a/src/chainstate/stacks/db/mod.rs +++ b/src/chainstate/stacks/db/mod.rs @@ -28,7 +28,7 @@ use rusqlite::Row; use rusqlite::Transaction; use rusqlite::NO_PARAMS; -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::{btree_map::Entry, BTreeMap}; use std::fmt; use std::fs; use std::io; @@ -129,7 +129,6 @@ pub struct MinerPaymentSchedule { pub stx_burns: u128, pub burnchain_commit_burn: u64, pub burnchain_sortition_burn: u64, - pub fill: u64, // fixed-point fraction of how full this block is, scaled up between 0 and 2**64 - 1 (i.e. 0x8000000000000000 == 50% full) pub miner: bool, // is this a schedule payment for the block's miner? pub stacks_block_height: u64, pub vtxindex: u32, @@ -523,7 +522,6 @@ const STACKS_CHAIN_STATE_SQL: &'static [&'static str] = &[ stx_burns TEXT NOT NULL, -- encodes u128 burnchain_commit_burn INT NOT NULL, burnchain_sortition_burn INT NOT NULL, - fill TEXT NOT NULL, -- encodes u64 miner INT NOT NULL, -- internal use @@ -832,7 +830,7 @@ impl StacksChainState { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Uncompressed, nonce: 0, - fee_rate: 0, + tx_fee: 0, signature: MessageSignature::empty(), }, )); @@ -961,7 +959,7 @@ impl StacksChainState { if let Some(get_schedules) = boot_data.get_bulk_initial_lockups.take() { info!("Initializing chain with lockups"); - let mut lockups_per_block: HashMap> = HashMap::new(); + let mut lockups_per_block: BTreeMap> = BTreeMap::new(); let initial_lockups = get_schedules(); for schedule in initial_lockups { let stx_address = @@ -983,6 +981,7 @@ impl StacksChainState { } }; } + let lockup_contract_id = boot_code_id("lockup"); clarity_tx.connection().as_transaction(|clarity| { clarity diff --git a/src/chainstate/stacks/db/transactions.rs b/src/chainstate/stacks/db/transactions.rs index 8c6b42b00d..8c3b23ab70 100644 --- a/src/chainstate/stacks/db/transactions.rs +++ b/src/chainstate/stacks/db/transactions.rs @@ -1096,7 +1096,7 @@ impl StacksChainState { // TODO: this field is the fee *rate*, not the absolute fee. This code is broken until we have // the true block reward system built. let new_payer_account = StacksChainState::get_payer_account(&mut transaction, tx); - let fee = tx.get_fee_rate(); + let fee = tx.get_tx_fee(); StacksChainState::pay_transaction_fee(&mut transaction, fee, new_payer_account)?; // update the account nonces @@ -1168,7 +1168,7 @@ pub mod test { tx_stx_transfer.chain_id = 0x80000000; tx_stx_transfer.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_stx_transfer); signer.sign_origin(&privk).unwrap(); @@ -1226,7 +1226,7 @@ pub mod test { tx_stx_transfer.chain_id = 0x80000000; tx_stx_transfer.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); tx_stx_transfer.set_origin_nonce(1); let mut signer = StacksTransactionSigner::new(&tx_stx_transfer); @@ -1373,12 +1373,12 @@ pub mod test { tx_stx_transfer_wrong_nonce_sponsored.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer_same_receiver.set_fee_rate(0); - tx_stx_transfer_wrong_network.set_fee_rate(0); - tx_stx_transfer_wrong_chain_id.set_fee_rate(0); - tx_stx_transfer_postconditions.set_fee_rate(0); - tx_stx_transfer_wrong_nonce.set_fee_rate(0); - tx_stx_transfer_wrong_nonce_sponsored.set_fee_rate(0); + tx_stx_transfer_same_receiver.set_tx_fee(0); + tx_stx_transfer_wrong_network.set_tx_fee(0); + tx_stx_transfer_wrong_chain_id.set_tx_fee(0); + tx_stx_transfer_postconditions.set_tx_fee(0); + tx_stx_transfer_wrong_nonce.set_tx_fee(0); + tx_stx_transfer_wrong_nonce_sponsored.set_tx_fee(0); let error_frags = vec![ "address tried to send to itself".to_string(), @@ -1490,7 +1490,7 @@ pub mod test { tx_stx_transfer.chain_id = 0x80000000; tx_stx_transfer.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_stx_transfer); signer.sign_origin(&privk_origin).unwrap(); @@ -1573,7 +1573,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract_call); signer.sign_origin(&privk).unwrap(); @@ -1670,7 +1670,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); tx_contract.set_origin_nonce(next_nonce); let mut signer = StacksTransactionSigner::new(&tx_contract); @@ -1769,7 +1769,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); tx_contract.set_origin_nonce(i as u64); let mut signer = StacksTransactionSigner::new(&tx_contract); @@ -1843,7 +1843,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract_call); signer.sign_origin(&privk_origin).unwrap(); @@ -1919,7 +1919,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk).unwrap(); @@ -1947,7 +1947,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); let mut signer_2 = StacksTransactionSigner::new(&tx_contract_call); signer_2.sign_origin(&privk_2).unwrap(); @@ -2041,7 +2041,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk).unwrap(); @@ -2094,7 +2094,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); tx_contract_call.set_origin_nonce(next_nonce); let mut signer_2 = StacksTransactionSigner::new(&tx_contract_call); @@ -2165,7 +2165,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk).unwrap(); @@ -2240,7 +2240,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); let mut signer_2 = StacksTransactionSigner::new(&tx_contract_call); signer_2.sign_origin(&privk_2).unwrap(); @@ -2298,7 +2298,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk).unwrap(); @@ -2336,7 +2336,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); let mut signer_2 = StacksTransactionSigner::new(&tx_contract_call); signer_2.sign_origin(&privk_origin).unwrap(); @@ -2516,7 +2516,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk_origin).unwrap(); @@ -2546,7 +2546,7 @@ pub mod test { ); tx_contract_call_stackaroos.chain_id = 0x80000000; - tx_contract_call_stackaroos.set_fee_rate(0); + tx_contract_call_stackaroos.set_tx_fee(0); // mint 100 stackaroos to recv_addr, and set a post-condition on the contract-principal // to check it. @@ -2647,7 +2647,7 @@ pub mod test { ); tx_contract_call_user_stackaroos.chain_id = 0x80000000; - tx_contract_call_user_stackaroos.set_fee_rate(0); + tx_contract_call_user_stackaroos.set_tx_fee(0); // recv_addr sends 100 stackaroos back to addr_publisher. // assert recv_addr sent ==, <=, or >= 100 stackaroos @@ -2733,7 +2733,7 @@ pub mod test { ); tx_contract_call_names.chain_id = 0x80000000; - tx_contract_call_names.set_fee_rate(0); + tx_contract_call_names.set_tx_fee(0); tx_contract_call_names.set_origin_nonce(nonce); tx_contract_call_names.add_post_condition(TransactionPostCondition::Nonfungible( @@ -2852,7 +2852,7 @@ pub mod test { ); tx_contract_call_names.chain_id = 0x80000000; - tx_contract_call_names.set_fee_rate(0); + tx_contract_call_names.set_tx_fee(0); tx_contract_call_names.set_origin_nonce(nonce); tx_contract_call_names.add_post_condition(TransactionPostCondition::Nonfungible( @@ -3209,7 +3209,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk_origin).unwrap(); @@ -3252,7 +3252,7 @@ pub mod test { ); tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(nonce); tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Deny; @@ -3295,7 +3295,7 @@ pub mod test { tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Allow; tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(nonce); let mut signer = StacksTransactionSigner::new(&tx_contract_call_both); @@ -3333,7 +3333,7 @@ pub mod test { ); tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(recv_nonce); tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Deny; @@ -3384,7 +3384,7 @@ pub mod test { ); tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(nonce); tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Deny; @@ -3430,7 +3430,7 @@ pub mod test { ); tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(nonce); tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Deny; @@ -3475,7 +3475,7 @@ pub mod test { ); tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(recv_nonce); tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Deny; @@ -3522,7 +3522,7 @@ pub mod test { ); tx_contract_call_both.chain_id = 0x80000000; - tx_contract_call_both.set_fee_rate(0); + tx_contract_call_both.set_tx_fee(0); tx_contract_call_both.set_origin_nonce(recv_nonce); tx_contract_call_both.post_condition_mode = TransactionPostConditionMode::Deny; @@ -3846,7 +3846,7 @@ pub mod test { ); tx_contract.chain_id = 0x80000000; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk_origin).unwrap(); @@ -3866,7 +3866,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(0); + tx_contract_call.set_tx_fee(0); tx_contract_call.set_origin_nonce(1); tx_contract_call.post_condition_mode = TransactionPostConditionMode::Deny; @@ -6969,7 +6969,7 @@ pub mod test { ); tx_contract_create.chain_id = 0x80000000; - tx_contract_create.set_fee_rate(0); + tx_contract_create.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract_create); signer.sign_origin(&privk).unwrap(); @@ -6995,7 +6995,7 @@ pub mod test { ); tx_contract_call.chain_id = 0x80000000; - tx_contract_call.set_fee_rate(1); + tx_contract_call.set_tx_fee(1); tx_contract_call.set_origin_nonce(1); tx_contract_call.post_condition_mode = TransactionPostConditionMode::Allow; @@ -7054,7 +7054,7 @@ pub mod test { ); tx_contract_create.chain_id = 0x80000000; - tx_contract_create.set_fee_rate(0); + tx_contract_create.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract_create); signer.sign_origin(&tx_privk).unwrap(); @@ -7145,7 +7145,7 @@ pub mod test { ); tx_poison_microblock.chain_id = 0x80000000; - tx_poison_microblock.set_fee_rate(0); + tx_poison_microblock.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_poison_microblock); signer.sign_origin(&reporter_privk).unwrap(); @@ -7255,7 +7255,7 @@ pub mod test { ); tx_poison_microblock.chain_id = 0x80000000; - tx_poison_microblock.set_fee_rate(0); + tx_poison_microblock.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_poison_microblock); signer.sign_origin(&reporter_privk).unwrap(); @@ -7349,7 +7349,7 @@ pub mod test { ); tx_poison_microblock_1.chain_id = 0x80000000; - tx_poison_microblock_1.set_fee_rate(0); + tx_poison_microblock_1.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_poison_microblock_1); signer.sign_origin(&reporter_privk_1).unwrap(); @@ -7373,7 +7373,7 @@ pub mod test { ); tx_poison_microblock_2.chain_id = 0x80000000; - tx_poison_microblock_2.set_fee_rate(0); + tx_poison_microblock_2.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_poison_microblock_2); signer.sign_origin(&reporter_privk_2).unwrap(); diff --git a/src/chainstate/stacks/db/unconfirmed.rs b/src/chainstate/stacks/db/unconfirmed.rs index 9bf8dd1ebe..1ca0b951a0 100644 --- a/src/chainstate/stacks/db/unconfirmed.rs +++ b/src/chainstate/stacks/db/unconfirmed.rs @@ -582,7 +582,7 @@ mod test { tx_stx_transfer.chain_id = 0x80000000; tx_stx_transfer.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); tx_stx_transfer.set_origin_nonce(tenure_id as u64); let mut signer = StacksTransactionSigner::new(&tx_stx_transfer); @@ -812,7 +812,7 @@ mod test { tx_stx_transfer.chain_id = 0x80000000; tx_stx_transfer.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); tx_stx_transfer.set_origin_nonce((100 * tenure_id + 10 * i + j) as u64); let mut signer = StacksTransactionSigner::new(&tx_stx_transfer); diff --git a/src/chainstate/stacks/events.rs b/src/chainstate/stacks/events.rs index 136dd83d2b..66f8708115 100644 --- a/src/chainstate/stacks/events.rs +++ b/src/chainstate/stacks/events.rs @@ -127,6 +127,13 @@ impl StacksTransactionEvent { "type": "nft_mint_event", "nft_mint_event": event_data.json_serialize() }), + StacksTransactionEvent::NFTEvent(NFTEventType::NFTBurnEvent(event_data)) => json!({ + "txid": format!("0x{:?}", txid), + "event_index": event_index, + "committed": committed, + "type": "nft_burn_event", + "nft_burn_event": event_data.json_serialize() + }), StacksTransactionEvent::FTEvent(FTEventType::FTTransferEvent(event_data)) => json!({ "txid": format!("0x{:?}", txid), "event_index": event_index, @@ -141,6 +148,13 @@ impl StacksTransactionEvent { "type": "ft_mint_event", "ft_mint_event": event_data.json_serialize() }), + StacksTransactionEvent::FTEvent(FTEventType::FTBurnEvent(event_data)) => json!({ + "txid": format!("0x{:?}", txid), + "event_index": event_index, + "committed": committed, + "type": "ft_burn_event", + "ft_burn_event": event_data.json_serialize() + }), } } } @@ -157,12 +171,14 @@ pub enum STXEventType { pub enum NFTEventType { NFTTransferEvent(NFTTransferEventData), NFTMintEvent(NFTMintEventData), + NFTBurnEvent(NFTBurnEventData), } #[derive(Debug, Clone, PartialEq)] pub enum FTEventType { FTTransferEvent(FTTransferEventData), FTMintEvent(FTMintEventData), + FTBurnEvent(FTBurnEventData), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -279,6 +295,30 @@ impl NFTMintEventData { } } +#[derive(Debug, Clone, PartialEq)] +pub struct NFTBurnEventData { + pub asset_identifier: AssetIdentifier, + pub sender: PrincipalData, + pub value: Value, +} + +impl NFTBurnEventData { + pub fn json_serialize(&self) -> serde_json::Value { + let raw_value = { + let mut bytes = vec![]; + self.value.consensus_serialize(&mut bytes).unwrap(); + let formatted_bytes: Vec = bytes.iter().map(|b| format!("{:02x}", b)).collect(); + formatted_bytes + }; + json!({ + "asset_identifier": format!("{}", self.asset_identifier), + "sender": format!("{}",self.sender), + "value": self.value, + "raw_value": format!("0x{}", raw_value.join("")), + }) + } +} + #[derive(Debug, Clone, PartialEq)] pub struct FTTransferEventData { pub asset_identifier: AssetIdentifier, @@ -315,6 +355,23 @@ impl FTMintEventData { } } +#[derive(Debug, Clone, PartialEq)] +pub struct FTBurnEventData { + pub asset_identifier: AssetIdentifier, + pub sender: PrincipalData, + pub amount: u128, +} + +impl FTBurnEventData { + pub fn json_serialize(&self) -> serde_json::Value { + json!({ + "asset_identifier": format!("{}", self.asset_identifier), + "sender": format!("{}",self.sender), + "amount": format!("{}", self.amount), + }) + } +} + #[derive(Debug, Clone, PartialEq)] pub struct SmartContractEventData { pub key: (QualifiedContractIdentifier, String), diff --git a/src/chainstate/stacks/index/storage.rs b/src/chainstate/stacks/index/storage.rs index 18ed4cca03..53a0040636 100644 --- a/src/chainstate/stacks/index/storage.rs +++ b/src/chainstate/stacks/index/storage.rs @@ -1369,11 +1369,7 @@ impl<'a, T: MarfTrieId> TrieStorageConnection<'a, T> { } // opening a different Trie than the one we're extending - self.data.cur_block_id = - Some(trie_sql::get_block_identifier(&self.db, bhh).map_err(|e| { - warn!("Failed to load identifier for block {}", &bhh); - e - })?); + self.data.cur_block_id = Some(trie_sql::get_block_identifier(&self.db, bhh)?); self.data.cur_block = bhh.clone(); Ok(()) diff --git a/src/chainstate/stacks/miner.rs b/src/chainstate/stacks/miner.rs index 7130e60b4c..278f77fc38 100644 --- a/src/chainstate/stacks/miner.rs +++ b/src/chainstate/stacks/miner.rs @@ -765,10 +765,15 @@ impl StacksBlockBuilder { assert!(!self.anchored_done); // add miner payments - if let Some((ref miner_reward, ref user_rewards)) = self.miner_payouts { + if let Some((ref miner_reward, ref user_rewards, ref parent_reward)) = self.miner_payouts { // grant in order by miner, then users - StacksChainState::process_matured_miner_rewards(clarity_tx, miner_reward, user_rewards) - .expect("FATAL: failed to process miner rewards"); + StacksChainState::process_matured_miner_rewards( + clarity_tx, + miner_reward, + user_rewards, + parent_reward, + ) + .expect("FATAL: failed to process miner rewards"); } // process unlocks @@ -888,10 +893,16 @@ impl StacksBlockBuilder { chainstate: &'a mut StacksChainState, burn_dbconn: &'a SortitionDBConn, ) -> Result, Error> { + let mainnet = chainstate.config().mainnet; + // find matured miner rewards, so we can grant them within the Clarity DB tx. - let latest_matured_miners = { + let (latest_matured_miners, matured_miner_parent) = { let mut tx = chainstate.index_tx_begin()?; - StacksChainState::get_scheduled_block_rewards(&mut tx, &self.chain_tip)? + let latest_miners = + StacksChainState::get_scheduled_block_rewards(&mut tx, &self.chain_tip)?; + let parent_miner = + StacksChainState::get_parent_matured_miner(&mut tx, mainnet, &latest_miners)?; + (latest_miners, parent_miner) }; // there's no way the miner can learn either the burn block hash or the stacks block hash, @@ -906,11 +917,13 @@ impl StacksBlockBuilder { self.header.parent_block) ); - if let Some((ref _miner_payout, ref _user_payouts)) = self.miner_payouts { + if let Some((ref _miner_payout, ref _user_payouts, ref _parent_reward)) = self.miner_payouts + { test_debug!( - "Miner payout to process: {:?}; user payouts: {:?}", + "Miner payout to process: {:?}; user payouts: {:?}; parent payout: {:?}", _miner_payout, - _user_payouts + _user_payouts, + _parent_reward ); } @@ -969,9 +982,11 @@ impl StacksBlockBuilder { &mut tx, &self.chain_tip, latest_matured_miners, + matured_miner_parent, )?; - self.miner_payouts = matured_miner_rewards_opt.map(|(miner, users, _)| (miner, users)); + self.miner_payouts = + matured_miner_rewards_opt.map(|(miner, users, parent, _)| (miner, users, parent)); test_debug!( "Miner {}: Apply {} parent microblocks", @@ -1503,8 +1518,23 @@ pub mod test { } impl TestStacksNode { - pub fn new(mainnet: bool, chain_id: u32, test_name: &str) -> TestStacksNode { - let chainstate = instantiate_chainstate(mainnet, chain_id, test_name); + pub fn new( + mainnet: bool, + chain_id: u32, + test_name: &str, + mut initial_balance_recipients: Vec, + ) -> TestStacksNode { + initial_balance_recipients.sort(); + let initial_balances = initial_balance_recipients + .into_iter() + .map(|addr| (addr, 10_000_000_000)) + .collect(); + let chainstate = instantiate_chainstate_with_balances( + mainnet, + chain_id, + test_name, + initial_balances, + ); TestStacksNode { chainstate: chainstate, prev_keys: vec![], @@ -2029,22 +2059,107 @@ pub mod test { block_height: u64, prev_block_rewards: &Vec>, ) -> bool { - let mut total: u128 = 0; + let mut block_rewards = HashMap::new(); + let mut stream_rewards = HashMap::new(); + let mut heights = HashMap::new(); + let mut confirmed = HashSet::new(); + for (i, reward_list) in prev_block_rewards.iter().enumerate() { + for reward in reward_list.iter() { + let ibh = StacksBlockHeader::make_index_block_hash( + &reward.consensus_hash, + &reward.block_hash, + ); + if reward.coinbase > 0 { + block_rewards.insert(ibh.clone(), reward.clone()); + } + if reward.tx_fees_streamed > 0 { + stream_rewards.insert(ibh.clone(), reward.clone()); + } + heights.insert(ibh.clone(), i); + confirmed.insert(( + StacksBlockHeader::make_index_block_hash( + &reward.parent_consensus_hash, + &reward.parent_block_hash, + ), + i, + )); + } + } + + // what was the miner's total spend? + let miner_nonce = clarity_tx.with_clarity_db_readonly(|db| { + db.get_account_nonce( + &StandardPrincipalData::from(miner.origin_address().unwrap()).into(), + ) + }); + + let mut spent_total = 0; + for (nonce, spent) in miner.spent_at_nonce.iter() { + if *nonce < miner_nonce { + spent_total += *spent; + } + } + + let mut total: u128 = 10_000_000_000 - spent_total; + test_debug!( + "Miner {} has spent {} in total so far", + &miner.origin_address().unwrap(), + spent_total + ); + if block_height >= MINER_REWARD_MATURITY { for (i, prev_block_reward) in prev_block_rewards.iter().enumerate() { if i as u64 > block_height - MINER_REWARD_MATURITY { break; } + let mut found = false; for recipient in prev_block_reward { if recipient.address == miner.origin_address().unwrap() { - let reward: u128 = recipient.coinbase; // TODO: expand to cover tx fees + let reward: u128 = recipient.coinbase + + recipient.tx_fees_anchored + + (3 * recipient.tx_fees_streamed / 5); + test_debug!( - "Miner {} received a reward {} at block {}", + "Miner {} received a reward {} = {} + {} + {} at block {}", &recipient.address.to_string(), reward, + recipient.coinbase, + recipient.tx_fees_anchored, + (3 * recipient.tx_fees_streamed / 5), i ); total += reward; + found = true; + } + } + if !found { + test_debug!( + "Miner {} received no reward at block {}", + miner.origin_address().unwrap(), + i + ); + } + } + + for (parent_block, confirmed_block_height) in confirmed.into_iter() { + if confirmed_block_height as u64 > block_height - MINER_REWARD_MATURITY { + continue; + } + if let Some(ref parent_reward) = stream_rewards.get(&parent_block) { + if parent_reward.address == miner.origin_address().unwrap() { + let parent_streamed = (2 * parent_reward.tx_fees_streamed) / 5; + let parent_ibh = StacksBlockHeader::make_index_block_hash( + &parent_reward.consensus_hash, + &parent_reward.block_hash, + ); + test_debug!( + "Miner {} received a produced-stream reward {} from {} confirmed at {}", + miner.origin_address().unwrap().to_string(), + parent_streamed, + heights.get(&parent_ibh).unwrap(), + confirmed_block_height + ); + total += parent_streamed; } } } @@ -2149,12 +2264,18 @@ pub mod test { G: FnMut(&StacksBlock, &Vec) -> bool, { let full_test_name = format!("{}-1_fork_1_miner_1_burnchain", test_name); - let mut node = TestStacksNode::new(false, 0x80000000, &full_test_name); let mut burn_node = TestBurnchainNode::new(); let mut miner_factory = TestMinerFactory::new(); let mut miner = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); + let mut node = TestStacksNode::new( + false, + 0x80000000, + &full_test_name, + vec![miner.origin_address().unwrap()], + ); + let first_snapshot = SortitionDB::get_first_block_snapshot(burn_node.sortdb.conn()).unwrap(); let mut fork = TestBurnchainFork::new( @@ -2315,7 +2436,6 @@ pub mod test { ) -> (StacksBlock, Vec), { let full_test_name = format!("{}-1_fork_2_miners_1_burnchain", test_name); - let mut node = TestStacksNode::new(false, 0x80000000, &full_test_name); let mut burn_node = TestBurnchainNode::new(); let mut miner_factory = TestMinerFactory::new(); let mut miner_1 = @@ -2323,6 +2443,16 @@ pub mod test { let mut miner_2 = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); + let mut node = TestStacksNode::new( + false, + 0x80000000, + &full_test_name, + vec![ + miner_1.origin_address().unwrap(), + miner_2.origin_address().unwrap(), + ], + ); + let mut sortition_winners = vec![]; let first_snapshot = @@ -2742,7 +2872,6 @@ pub mod test { ) -> (StacksBlock, Vec), { let full_test_name = format!("{}-2_forks_2_miners_1_burnchain", test_name); - let mut node = TestStacksNode::new(false, 0x80000000, &full_test_name); let mut burn_node = TestBurnchainNode::new(); let mut miner_factory = TestMinerFactory::new(); let mut miner_1 = @@ -2750,6 +2879,16 @@ pub mod test { let mut miner_2 = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); + let mut node = TestStacksNode::new( + false, + 0x80000000, + &full_test_name, + vec![ + miner_1.origin_address().unwrap(), + miner_2.origin_address().unwrap(), + ], + ); + let mut sortition_winners = vec![]; let first_snapshot = @@ -3322,13 +3461,22 @@ pub mod test { { let full_test_name = format!("{}-1_fork_2_miners_2_burnchain", test_name); let mut burn_node = TestBurnchainNode::new(); - let mut node = TestStacksNode::new(false, 0x80000000, &full_test_name); let mut miner_factory = TestMinerFactory::new(); let mut miner_1 = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); let mut miner_2 = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); + let mut node = TestStacksNode::new( + false, + 0x80000000, + &full_test_name, + vec![ + miner_1.origin_address().unwrap(), + miner_2.origin_address().unwrap(), + ], + ); + let first_snapshot = SortitionDB::get_first_block_snapshot(burn_node.sortdb.conn()).unwrap(); let mut fork_1 = TestBurnchainFork::new( @@ -3852,13 +4000,22 @@ pub mod test { { let full_test_name = format!("{}-2_forks_2_miner_2_burnchains", test_name); let mut burn_node = TestBurnchainNode::new(); - let mut node = TestStacksNode::new(false, 0x80000000, &full_test_name); let mut miner_factory = TestMinerFactory::new(); let mut miner_1 = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); let mut miner_2 = miner_factory.next_miner(&burn_node.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); + let mut node = TestStacksNode::new( + false, + 0x80000000, + &full_test_name, + vec![ + miner_1.origin_address().unwrap(), + miner_2.origin_address().unwrap(), + ], + ); + let first_snapshot = SortitionDB::get_first_block_snapshot(burn_node.sortdb.conn()).unwrap(); let mut fork_1 = TestBurnchainFork::new( @@ -4525,7 +4682,16 @@ pub mod test { let mut nodes = HashMap::new(); for (i, test_name) in test_names.iter().enumerate() { let rnd_test_name = format!("{}-replay_randomized", test_name); - let next_node = TestStacksNode::new(false, 0x80000000, &rnd_test_name); + let next_node = TestStacksNode::new( + false, + 0x80000000, + &rnd_test_name, + miner_trace + .miners + .iter() + .map(|ref miner| miner.origin_address().unwrap()) + .collect(), + ); nodes.insert(test_name, next_node); } @@ -4801,7 +4967,13 @@ pub mod test { tx_contract.chain_id = 0x80000000; tx_contract.auth.set_origin_nonce(miner.get_nonce()); - tx_contract.set_fee_rate(0); + + if miner.test_with_tx_fees { + tx_contract.set_tx_fee(123); + miner.spent_at_nonce.insert(miner.get_nonce(), 123); + } else { + tx_contract.set_tx_fee(0); + } let mut tx_signer = StacksTransactionSigner::new(&tx_contract); miner.sign_as_origin(&mut tx_signer); @@ -4833,7 +5005,13 @@ pub mod test { tx_contract_call.chain_id = 0x80000000; tx_contract_call.auth.set_origin_nonce(miner.get_nonce()); - tx_contract_call.set_fee_rate(0); + + if miner.test_with_tx_fees { + tx_contract_call.set_tx_fee(456); + miner.spent_at_nonce.insert(miner.get_nonce(), 456); + } else { + tx_contract_call.set_tx_fee(0); + } let mut tx_signer = StacksTransactionSigner::new(&tx_contract_call); miner.sign_as_origin(&mut tx_signer); @@ -4861,7 +5039,7 @@ pub mod test { tx_stx_transfer .auth .set_origin_nonce(nonce.unwrap_or(miner.get_nonce())); - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); let mut tx_signer = StacksTransactionSigner::new(&tx_stx_transfer); miner.sign_as_origin(&mut tx_signer); @@ -4901,6 +5079,10 @@ pub mod test { ); builder.force_mine_tx(clarity_tx, &tx1).unwrap(); + if miner.spent_at_nonce.get(&1).is_none() { + miner.spent_at_nonce.insert(1, 11111); + } + let tx2 = make_token_transfer( miner, burnchain_height, @@ -4911,6 +5093,10 @@ pub mod test { ); builder.force_mine_tx(clarity_tx, &tx2).unwrap(); + if miner.spent_at_nonce.get(&2).is_none() { + miner.spent_at_nonce.insert(2, 22222); + } + let tx3 = make_token_transfer( miner, burnchain_height, @@ -4934,6 +5120,7 @@ pub mod test { let stacks_block = builder.mine_anchored_block(clarity_tx); test_debug!("Produce anchored stacks block {} with invalid token transfers at burnchain height {} stacks height {}", stacks_block.block_hash(), burnchain_height, stacks_block.header.total_work.work); + (stacks_block, vec![]) } @@ -5725,7 +5912,7 @@ pub mod test { pub fn make_user_contract_publish( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, contract_name: &str, contract_content: &str, ) -> StacksTransaction { @@ -5734,13 +5921,13 @@ pub mod test { let payload = TransactionSmartContract { name, code_body }; - sign_standard_singlesig_tx(payload.into(), sender, nonce, fee_rate) + sign_standard_singlesig_tx(payload.into(), sender, nonce, tx_fee) } pub fn make_user_stacks_transfer( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, recipient: &PrincipalData, amount: u64, ) -> StacksTransaction { @@ -5749,39 +5936,39 @@ pub mod test { amount, TokenTransferMemo([0; 34]), ); - sign_standard_singlesig_tx(payload.into(), sender, nonce, fee_rate) + sign_standard_singlesig_tx(payload.into(), sender, nonce, tx_fee) } pub fn make_user_coinbase( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, ) -> StacksTransaction { let payload = TransactionPayload::Coinbase(CoinbasePayload([0; 32])); - sign_standard_singlesig_tx(payload.into(), sender, nonce, fee_rate) + sign_standard_singlesig_tx(payload.into(), sender, nonce, tx_fee) } pub fn make_user_poison_microblock( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, payload: TransactionPayload, ) -> StacksTransaction { - sign_standard_singlesig_tx(payload.into(), sender, nonce, fee_rate) + sign_standard_singlesig_tx(payload.into(), sender, nonce, tx_fee) } pub fn sign_standard_singlesig_tx( payload: TransactionPayload, sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, ) -> StacksTransaction { let mut spending_condition = TransactionSpendingCondition::new_singlesig_p2pkh( StacksPublicKey::from_private(sender), ) .expect("Failed to create p2pkh spending condition from public key."); spending_condition.set_nonce(nonce); - spending_condition.set_fee_rate(fee_rate); + spending_condition.set_tx_fee(tx_fee); let auth = TransactionAuth::Standard(spending_condition); let mut unsigned_tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload); diff --git a/src/chainstate/stacks/mod.rs b/src/chainstate/stacks/mod.rs index eef6f0047d..6d58b04e75 100644 --- a/src/chainstate/stacks/mod.rs +++ b/src/chainstate/stacks/mod.rs @@ -547,8 +547,8 @@ impl MultisigHashMode { pub struct MultisigSpendingCondition { pub hash_mode: MultisigHashMode, pub signer: Hash160, - pub nonce: u64, // nth authorization from this account - pub fee_rate: u64, // microSTX/compute rate offered by this account + pub nonce: u64, // nth authorization from this account + pub tx_fee: u64, // microSTX/compute rate offered by this account pub fields: Vec, pub signatures_required: u16, } @@ -557,8 +557,8 @@ pub struct MultisigSpendingCondition { pub struct SinglesigSpendingCondition { pub hash_mode: SinglesigHashMode, pub signer: Hash160, - pub nonce: u64, // nth authorization from this account - pub fee_rate: u64, // microSTX/compute rate offerred by this account + pub nonce: u64, // nth authorization from this account + pub tx_fee: u64, // microSTX/compute rate offerred by this account pub key_encoding: TransactionPublicKeyEncoding, pub signature: MessageSignature, } @@ -891,7 +891,7 @@ pub struct StacksBlockBuilder { bytes_so_far: u64, prev_microblock_header: StacksMicroblockHeader, miner_privkey: StacksPrivateKey, - miner_payouts: Option<(MinerReward, Vec)>, + miner_payouts: Option<(MinerReward, Vec, MinerReward)>, parent_microblock_hash: Option, miner_id: usize, } @@ -964,7 +964,7 @@ pub mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Uncompressed, nonce: 123, - fee_rate: 456, + tx_fee: 456, signature: MessageSignature::from_raw(&vec![0xff; 65]) }), TransactionSpendingCondition::Singlesig(SinglesigSpendingCondition { @@ -972,14 +972,14 @@ pub mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 234, - fee_rate: 567, + tx_fee: 567, signature: MessageSignature::from_raw(&vec![0xff; 65]) }), TransactionSpendingCondition::Multisig(MultisigSpendingCondition { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2SH, nonce: 345, - fee_rate: 678, + tx_fee: 678, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Uncompressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -991,7 +991,7 @@ pub mod test { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2SH, nonce: 456, - fee_rate: 789, + tx_fee: 789, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xfe; 65])), @@ -1004,14 +1004,14 @@ pub mod test { hash_mode: SinglesigHashMode::P2WPKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 567, - fee_rate: 890, + tx_fee: 890, signature: MessageSignature::from_raw(&vec![0xfe; 65]), }), TransactionSpendingCondition::Multisig(MultisigSpendingCondition { signer: Hash160([0x11; 20]), hash_mode: MultisigHashMode::P2WSH, nonce: 678, - fee_rate: 901, + tx_fee: 901, fields: vec![ TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xff; 65])), TransactionAuthField::Signature(TransactionPublicKeyEncoding::Compressed, MessageSignature::from_raw(&vec![0xfe; 65])), diff --git a/src/chainstate/stacks/transaction.rs b/src/chainstate/stacks/transaction.rs index 685fa0093e..025322cba1 100644 --- a/src/chainstate/stacks/transaction.rs +++ b/src/chainstate/stacks/transaction.rs @@ -558,13 +558,13 @@ impl StacksTransaction { } /// Get fee rate - pub fn get_fee_rate(&self) -> u64 { - self.auth.get_fee_rate() + pub fn get_tx_fee(&self) -> u64 { + self.auth.get_tx_fee() } /// Set fee rate - pub fn set_fee_rate(&mut self, fee_rate: u64) -> () { - self.auth.set_fee_rate(fee_rate); + pub fn set_tx_fee(&mut self, tx_fee: u64) -> () { + self.auth.set_tx_fee(tx_fee); } /// Get origin nonce @@ -650,7 +650,7 @@ impl StacksTransaction { let (next_sig, next_sighash) = TransactionSpendingCondition::next_signature( cur_sighash, auth_flag, - condition.fee_rate(), + condition.tx_fee(), condition.nonce(), privk, )?; @@ -1402,7 +1402,7 @@ mod test { // mess with transaction fee let mut corrupt_tx_fee = signed_tx.clone(); - corrupt_tx_fee.set_fee_rate(corrupt_tx_fee.get_fee_rate() + 1); + corrupt_tx_fee.set_tx_fee(corrupt_tx_fee.get_tx_fee() + 1); assert!(corrupt_tx_fee.txid() != signed_tx.txid()); // mess with anchor mode @@ -3401,7 +3401,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -3480,7 +3480,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -3494,7 +3494,7 @@ mod test { StacksPublicKey::from_private(&privk_diff_sponsor), ) .unwrap(); - sponsor_auth.set_fee_rate(456); + sponsor_auth.set_tx_fee(456); sponsor_auth.set_nonce(789); let mut tx_sponsor_signer = @@ -3504,7 +3504,7 @@ mod test { tx_sponsor_signer.sign_sponsor(&privk_diff_sponsor).unwrap(); // make comparable - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_sponsor_signer.get_tx().unwrap(); @@ -3518,7 +3518,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); assert_eq!(tx.chain_id, signed_tx.chain_id); - assert_eq!(tx.get_fee_rate(), 456); + assert_eq!(tx.get_tx_fee(), 456); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -3596,7 +3596,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -3677,7 +3677,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -3686,13 +3686,13 @@ mod test { // sponsor sets and pays fee after origin signs let mut origin_tx = tx_signer.get_tx_incomplete(); origin_tx.auth.set_sponsor(real_sponsor.clone()).unwrap(); - origin_tx.set_fee_rate(456); + origin_tx.set_tx_fee(456); origin_tx.set_sponsor_nonce(789).unwrap(); tx_signer.resume(&origin_tx); tx_signer.sign_sponsor(&privk_sponsored).unwrap(); - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_signer.get_tx().unwrap(); @@ -3706,7 +3706,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); assert_eq!(tx.chain_id, signed_tx.chain_id); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -3797,7 +3797,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -3902,7 +3902,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -3911,7 +3911,7 @@ mod test { // sponsor sets and pays fee after origin signs let mut origin_tx = tx_signer.get_tx_incomplete(); origin_tx.auth.set_sponsor(real_sponsor.clone()).unwrap(); - origin_tx.set_fee_rate(456); + origin_tx.set_tx_fee(456); origin_tx.set_sponsor_nonce(789).unwrap(); tx_signer.resume(&origin_tx); @@ -3919,7 +3919,7 @@ mod test { tx_signer.sign_sponsor(&privk_2).unwrap(); tx_signer.append_sponsor(&pubk_3).unwrap(); - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_signer.get_tx().unwrap(); @@ -3932,7 +3932,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); assert_eq!(tx.chain_id, signed_tx.chain_id); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4038,7 +4038,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); assert_eq!(tx.chain_id, signed_tx.chain_id); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4143,7 +4143,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -4152,7 +4152,7 @@ mod test { // sponsor sets and pays fee after origin signs let mut origin_tx = tx_signer.get_tx_incomplete(); origin_tx.auth.set_sponsor(real_sponsor.clone()).unwrap(); - origin_tx.set_fee_rate(456); + origin_tx.set_tx_fee(456); origin_tx.set_sponsor_nonce(789).unwrap(); tx_signer.resume(&origin_tx); @@ -4160,7 +4160,7 @@ mod test { tx_signer.sign_sponsor(&privk_2).unwrap(); tx_signer.append_sponsor(&pubk_3).unwrap(); - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_signer.get_tx().unwrap(); @@ -4172,7 +4172,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4274,7 +4274,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4379,7 +4379,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -4388,7 +4388,7 @@ mod test { // sponsor sets and pays fee after origin signs let mut origin_tx = tx_signer.get_tx_incomplete(); origin_tx.auth.set_sponsor(real_sponsor.clone()).unwrap(); - origin_tx.set_fee_rate(456); + origin_tx.set_tx_fee(456); origin_tx.set_sponsor_nonce(789).unwrap(); tx_signer.resume(&origin_tx); @@ -4396,7 +4396,7 @@ mod test { tx_signer.append_sponsor(&pubk_2).unwrap(); tx_signer.sign_sponsor(&privk_3).unwrap(); - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_signer.get_tx().unwrap(); @@ -4409,7 +4409,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); assert_eq!(tx.chain_id, signed_tx.chain_id); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4496,7 +4496,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4573,7 +4573,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -4582,13 +4582,13 @@ mod test { // sponsor sets and pays fee after origin signs let mut origin_tx = tx_signer.get_tx_incomplete(); origin_tx.auth.set_sponsor(real_sponsor.clone()).unwrap(); - origin_tx.set_fee_rate(456); + origin_tx.set_tx_fee(456); origin_tx.set_sponsor_nonce(789).unwrap(); tx_signer.resume(&origin_tx); tx_signer.sign_sponsor(&privk).unwrap(); - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_signer.get_tx().unwrap(); @@ -4601,7 +4601,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4690,7 +4690,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); @@ -4795,7 +4795,7 @@ mod test { assert_eq!(tx.auth().origin().num_signatures(), 0); assert_eq!(tx.auth().sponsor().unwrap().num_signatures(), 0); - tx.set_fee_rate(123); + tx.set_tx_fee(123); tx.set_sponsor_nonce(456).unwrap(); let mut tx_signer = StacksTransactionSigner::new(&tx); @@ -4804,7 +4804,7 @@ mod test { // sponsor sets and pays fee after origin signs let mut origin_tx = tx_signer.get_tx_incomplete(); origin_tx.auth.set_sponsor(real_sponsor.clone()).unwrap(); - origin_tx.set_fee_rate(456); + origin_tx.set_tx_fee(456); origin_tx.set_sponsor_nonce(789).unwrap(); tx_signer.resume(&origin_tx); @@ -4812,7 +4812,7 @@ mod test { tx_signer.sign_sponsor(&privk_2).unwrap(); tx_signer.append_sponsor(&pubk_3).unwrap(); - tx.set_fee_rate(456); + tx.set_tx_fee(456); tx.set_sponsor_nonce(789).unwrap(); let mut signed_tx = tx_signer.get_tx().unwrap(); @@ -4826,7 +4826,7 @@ mod test { // tx and signed_tx are otherwise equal assert_eq!(tx.version, signed_tx.version); assert_eq!(tx.chain_id, signed_tx.chain_id); - assert_eq!(tx.get_fee_rate(), signed_tx.get_fee_rate()); + assert_eq!(tx.get_tx_fee(), signed_tx.get_tx_fee()); assert_eq!(tx.get_origin_nonce(), signed_tx.get_origin_nonce()); assert_eq!(tx.get_sponsor_nonce(), signed_tx.get_sponsor_nonce()); assert_eq!(tx.anchor_mode, signed_tx.anchor_mode); diff --git a/src/core/mempool.rs b/src/core/mempool.rs index e78052a65d..b033c0d5f4 100644 --- a/src/core/mempool.rs +++ b/src/core/mempool.rs @@ -97,7 +97,7 @@ pub struct MemPoolTxInfo { pub struct MemPoolTxMetadata { pub txid: Txid, pub len: u64, - pub fee_rate: u64, + pub tx_fee: u64, pub estimated_fee: u64, // upper bound on what the fee to pay will be pub consensus_hash: ConsensusHash, pub block_header_hash: BlockHeaderHash, @@ -115,7 +115,7 @@ impl FromRow for MemPoolTxMetadata { let consensus_hash = ConsensusHash::from_column(row, "consensus_hash")?; let block_header_hash = BlockHeaderHash::from_column(row, "block_header_hash")?; let estimated_fee = u64::from_column(row, "estimated_fee")?; - let fee_rate = u64::from_column(row, "fee_rate")?; + let tx_fee = u64::from_column(row, "tx_fee")?; let height = u64::from_column(row, "height")?; let len = u64::from_column(row, "length")?; let ts = u64::from_column(row, "accept_time")?; @@ -127,7 +127,7 @@ impl FromRow for MemPoolTxMetadata { Ok(MemPoolTxMetadata { txid: txid, estimated_fee: estimated_fee, - fee_rate: fee_rate, + tx_fee: tx_fee, len: len, consensus_hash: consensus_hash, block_header_hash: block_header_hash, @@ -168,7 +168,7 @@ const MEMPOOL_SQL: &'static [&'static str] = &[ sponsor_address TEXT NOT NULL, sponsor_nonce INTEGER NOT NULL, estimated_fee INTEGER NOT NULL, - fee_rate INTEGER NOT NULL, + tx_fee INTEGER NOT NULL, length INTEGER NOT NULL, consensus_hash TEXT NOT NULL, block_header_hash TEXT NOT NULL, @@ -277,7 +277,7 @@ impl MemPoolTxInfo { let metadata = MemPoolTxMetadata { txid: txid, len: tx_data.len() as u64, - fee_rate: tx.get_fee_rate(), + tx_fee: tx.get_tx_fee(), estimated_fee: estimated_fee, consensus_hash: consensus_hash, block_header_hash: block_header_hash, @@ -709,7 +709,7 @@ impl MemPoolDB { sponsor_address, sponsor_nonce, estimated_fee, - fee_rate, + tx_fee, length, consensus_hash, block_header_hash, @@ -759,7 +759,7 @@ impl MemPoolDB { txid: Txid, tx_bytes: Vec, estimated_fee: u64, - fee_rate: u64, + tx_fee: u64, height: u64, origin_address: &StacksAddress, origin_nonce: u64, @@ -824,7 +824,7 @@ impl MemPoolDB { sponsor_address, sponsor_nonce, estimated_fee, - fee_rate, + tx_fee, length, consensus_hash, block_header_hash, @@ -840,7 +840,7 @@ impl MemPoolDB { &sponsor_address.to_string(), &u64_to_sql(sponsor_nonce)?, &u64_to_sql(estimated_fee)?, - &u64_to_sql(fee_rate)?, + &u64_to_sql(tx_fee)?, &u64_to_sql(length)?, consensus_hash, block_header_hash, @@ -941,7 +941,7 @@ impl MemPoolDB { .map_err(MemPoolRejection::SerializationFailure)?; let len = tx_data.len() as u64; - let fee_rate = tx.get_fee_rate(); + let tx_fee = tx.get_tx_fee(); let origin_address = tx.origin_address(); let origin_nonce = tx.get_origin_nonce(); let (sponsor_address, sponsor_nonce) = @@ -951,8 +951,8 @@ impl MemPoolDB { (origin_address.clone(), origin_nonce) }; - // TODO; estimate the true fee using Clarity analysis data. For now, just do fee_rate - let estimated_fee = fee_rate + // TODO; estimate the true fee using Clarity analysis data. For now, just do tx_fee + let estimated_fee = tx_fee .checked_mul(len) .ok_or(MemPoolRejection::Other("Fee numeric overflow".to_string()))?; @@ -971,7 +971,7 @@ impl MemPoolDB { txid, tx_data, estimated_fee, - fee_rate, + tx_fee, height, &origin_address, origin_nonce, @@ -1189,14 +1189,14 @@ mod tests { bytes: Hash160::from_data(&[1; 32]), }; - tx.set_fee_rate(123); + tx.set_tx_fee(123); // test insert let txid = tx.txid(); let tx_bytes = tx.serialize_to_vec(); let len = tx_bytes.len() as u64; - let estimated_fee = tx.get_fee_rate() * len; //TODO: use clarity analysis data to make this estimate + let estimated_fee = tx.get_tx_fee() * len; //TODO: use clarity analysis data to make this estimate let height = 100; let origin_nonce = tx.get_origin_nonce(); @@ -1215,7 +1215,7 @@ mod tests { txid, tx_bytes, estimated_fee, - tx.get_fee_rate(), + tx.get_tx_fee(), height, &origin_address, origin_nonce, @@ -1229,11 +1229,11 @@ mod tests { let prior_txid = txid.clone(); // now, let's try inserting again, with a lower fee, but at a different block hash - tx.set_fee_rate(100); + tx.set_tx_fee(100); let txid = tx.txid(); let tx_bytes = tx.serialize_to_vec(); let len = tx_bytes.len() as u64; - let estimated_fee = tx.get_fee_rate() * len; //TODO: use clarity analysis data to make this estimate + let estimated_fee = tx.get_tx_fee() * len; //TODO: use clarity analysis data to make this estimate let height = 100; let err_resp = MemPoolDB::try_add_tx( @@ -1244,7 +1244,7 @@ mod tests { txid, tx_bytes, estimated_fee, - tx.get_fee_rate(), + tx.get_tx_fee(), height, &origin_address, origin_nonce, @@ -1290,7 +1290,7 @@ mod tests { bytes: Hash160::from_data(&(i + 1).to_be_bytes()), }; - tx.set_fee_rate(123); + tx.set_tx_fee(123); // test insert let txid = tx.txid(); @@ -1299,7 +1299,7 @@ mod tests { let expected_tx = tx.clone(); let len = tx_bytes.len() as u64; - let estimated_fee = tx.get_fee_rate() * len; //TODO: use clarity analysis data to make this estimate + let estimated_fee = tx.get_tx_fee() * len; //TODO: use clarity analysis data to make this estimate let height = 100; let origin_nonce = tx.get_origin_nonce(); @@ -1318,7 +1318,7 @@ mod tests { txid, tx_bytes, estimated_fee, - tx.get_fee_rate(), + tx.get_tx_fee(), height, &origin_address, origin_nonce, @@ -1336,7 +1336,7 @@ mod tests { assert_eq!(tx_info.tx, expected_tx); assert_eq!(tx_info.metadata.len, len); assert_eq!(tx_info.metadata.estimated_fee, estimated_fee); - assert_eq!(tx_info.metadata.fee_rate, 123); + assert_eq!(tx_info.metadata.tx_fee, 123); assert_eq!(tx_info.metadata.origin_address, origin_address); assert_eq!(tx_info.metadata.origin_nonce, origin_nonce); assert_eq!(tx_info.metadata.sponsor_address, sponsor_address); @@ -1351,14 +1351,14 @@ mod tests { // test replace-by-fee with a higher fee let old_txid = txid; - tx.set_fee_rate(124); + tx.set_tx_fee(124); assert!(txid != tx.txid()); let txid = tx.txid(); let mut tx_bytes = vec![]; tx.consensus_serialize(&mut tx_bytes).unwrap(); let expected_tx = tx.clone(); - let estimated_fee = tx.get_fee_rate() * len; // TODO: use clarity analysis data to make this estimate + let estimated_fee = tx.get_tx_fee() * len; // TODO: use clarity analysis data to make this estimate assert!(!MemPoolDB::db_has_tx(&mempool_tx, &txid).unwrap()); @@ -1380,7 +1380,7 @@ mod tests { txid, tx_bytes, estimated_fee, - tx.get_fee_rate(), + tx.get_tx_fee(), height, &origin_address, origin_nonce, @@ -1413,7 +1413,7 @@ mod tests { assert_eq!(tx_info.tx, expected_tx); assert_eq!(tx_info.metadata.len, len); assert_eq!(tx_info.metadata.estimated_fee, estimated_fee); - assert_eq!(tx_info.metadata.fee_rate, 124); + assert_eq!(tx_info.metadata.tx_fee, 124); assert_eq!(tx_info.metadata.origin_address, origin_address); assert_eq!(tx_info.metadata.origin_nonce, origin_nonce); assert_eq!(tx_info.metadata.sponsor_address, sponsor_address); @@ -1428,14 +1428,14 @@ mod tests { // test replace-by-fee with a lower fee let old_txid = txid; - tx.set_fee_rate(122); + tx.set_tx_fee(122); assert!(txid != tx.txid()); let txid = tx.txid(); let mut tx_bytes = vec![]; tx.consensus_serialize(&mut tx_bytes).unwrap(); let _expected_tx = tx.clone(); - let estimated_fee = tx.get_fee_rate() * len; // TODO: use clarity analysis metadata to make this estimate + let estimated_fee = tx.get_tx_fee() * len; // TODO: use clarity analysis metadata to make this estimate assert!(match MemPoolDB::try_add_tx( &mut mempool_tx, @@ -1445,7 +1445,7 @@ mod tests { txid, tx_bytes, estimated_fee, - tx.get_fee_rate(), + tx.get_tx_fee(), height, &origin_address, origin_nonce, diff --git a/src/core/mod.rs b/src/core/mod.rs index 58d148e9a4..b8a4200d2b 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -67,8 +67,8 @@ pub const MICROSTACKS_PER_STACKS: u32 = 1_000_000; pub const POX_SUNSET_START: u64 = (FIRST_BURNCHAIN_BLOCK_HEIGHT as u64) + 100_000; pub const POX_SUNSET_END: u64 = POX_SUNSET_START + 400_000; -pub const POX_PREPARE_WINDOW_LENGTH: u32 = 240; -pub const POX_REWARD_CYCLE_LENGTH: u32 = 2000; +pub const POX_PREPARE_WINDOW_LENGTH: u32 = 100; +pub const POX_REWARD_CYCLE_LENGTH: u32 = 2100; /// The maximum amount that PoX rewards can be scaled by. /// That is, if participation is very low, rewards are: /// POX_MAXIMAL_SCALING x (rewards with 100% participation) diff --git a/src/net/chat.rs b/src/net/chat.rs index f1cf864a54..e68e37101b 100644 --- a/src/net/chat.rs +++ b/src/net/chat.rs @@ -2498,6 +2498,7 @@ mod test { &prev_snapshot, &next_snapshot, &vec![], + &vec![], None, None, None, diff --git a/src/net/http.rs b/src/net/http.rs index 4fc9990544..879a484887 100644 --- a/src/net/http.rs +++ b/src/net/http.rs @@ -4950,7 +4950,7 @@ mod test { ); tx_stx_transfer.chain_id = 0x80000000; tx_stx_transfer.post_condition_mode = TransactionPostConditionMode::Allow; - tx_stx_transfer.set_fee_rate(0); + tx_stx_transfer.set_tx_fee(0); tx_stx_transfer } diff --git a/src/net/mod.rs b/src/net/mod.rs index 36344bca28..469b77725b 100644 --- a/src/net/mod.rs +++ b/src/net/mod.rs @@ -2102,12 +2102,13 @@ pub mod test { burnchain.pox_constants = PoxConstants::new(5, 3, 3, 25, 5, u64::max_value(), u64::max_value()); - let spending_account = TestMinerFactory::new().next_miner( + let mut spending_account = TestMinerFactory::new().next_miner( &burnchain, 1, 1, AddressHashMode::SerializeP2PKH, ); + spending_account.test_with_tx_fees = false; // manually set transaction fees TestPeerConfig { network_id: 0x80000000, @@ -2239,9 +2240,13 @@ pub mod test { let mut miner = miner_factory.next_miner(&config.burnchain, 1, 1, AddressHashMode::SerializeP2PKH); + // manually set fees + miner.test_with_tx_fees = false; + let mut burnchain = get_burnchain(&test_path, None); burnchain.first_block_height = config.burnchain.first_block_height; burnchain.first_block_hash = config.burnchain.first_block_hash; + burnchain.pox_constants = config.burnchain.pox_constants; config.burnchain = burnchain.clone(); @@ -2309,7 +2314,7 @@ pub mod test { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Uncompressed, nonce: 0, - fee_rate: 0, + tx_fee: 0, signature: MessageSignature::empty(), }), ); @@ -3061,6 +3066,12 @@ pub mod test { Some(info) => info.recipients.into_iter().map(|x| x.0).collect(), None => vec![], }; + test_debug!( + "Block commit at height {} has {} recipients: {:?}", + block_commit_op.block_height, + block_commit_op.commit_outs.len(), + &block_commit_op.commit_outs + ); } Err(e) => { panic!("Failure fetching recipient set: {:?}", e); diff --git a/src/net/relay.rs b/src/net/relay.rs index d4f5468d8a..be41ec51bb 100644 --- a/src/net/relay.rs +++ b/src/net/relay.rs @@ -2567,7 +2567,7 @@ mod test { tx_contract.chain_id = 0x80000000; tx_contract.auth.set_origin_nonce(cur_nonce); - tx_contract.set_fee_rate(MINIMUM_TX_FEE_RATE_PER_BYTE * 500); + tx_contract.set_tx_fee(MINIMUM_TX_FEE_RATE_PER_BYTE * 500); let mut tx_signer = StacksTransactionSigner::new(&tx_contract); spending_account.sign_as_origin(&mut tx_signer); diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 428c6de183..a3af8b1ef5 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -1127,7 +1127,10 @@ impl ConversationHttp { chainstate.maybe_read_only_clarity_tx(&sortdb.index_conn(), tip, |clarity_tx| { let cost_track = clarity_tx .with_clarity_db_readonly(|clarity_db| { - LimitedCostTracker::new(options.read_only_call_limit.clone(), clarity_db) + LimitedCostTracker::new_mid_block( + options.read_only_call_limit.clone(), + clarity_db, + ) }) .map_err(|_| { ClarityRuntimeError::from(InterpreterError::CostContractLoadFailure) @@ -2691,7 +2694,7 @@ mod test { tx_contract.chain_id = 0x80000000; tx_contract.auth.set_origin_nonce(1); - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut tx_signer = StacksTransactionSigner::new(&tx_contract); tx_signer.sign_origin(&privk1).unwrap(); @@ -2707,7 +2710,7 @@ mod test { tx_cc.chain_id = 0x80000000; tx_cc.auth.set_origin_nonce(2); - tx_cc.set_fee_rate(123); + tx_cc.set_tx_fee(123); let mut tx_signer = StacksTransactionSigner::new(&tx_cc); tx_signer.sign_origin(&privk1).unwrap(); @@ -2732,7 +2735,7 @@ mod test { tx_unconfirmed_contract.chain_id = 0x80000000; tx_unconfirmed_contract.auth.set_origin_nonce(3); - tx_unconfirmed_contract.set_fee_rate(0); + tx_unconfirmed_contract.set_tx_fee(0); let mut tx_signer = StacksTransactionSigner::new(&tx_unconfirmed_contract); tx_signer.sign_origin(&privk1).unwrap(); diff --git a/src/net/server.rs b/src/net/server.rs index 4ef814cf2a..a71301d3b5 100644 --- a/src/net/server.rs +++ b/src/net/server.rs @@ -1216,7 +1216,7 @@ mod test { ); tx_contract.chain_id = chainstate.config().chain_id; - tx_contract.set_fee_rate(0); + tx_contract.set_tx_fee(0); let mut signer = StacksTransactionSigner::new(&tx_contract); signer.sign_origin(&privk_origin).unwrap(); diff --git a/src/util/macros.rs b/src/util/macros.rs index 1412ba5adf..61fc85b9dd 100644 --- a/src/util/macros.rs +++ b/src/util/macros.rs @@ -27,7 +27,7 @@ pub fn is_big_endian() -> bool { macro_rules! define_named_enum { ($Name:ident { $($Variant:ident($VarName:literal),)* }) => { - #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] + #[derive(::serde::Serialize, ::serde::Deserialize, Debug, Hash, PartialEq, Eq, Copy, Clone)] pub enum $Name { $($Variant),*, } diff --git a/src/util/uint.rs b/src/util/uint.rs index 10a4ceb97a..776b69dfe7 100644 --- a/src/util/uint.rs +++ b/src/util/uint.rs @@ -49,6 +49,7 @@ pub trait BitArray { macro_rules! construct_uint { ($name:ident, $n_words:expr) => { /// Little-endian large integer type + #[derive(Serialize, Deserialize)] #[repr(C)] pub struct $name(pub [u64; $n_words]); impl_array_newtype!($name, u64, $n_words); diff --git a/src/vm/analysis/analysis_db.rs b/src/vm/analysis/analysis_db.rs index 1cba6c0d51..04833d07e6 100644 --- a/src/vm/analysis/analysis_db.rs +++ b/src/vm/analysis/analysis_db.rs @@ -76,7 +76,7 @@ impl<'a> AnalysisDatabase<'a> { self.store.rollback(); } - fn storage_key() -> &'static str { + pub fn storage_key() -> &'static str { "analysis" } diff --git a/src/vm/analysis/arithmetic_checker/mod.rs b/src/vm/analysis/arithmetic_checker/mod.rs new file mode 100644 index 0000000000..4c4d9b7b72 --- /dev/null +++ b/src/vm/analysis/arithmetic_checker/mod.rs @@ -0,0 +1,270 @@ +// Copyright (C) 2013-2020 Blocstack PBC, a public benefit corporation +// Copyright (C) 2020 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use vm::analysis::types::{AnalysisPass, ContractAnalysis}; +use vm::functions::define::{DefineFunctions, DefineFunctionsParsed}; +use vm::functions::tuples; +use vm::functions::NativeFunctions; +use vm::representations::SymbolicExpressionType::{ + Atom, AtomValue, Field, List, LiteralValue, TraitReference, +}; +use vm::representations::{ClarityName, SymbolicExpression, SymbolicExpressionType}; +use vm::types::{parse_name_type_pairs, PrincipalData, TupleTypeSignature, TypeSignature, Value}; + +use std::collections::HashMap; +use vm::variables::NativeVariables; + +pub use super::errors::{ + check_argument_count, check_arguments_at_least, CheckError, CheckErrors, CheckResult, +}; +use super::AnalysisDatabase; + +#[cfg(test)] +mod tests; + +/// +/// A static-analysis pass that checks whether or not +/// a proposed cost-function defining contract is allowable. +/// Cost-function defining contracts may not use contract-call, +/// any database operations, traits, or iterating operations (e.g., list +/// operations) +/// +pub struct ArithmeticOnlyChecker(); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Error { + DefineTypeForbidden(DefineFunctions), + VariableForbidden(NativeVariables), + FunctionNotPermitted(NativeFunctions), + TraitReferencesForbidden, + UnexpectedContractStructure, +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl ArithmeticOnlyChecker { + pub fn check_contract_cost_eligible(contract_analysis: &mut ContractAnalysis) { + let is_eligible = ArithmeticOnlyChecker::run(contract_analysis).is_ok(); + contract_analysis.is_cost_contract_eligible = is_eligible; + } + + pub fn run(contract_analysis: &ContractAnalysis) -> Result<(), Error> { + let checker = ArithmeticOnlyChecker(); + for exp in contract_analysis.expressions.iter() { + checker.check_top_levels(&exp)?; + } + + Ok(()) + } + + fn check_define_function( + &self, + _signature: &[SymbolicExpression], + body: &SymbolicExpression, + ) -> Result<(), Error> { + self.check_expression(body) + } + + fn check_top_levels(&self, expr: &SymbolicExpression) -> Result<(), Error> { + use vm::functions::define::DefineFunctionsParsed::*; + if let Some(define_type) = DefineFunctionsParsed::try_parse(expr) + .map_err(|_| Error::UnexpectedContractStructure)? + { + match define_type { + // The _arguments_ to constant defines must be checked to ensure that + // any _evaluated arguments_ supplied to them are valid. + Constant { value, .. } => self.check_expression(value), + PrivateFunction { signature, body } => self.check_define_function(signature, body), + ReadOnlyFunction { signature, body } => self.check_define_function(signature, body), + PersistedVariable { .. } => Err(Error::DefineTypeForbidden( + DefineFunctions::PersistedVariable, + )), + BoundedFungibleToken { .. } => { + Err(Error::DefineTypeForbidden(DefineFunctions::FungibleToken)) + } + PublicFunction { .. } => { + Err(Error::DefineTypeForbidden(DefineFunctions::PublicFunction)) + } + Map { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::Map)), + NonFungibleToken { .. } => Err(Error::DefineTypeForbidden( + DefineFunctions::NonFungibleToken, + )), + UnboundedFungibleToken { .. } => { + Err(Error::DefineTypeForbidden(DefineFunctions::FungibleToken)) + } + Trait { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::Trait)), + UseTrait { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::UseTrait)), + ImplTrait { .. } => Err(Error::DefineTypeForbidden(DefineFunctions::ImplTrait)), + } + } else { + self.check_expression(expr) + } + } + + fn check_expression(&self, expr: &SymbolicExpression) -> Result<(), Error> { + match expr.expr { + AtomValue(_) | LiteralValue(_) => { + // values and literals are always allowed + Ok(()) + } + Atom(ref variable) => self.check_variables_allowed(variable), + Field(_) | TraitReference(_, _) => Err(Error::TraitReferencesForbidden), + List(ref expression) => self.check_function_application(expression), + } + } + + fn check_variables_allowed(&self, var_name: &ClarityName) -> Result<(), Error> { + use vm::variables::NativeVariables::*; + if let Some(native_var) = NativeVariables::lookup_by_name(var_name) { + match native_var { + ContractCaller | TxSender | TotalLiquidMicroSTX | BlockHeight | BurnBlockHeight + | Regtest => Err(Error::VariableForbidden(native_var)), + NativeNone | NativeTrue | NativeFalse => Ok(()), + } + } else { + Ok(()) + } + } + + fn try_native_function_check( + &self, + function: &str, + args: &[SymbolicExpression], + ) -> Option> { + NativeFunctions::lookup_by_name(function) + .map(|function| self.check_native_function(function, args)) + } + + fn check_native_function( + &self, + function: NativeFunctions, + args: &[SymbolicExpression], + ) -> Result<(), Error> { + use vm::functions::NativeFunctions::*; + match function { + FetchVar | GetBlockInfo | GetTokenBalance | GetAssetOwner | FetchEntry | SetEntry + | DeleteEntry | InsertEntry | SetVar | MintAsset | MintToken | TransferAsset + | TransferToken | ContractCall | StxTransfer | StxBurn | AtBlock | GetStxBalance + | GetTokenSupply | BurnToken | BurnAsset => { + return Err(Error::FunctionNotPermitted(function)); + } + Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | Print + | AsContract | ElementAt | IndexOf | Map | Filter | Fold => { + return Err(Error::FunctionNotPermitted(function)); + } + Sha512 | Sha512Trunc256 | Secp256k1Recover | Secp256k1Verify | Hash160 | Sha256 + | Keccak256 => { + return Err(Error::FunctionNotPermitted(function)); + } + Add | Subtract | Divide | Multiply | CmpGeq | CmpLeq | CmpLess | CmpGreater + | Modulo | Power | Sqrti | Log2 | BitwiseXOR | And | Or | Not | Equals | If + | ConsSome | ConsOkay | ConsError | DefaultTo | UnwrapRet | UnwrapErrRet | IsOkay + | IsNone | Asserts | Unwrap | UnwrapErr | IsErr | IsSome | TryRet | ToUInt | ToInt + | Len | Begin | TupleMerge => self.check_all(args), + // we need to treat all the remaining functions specially, because these + // do not eval all of their arguments (rather, one or more of their arguments + // is a name) + TupleGet => { + // these functions use a name in the first argument + check_argument_count(2, args).map_err(|_| Error::UnexpectedContractStructure)?; + self.check_all(&args[1..]) + } + Match => { + if !(args.len() == 4 || args.len() == 5) { + return Err(Error::UnexpectedContractStructure); + } + // check the match input + self.check_expression(&args[0])?; + // check the 'ok' branch + self.check_expression(&args[2])?; + // check the 'err' branch + if args.len() == 4 { + self.check_expression(&args[3]) + } else { + self.check_expression(&args[4]) + } + } + Let => { + check_arguments_at_least(2, args) + .map_err(|_| Error::UnexpectedContractStructure)?; + + let binding_list = args[0] + .match_list() + .ok_or(Error::UnexpectedContractStructure)?; + + for pair in binding_list.iter() { + let pair_expression = pair + .match_list() + .ok_or(Error::UnexpectedContractStructure)?; + if pair_expression.len() != 2 { + return Err(Error::UnexpectedContractStructure); + } + + self.check_expression(&pair_expression[1])?; + } + + self.check_all(&args[1..args.len()]) + } + TupleCons => { + for pair in args.iter() { + let pair_expression = pair + .match_list() + .ok_or(Error::UnexpectedContractStructure)?; + if pair_expression.len() != 2 { + return Err(Error::UnexpectedContractStructure); + } + + self.check_expression(&pair_expression[1])?; + } + Ok(()) + } + } + } + + fn check_all(&self, expressions: &[SymbolicExpression]) -> Result<(), Error> { + for expr in expressions.iter() { + self.check_expression(expr)?; + } + Ok(()) + } + + fn check_function_application(&self, expression: &[SymbolicExpression]) -> Result<(), Error> { + let (function_name, args) = expression + .split_first() + .ok_or(Error::UnexpectedContractStructure)?; + + let function_name = function_name + .match_atom() + .ok_or(Error::UnexpectedContractStructure)?; + + if let Some(result) = self.try_native_function_check(function_name, args) { + result + } else { + // non-native function invocations are always okay, just check the args! + self.check_all(args) + } + } +} diff --git a/src/vm/analysis/arithmetic_checker/tests.rs b/src/vm/analysis/arithmetic_checker/tests.rs new file mode 100644 index 0000000000..2dbb1f7307 --- /dev/null +++ b/src/vm/analysis/arithmetic_checker/tests.rs @@ -0,0 +1,250 @@ +// Copyright (C) 2013-2020 Blocstack PBC, a public benefit corporation +// Copyright (C) 2020 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use chainstate::stacks::boot::BOOT_CODE_COSTS; +use vm::analysis::{ + arithmetic_checker::ArithmeticOnlyChecker, arithmetic_checker::Error, + arithmetic_checker::Error::*, mem_type_check, ContractAnalysis, +}; +use vm::ast::parse; +use vm::costs::LimitedCostTracker; +use vm::database::MemoryBackingStore; +use vm::functions::define::DefineFunctions; +use vm::functions::NativeFunctions; +use vm::types::QualifiedContractIdentifier; +use vm::variables::NativeVariables; + +fn arithmetic_check(contract: &str) -> Result<(), Error> { + let contract_identifier = QualifiedContractIdentifier::transient(); + let expressions = parse(&contract_identifier, contract).unwrap(); + + let analysis = ContractAnalysis::new( + contract_identifier, + expressions, + LimitedCostTracker::new_free(), + ); + + ArithmeticOnlyChecker::run(&analysis) +} + +fn check_good(contract: &str) { + let analysis = mem_type_check(contract).unwrap().1; + ArithmeticOnlyChecker::run(&analysis).expect("Should pass arithmetic checks"); +} + +#[test] +fn test_boot_definitions() { + check_good(BOOT_CODE_COSTS); +} + +#[test] +fn test_bad_defines() { + let tests = [ + ("(define-public (foo) (ok 1))", DefineTypeForbidden(DefineFunctions::PublicFunction)), + ("(define-map foo-map ((a uint)) ((b uint))) (define-private (foo) (map-get? foo-map {a: u1}))", DefineTypeForbidden(DefineFunctions::Map)), + ("(define-data-var foo-var uint u1) (define-private (foo) (var-get foo-var))", DefineTypeForbidden(DefineFunctions::PersistedVariable)), + ("(define-fungible-token tokaroos u500)", DefineTypeForbidden(DefineFunctions::FungibleToken)), + ("(define-fungible-token tokaroos)", DefineTypeForbidden(DefineFunctions::FungibleToken)), + ("(define-non-fungible-token tokaroos uint)", DefineTypeForbidden(DefineFunctions::NonFungibleToken)), + ("(define-trait foo-trait ((foo (uint)) (response uint uint)))", DefineTypeForbidden(DefineFunctions::Trait)), + ]; + + for (contract, error) in tests.iter() { + assert_eq!( + arithmetic_check(contract), + Err(error.clone()), + "Check contract:\n {}", + contract + ); + } +} + +#[test] +fn test_variables() { + let tests = [ + ( + "(define-private (foo) burn-block-height)", + VariableForbidden(NativeVariables::BurnBlockHeight), + ), + ( + "(define-private (foo) block-height)", + VariableForbidden(NativeVariables::BlockHeight), + ), + ( + "(define-private (foo) tx-sender)", + VariableForbidden(NativeVariables::TxSender), + ), + ( + "(define-private (foo) contract-caller)", + VariableForbidden(NativeVariables::ContractCaller), + ), + ( + "(define-private (foo) is-in-regtest)", + VariableForbidden(NativeVariables::Regtest), + ), + ( + "(define-private (foo) stx-liquid-supply)", + VariableForbidden(NativeVariables::TotalLiquidMicroSTX), + ), + ]; + + for (contract, error) in tests.iter() { + assert_eq!( + arithmetic_check(contract), + Err(error.clone()), + "Check contract:\n {}", + contract + ); + } + + let tests = [ + "(define-private (foo) (begin true false none))", + "(define-private (foo) 1)", + ]; + + for contract in tests.iter() { + check_good(contract); + } +} + +#[test] +fn test_functions() { + let bad_tests = [ + ("(define-private (foo) (at-block 0x0202020202020202020202020202020202020202020202020202020202020202 (+ 1 2)))", + FunctionNotPermitted(NativeFunctions::AtBlock)), + ("(define-private (foo) (map-get? foo-map {a: u1}))", + FunctionNotPermitted(NativeFunctions::FetchEntry)), + ("(define-private (foo) (map-delete foo-map {a: u1}))", + FunctionNotPermitted(NativeFunctions::DeleteEntry)), + ("(define-private (foo) (map-set foo-map {a: u1} {b: u2}))", + FunctionNotPermitted(NativeFunctions::SetEntry)), + ("(define-private (foo) (map-insert foo-map {a: u1} {b: u2}))", + FunctionNotPermitted(NativeFunctions::InsertEntry)), + ("(define-private (foo) (var-get foo-var))", + FunctionNotPermitted(NativeFunctions::FetchVar)), + ("(define-private (foo) (var-set foo-var u2))", + FunctionNotPermitted(NativeFunctions::SetVar)), + ("(define-private (foo (a principal)) (ft-get-balance tokaroos a))", + FunctionNotPermitted(NativeFunctions::GetTokenBalance)), + ("(define-private (foo (a principal)) + (ft-transfer? stackaroo u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF))", + FunctionNotPermitted(NativeFunctions::TransferToken)), + ("(define-private (foo (a principal)) + (ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))", + FunctionNotPermitted(NativeFunctions::MintToken)), + ("(define-private (foo (a principal)) + (nft-mint? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))", + FunctionNotPermitted(NativeFunctions::MintAsset)), + ("(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", + FunctionNotPermitted(NativeFunctions::TransferAsset)), + ("(nft-get-owner? stackaroo \"Roo\")", + FunctionNotPermitted(NativeFunctions::GetAssetOwner)), + ("(get-block-info? id-header-hash 0)", + FunctionNotPermitted(NativeFunctions::GetBlockInfo)), + ("(define-private (foo) (contract-call? .bar outer-call))", + FunctionNotPermitted(NativeFunctions::ContractCall)), + ("(stx-get-balance 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", + FunctionNotPermitted(NativeFunctions::GetStxBalance)), + ("(stx-burn? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", + FunctionNotPermitted(NativeFunctions::StxBurn)), + ("(stx-transfer? u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)", + FunctionNotPermitted(NativeFunctions::StxTransfer)), + ("(define-private (foo (a (list 3 uint))) + (map log2 a))", + FunctionNotPermitted(NativeFunctions::Map)), + ("(define-private (foo (a (list 3 (optional uint)))) + (filter is-none a))", + FunctionNotPermitted(NativeFunctions::Filter)), + ("(define-private (foo (a (list 3 uint))) + (append a u4))", + FunctionNotPermitted(NativeFunctions::Append)), + ("(define-private (foo (a (list 3 uint))) + (concat a a))", + FunctionNotPermitted(NativeFunctions::Concat)), + ("(define-private (foo (a (list 3 uint))) + (as-max-len? a u4))", + FunctionNotPermitted(NativeFunctions::AsMaxLen)), + ("(define-private (foo) (print 10))", + FunctionNotPermitted(NativeFunctions::Print)), + ("(define-private (foo) (list 3 4 10))", + FunctionNotPermitted(NativeFunctions::ListCons)), + ("(define-private (foo) (keccak256 0))", + FunctionNotPermitted(NativeFunctions::Keccak256)), + ("(define-private (foo) (hash160 0))", + FunctionNotPermitted(NativeFunctions::Hash160)), + ("(define-private (foo) (secp256k1-recover? 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a1301))", + FunctionNotPermitted(NativeFunctions::Secp256k1Recover)), + ("(define-private (foo) (secp256k1-verify 0xde5b9eb9e7c5592930eb2e30a01369c36586d872082ed8181ee83d2a0ec20f04 + 0x8738487ebe69b93d8e51583be8eee50bb4213fc49c767d329632730cc193b873554428fc936ca3569afc15f1c9365f6591d6251a89fee9c9ac661116824d3a13 + 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110))", + FunctionNotPermitted(NativeFunctions::Secp256k1Verify)), + ("(define-private (foo) (sha256 0))", + FunctionNotPermitted(NativeFunctions::Sha256)), + ("(define-private (foo) (sha512 0))", + FunctionNotPermitted(NativeFunctions::Sha512)), + ("(define-private (foo) (sha512/256 0))", + FunctionNotPermitted(NativeFunctions::Sha512Trunc256)), + + ]; + + for (contract, error) in bad_tests.iter() { + eprintln!("{}", contract); + assert_eq!( + arithmetic_check(contract), + Err(error.clone()), + "Check contract:\n {}", + contract + ); + } + + let good_tests = [ + "(match (if (is-eq 0 1) (ok 1) (err 2)) + ok-val (+ 1 ok-val) + err-val (+ 2 err-val))", + "(match (if (is-eq 0 1) (some 1) none) + ok-val (+ 1 ok-val) + 2)", + "(get a { a: (+ 9 1), b: (if (> 2 3) (* 4 4) (/ 4 2)) })", + "(define-private (foo) + (let ((a (+ 3 2 3))) (log2 a)))", + "(define-private (foo) + (let ((a (+ 32 3 4)) (b (- 32 1)) + (c (if (and (< 3 2) (<= 3 2) (>= 4 5) (or (is-eq (mod 5 4) 0) (not (> 3 2)))) + (pow u2 (log2 (sqrti u100000))) + (xor u120 u280))) + (d (default-to u0 (some u2)))) + (begin (unwrap! (some u3) u1) + (unwrap-err! (err u5) u4) + (asserts! true u4) + (unwrap-panic (some u3)) + (unwrap-err-panic (err u5)))))", + "(define-private (foo) (to-int (to-uint 34))) + (define-private (bar) (foo))", + "(define-private (foo) (begin + (is-some (some 4)) + (is-none (some 4)) + (is-ok (ok 4)) + (is-err (ok 5)) + (try! (some 4)) + (some 5))) + (define-read-only (bar) (foo))", + ]; + + for contract in good_tests.iter() { + eprintln!("{}", contract); + check_good(contract); + } +} diff --git a/src/vm/analysis/contract_interface_builder/mod.rs b/src/vm/analysis/contract_interface_builder/mod.rs index 854d3bae97..f97ae0b62f 100644 --- a/src/vm/analysis/contract_interface_builder/mod.rs +++ b/src/vm/analysis/contract_interface_builder/mod.rs @@ -38,6 +38,7 @@ pub fn build_contract_interface(contract_analysis: &ContractAnalysis) -> Contrac type_map: _, cost_track: _, contract_interface: _, + is_cost_contract_eligible: _, } = contract_analysis; contract_interface diff --git a/src/vm/analysis/errors.rs b/src/vm/analysis/errors.rs index d900524a06..2eda5a7d0a 100644 --- a/src/vm/analysis/errors.rs +++ b/src/vm/analysis/errors.rs @@ -88,6 +88,7 @@ pub enum CheckErrors { BadTransferFTArguments, BadTransferNFTArguments, BadMintFTArguments, + BadBurnFTArguments, // tuples BadTupleFieldName, @@ -348,7 +349,8 @@ impl DiagnosableError for CheckErrors { CheckErrors::BadTransferSTXArguments => format!("STX transfer expects an int amount, from principal, to principal"), CheckErrors::BadTransferFTArguments => format!("transfer expects an int amount, from principal, to principal"), CheckErrors::BadTransferNFTArguments => format!("transfer expects an asset, from principal, to principal"), - CheckErrors::BadMintFTArguments => format!("mint expects an int amount and from principal"), + CheckErrors::BadMintFTArguments => format!("mint expects a uint amount and from principal"), + CheckErrors::BadBurnFTArguments => format!("burn expects a uint amount and from principal"), CheckErrors::BadMapName => format!("invalid map name"), CheckErrors::NoSuchMap(map_name) => format!("use of unresolved map '{}'", map_name), CheckErrors::DefineFunctionBadSignature => format!("invalid function definition"), diff --git a/src/vm/analysis/mod.rs b/src/vm/analysis/mod.rs index 02f8a90f12..0334c87f85 100644 --- a/src/vm/analysis/mod.rs +++ b/src/vm/analysis/mod.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . pub mod analysis_db; +pub mod arithmetic_checker; pub mod contract_interface_builder; pub mod errors; pub mod read_only_checker; @@ -31,6 +32,7 @@ use vm::types::{QualifiedContractIdentifier, TypeSignature}; pub use self::analysis_db::AnalysisDatabase; pub use self::errors::{CheckError, CheckErrors, CheckResult}; +use self::arithmetic_checker::ArithmeticOnlyChecker; use self::contract_interface_builder::build_contract_interface; use self::read_only_checker::ReadOnlyChecker; use self::trait_checker::TraitChecker; @@ -95,6 +97,8 @@ pub fn run_analysis( ReadOnlyChecker::run_pass(&mut contract_analysis, db)?; TypeChecker::run_pass(&mut contract_analysis, db)?; TraitChecker::run_pass(&mut contract_analysis, db)?; + ArithmeticOnlyChecker::check_contract_cost_eligible(&mut contract_analysis); + if STORE_CONTRACT_SRC_INTERFACE { let interface = build_contract_interface(&contract_analysis); contract_analysis.contract_interface = Some(interface); diff --git a/src/vm/analysis/read_only_checker/mod.rs b/src/vm/analysis/read_only_checker/mod.rs index b4e0e49375..76e21bae9e 100644 --- a/src/vm/analysis/read_only_checker/mod.rs +++ b/src/vm/analysis/read_only_checker/mod.rs @@ -179,7 +179,7 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { | IsSome | TryRet | ToUInt | ToInt | Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo | TupleGet | TupleMerge | Len | Print | AsContract | Begin | FetchVar | GetStxBalance | GetTokenBalance | GetAssetOwner - | ElementAt | IndexOf => self.check_all_read_only(args), + | GetTokenSupply | ElementAt | IndexOf => self.check_all_read_only(args), AtBlock => { check_argument_count(2, args)?; @@ -195,7 +195,7 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { self.check_all_read_only(args) } StxTransfer | StxBurn | SetEntry | DeleteEntry | InsertEntry | SetVar | MintAsset - | MintToken | TransferAsset | TransferToken => { + | MintToken | TransferAsset | TransferToken | BurnAsset | BurnToken => { self.check_all_read_only(args)?; Ok(false) } diff --git a/src/vm/analysis/type_checker/mod.rs b/src/vm/analysis/type_checker/mod.rs index 41dbda00d2..1db54e1a14 100644 --- a/src/vm/analysis/type_checker/mod.rs +++ b/src/vm/analysis/type_checker/mod.rs @@ -81,7 +81,7 @@ impl CostTracker for TypeChecker<'_, '_> { fn compute_cost( &mut self, cost_function: ClarityCostFunction, - input: u64, + input: &[u64], ) -> Result { self.cost_track.compute_cost(cost_function, input) } diff --git a/src/vm/analysis/type_checker/natives/assets.rs b/src/vm/analysis/type_checker/natives/assets.rs index c5e99cebbc..9433c8f83d 100644 --- a/src/vm/analysis/type_checker/natives/assets.rs +++ b/src/vm/analysis/type_checker/natives/assets.rs @@ -185,3 +185,79 @@ pub fn check_special_transfer_token( .into(), ) } + +pub fn check_special_get_token_supply( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + _context: &TypingContext, +) -> TypeResult { + check_argument_count(1, args)?; + + let asset_name = args[0].match_atom().ok_or(CheckErrors::BadTokenName)?; + + if !checker.contract_context.ft_exists(asset_name) { + return Err(CheckErrors::NoSuchFT(asset_name.to_string()).into()); + } + + runtime_cost(ClarityCostFunction::AnalysisTypeLookup, checker, 1)?; + + Ok(TypeSignature::UIntType) +} + +pub fn check_special_burn_asset( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> TypeResult { + check_argument_count(3, args)?; + + let asset_name = args[0].match_atom().ok_or(CheckErrors::BadTokenName)?; + + let expected_owner_type: TypeSignature = TypeSignature::PrincipalType; + let expected_asset_type = checker + .contract_context + .get_nft_type(asset_name) + .ok_or(CheckErrors::NoSuchNFT(asset_name.to_string()))? + .clone(); // this clone shouldn't be strictly necessary, but to use `type_check_expects` with this, it would have to be. + + runtime_cost( + ClarityCostFunction::AnalysisTypeLookup, + checker, + expected_asset_type.type_size()?, + )?; + + checker.type_check_expects(&args[1], context, &expected_asset_type)?; + checker.type_check_expects(&args[2], context, &expected_owner_type)?; + + Ok( + TypeSignature::ResponseType(Box::new((TypeSignature::BoolType, TypeSignature::UIntType))) + .into(), + ) +} + +pub fn check_special_burn_token( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> TypeResult { + check_argument_count(3, args)?; + + let asset_name = args[0].match_atom().ok_or(CheckErrors::BadTokenName)?; + + let expected_amount: TypeSignature = TypeSignature::UIntType; + let expected_owner_type: TypeSignature = TypeSignature::PrincipalType; + + runtime_cost(ClarityCostFunction::AnalysisTypeLookup, checker, 1)?; + + checker.type_check_expects(&args[1], context, &expected_amount)?; + checker.type_check_expects(&args[2], context, &expected_owner_type)?; + + if !checker.contract_context.ft_exists(asset_name) { + return Err(CheckErrors::NoSuchFT(asset_name.to_string()).into()); + } + + Ok( + TypeSignature::ResponseType(Box::new((TypeSignature::BoolType, TypeSignature::UIntType))) + .into(), + ) +} diff --git a/src/vm/analysis/type_checker/natives/mod.rs b/src/vm/analysis/type_checker/natives/mod.rs index b07b58a89b..84b636341c 100644 --- a/src/vm/analysis/type_checker/natives/mod.rs +++ b/src/vm/analysis/type_checker/natives/mod.rs @@ -675,6 +675,11 @@ impl TypedNativeFunction { TransferAsset => Special(SpecialNativeFunction(&assets::check_special_transfer_asset)), MintAsset => Special(SpecialNativeFunction(&assets::check_special_mint_asset)), MintToken => Special(SpecialNativeFunction(&assets::check_special_mint_token)), + BurnAsset => Special(SpecialNativeFunction(&assets::check_special_burn_asset)), + BurnToken => Special(SpecialNativeFunction(&assets::check_special_burn_token)), + GetTokenSupply => Special(SpecialNativeFunction( + &assets::check_special_get_token_supply, + )), Equals => Special(SpecialNativeFunction(&check_special_equals)), If => Special(SpecialNativeFunction(&check_special_if)), Let => Special(SpecialNativeFunction(&check_special_let)), diff --git a/src/vm/analysis/type_checker/tests/assets.rs b/src/vm/analysis/type_checker/tests/assets.rs index b69214ebab..c6935fe1ff 100644 --- a/src/vm/analysis/type_checker/tests/assets.rs +++ b/src/vm/analysis/type_checker/tests/assets.rs @@ -33,11 +33,15 @@ const FIRST_CLASS_TOKENS: &str = "(define-fungible-token stackaroos) (nft-get-owner? stacka-nfts \"1234567890\" ) (define-read-only (my-ft-get-balance (account principal)) (ft-get-balance stackaroos account)) + (define-read-only (my-ft-get-supply) + (ft-get-supply stackaroos)) (define-public (my-token-transfer (to principal) (amount uint)) (ft-transfer? stackaroos amount tx-sender to)) (define-public (faucet) (let ((original-sender tx-sender)) (as-contract (ft-transfer? stackaroos u1 tx-sender original-sender)))) + (define-public (burn) + (ft-burn? stackaroos u1 tx-sender)) (define-public (mint-after (block-to-release uint)) (if (>= block-height block-to-release) (faucet) @@ -91,7 +95,10 @@ const ASSET_NAMES: &str = "(define-constant burn-address 'SP00000000000000000000 (tuple (name-hash (hash160 (xor name salt)))))) (ok u0) (err u3)) - (err u4))))"; + (err u4)))) + (define-public (revoke (name uint)) + (nft-burn? names name tx-sender)) + "; #[test] fn test_names_tokens_contracts() { @@ -140,6 +147,9 @@ fn test_bad_asset_usage() { "(nft-transfer? stacka-nfts \"a\" u2 tx-sender)", "(nft-transfer? stacka-nfts \"a\" tx-sender u2)", "(nft-transfer? stacka-nfts u2 tx-sender tx-sender)", + "(nft-burn? u1234 \"a\" tx-sender)", + "(nft-burn? stacka-nfts u2 tx-sender)", + "(nft-burn? stacka-nfts \"a\" u2)", "(ft-transfer? stackoos u1 tx-sender tx-sender)", "(ft-transfer? u1234 u1 tx-sender tx-sender)", "(ft-transfer? stackaroos u2 u100 tx-sender)", @@ -149,6 +159,10 @@ fn test_bad_asset_usage() { "(define-non-fungible-token stackaroos integer)", "(ft-mint? stackaroos 100 tx-sender)", "(ft-transfer? stackaroos 1 tx-sender tx-sender)", + "(ft-get-supply stackoos)", + "(ft-burn? stackoos u1 tx-sender)", + "(ft-burn? stackaroos 1 tx-sender)", + "(ft-burn? stackaroos u1 123432343)", ]; let expected = [ @@ -175,6 +189,9 @@ fn test_bad_asset_usage() { CheckErrors::TypeError(TypeSignature::PrincipalType, TypeSignature::UIntType), CheckErrors::TypeError(TypeSignature::PrincipalType, TypeSignature::UIntType), CheckErrors::TypeError(string_ascii_type(10), TypeSignature::UIntType), + CheckErrors::BadTokenName, + CheckErrors::TypeError(string_ascii_type(10), TypeSignature::UIntType), + CheckErrors::TypeError(TypeSignature::PrincipalType, TypeSignature::UIntType), CheckErrors::NoSuchFT("stackoos".to_string()), CheckErrors::BadTokenName, CheckErrors::TypeError(TypeSignature::PrincipalType, TypeSignature::UIntType), @@ -184,12 +201,16 @@ fn test_bad_asset_usage() { CheckErrors::DefineNFTBadSignature.into(), CheckErrors::TypeError(TypeSignature::UIntType, TypeSignature::IntType), CheckErrors::TypeError(TypeSignature::UIntType, TypeSignature::IntType), + CheckErrors::NoSuchFT("stackoos".to_string()), + CheckErrors::NoSuchFT("stackoos".to_string()), + CheckErrors::TypeError(TypeSignature::UIntType, TypeSignature::IntType), + CheckErrors::TypeError(TypeSignature::PrincipalType, TypeSignature::IntType), ]; for (script, expected_err) in bad_scripts.iter().zip(expected.iter()) { let tokens_contract = format!("{}\n{}", FIRST_CLASS_TOKENS, script); let actual_err = mem_type_check(&tokens_contract).unwrap_err(); - + println!("{}", script); assert_eq!(&actual_err.err, expected_err); } } diff --git a/src/vm/analysis/types.rs b/src/vm/analysis/types.rs index 7869678324..c43ff11490 100644 --- a/src/vm/analysis/types.rs +++ b/src/vm/analysis/types.rs @@ -50,6 +50,7 @@ pub struct ContractAnalysis { pub defined_traits: BTreeMap>, pub implemented_traits: BTreeSet, pub contract_interface: Option, + pub is_cost_contract_eligible: bool, #[serde(skip)] pub expressions: Vec, #[serde(skip)] @@ -80,6 +81,7 @@ impl ContractAnalysis { fungible_tokens: BTreeSet::new(), non_fungible_tokens: BTreeMap::new(), cost_track: Some(cost_track), + is_cost_contract_eligible: false, } } diff --git a/src/vm/callables.rs b/src/vm/callables.rs index c7f728da98..36d825d0f5 100644 --- a/src/vm/callables.rs +++ b/src/vm/callables.rs @@ -21,7 +21,7 @@ use std::iter::FromIterator; use chainstate::stacks::events::StacksTransactionEvent; -use vm::costs::{cost_functions, runtime_cost, SimpleCostSpecification}; +use vm::costs::{cost_functions, runtime_cost}; use vm::analysis::errors::CheckErrors; use vm::contexts::ContractContext; diff --git a/src/vm/clarity.rs b/src/vm/clarity.rs index 28fd934947..6b27697bd4 100644 --- a/src/vm/clarity.rs +++ b/src/vm/clarity.rs @@ -40,7 +40,10 @@ use chainstate::stacks::StacksBlockId; use chainstate::stacks::StacksMicroblockHeader; #[cfg(test)] -use chainstate::stacks::boot::{BOOT_CODE_COSTS, STACKS_BOOT_COST_CONTRACT}; +use chainstate::stacks::boot::{ + BOOT_CODE_COSTS, BOOT_CODE_COST_VOTING, STACKS_BOOT_COST_CONTRACT, + STACKS_BOOT_COST_VOTE_CONTRACT, +}; use std::error; use std::fmt; @@ -362,6 +365,20 @@ impl ClarityInstance { .unwrap(); }); + conn.as_transaction(|clarity_db| { + let (ast, _) = clarity_db + .analyze_smart_contract(&*STACKS_BOOT_COST_VOTE_CONTRACT, BOOT_CODE_COST_VOTING) + .unwrap(); + clarity_db + .initialize_smart_contract( + &*STACKS_BOOT_COST_VOTE_CONTRACT, + &ast, + BOOT_CODE_COST_VOTING, + |_, _| false, + ) + .unwrap(); + }); + conn } @@ -921,7 +938,7 @@ impl<'a> ClarityTransactionConnection<'a> { /// Initialize a contract in the current block. /// If an error occurs while processing the initialization, it's modifications will be rolled back. /// abort_call_back is called with an AssetMap and a ClarityDatabase reference, - /// if abort_call_back returns false, all modifications from this transaction will be rolled back. + /// if abort_call_back returns true, all modifications from this transaction will be rolled back. /// otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back). pub fn initialize_smart_contract( &mut self, @@ -1594,7 +1611,7 @@ mod tests { hash_mode: SinglesigHashMode::P2PKH, key_encoding: TransactionPublicKeyEncoding::Compressed, nonce: 0, - fee_rate: 1, + tx_fee: 1, signature: MessageSignature::from_raw(&vec![0xfe; 65]), }); diff --git a/src/vm/contexts.rs b/src/vm/contexts.rs index ece147b820..7697082cd2 100644 --- a/src/vm/contexts.rs +++ b/src/vm/contexts.rs @@ -638,7 +638,7 @@ impl CostTracker for Environment<'_, '_> { fn compute_cost( &mut self, cost_function: ClarityCostFunction, - input: u64, + input: &[u64], ) -> std::result::Result { self.global_context .cost_track @@ -672,7 +672,7 @@ impl CostTracker for GlobalContext<'_> { fn compute_cost( &mut self, cost_function: ClarityCostFunction, - input: u64, + input: &[u64], ) -> std::result::Result { self.cost_track.compute_cost(cost_function, input) } @@ -1162,6 +1162,26 @@ impl<'a, 'b> Environment<'a, 'b> { Ok(()) } + pub fn register_nft_burn_event( + &mut self, + sender: PrincipalData, + value: Value, + asset_identifier: AssetIdentifier, + ) -> Result<()> { + let event_data = NFTBurnEventData { + sender, + asset_identifier, + value, + }; + + if let Some(batch) = self.global_context.event_batches.last_mut() { + batch.events.push(StacksTransactionEvent::NFTEvent( + NFTEventType::NFTBurnEvent(event_data), + )); + } + Ok(()) + } + pub fn register_ft_transfer_event( &mut self, sender: PrincipalData, @@ -1205,6 +1225,28 @@ impl<'a, 'b> Environment<'a, 'b> { } Ok(()) } + + pub fn register_ft_burn_event( + &mut self, + sender: PrincipalData, + amount: u128, + asset_identifier: AssetIdentifier, + ) -> Result<()> { + let event_data = FTBurnEventData { + sender, + asset_identifier, + amount, + }; + + if let Some(batch) = self.global_context.event_batches.last_mut() { + batch + .events + .push(StacksTransactionEvent::FTEvent(FTEventType::FTBurnEvent( + event_data, + ))); + } + Ok(()) + } } impl<'a> GlobalContext<'a> { diff --git a/src/vm/costs/cost_functions.rs b/src/vm/costs/cost_functions.rs index a52d9d50a3..ec21602ba8 100644 --- a/src/vm/costs/cost_functions.rs +++ b/src/vm/costs/cost_functions.rs @@ -123,8 +123,11 @@ define_named_enum!(ClarityCostFunction { FtMint("cost_ft_mint"), FtTransfer("cost_ft_transfer"), FtBalance("cost_ft_balance"), + FtSupply("cost_ft_get_supply"), + FtBurn("cost_ft_burn"), NftMint("cost_nft_mint"), NftTransfer("cost_nft_transfer"), NftOwner("cost_nft_owner"), + NftBurn("cost_nft_burn"), PoisonMicroblock("poison_microblock"), }); diff --git a/src/vm/costs/mod.rs b/src/vm/costs/mod.rs index 3d00192e37..de7d4ab98b 100644 --- a/src/vm/costs/mod.rs +++ b/src/vm/costs/mod.rs @@ -19,33 +19,53 @@ pub mod cost_functions; use regex::internal::Exec; use rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use std::{cmp, fmt}; use std::collections::{BTreeMap, HashMap}; -use chainstate::stacks::boot::STACKS_BOOT_COST_CONTRACT; +use chainstate::stacks::boot::{STACKS_BOOT_COST_CONTRACT, STACKS_BOOT_COST_VOTE_CONTRACT}; use vm::ast::ContractAST; use vm::contexts::{ContractContext, Environment, GlobalContext, OwnedEnvironment}; use vm::costs::cost_functions::ClarityCostFunction; use vm::database::{marf::NullBackingStore, ClarityDatabase, MemoryBackingStore}; use vm::errors::{Error, InterpreterResult}; +use vm::types::signatures::FunctionType::Fixed; use vm::types::Value::UInt; -use vm::types::{QualifiedContractIdentifier, TypeSignature, NONE}; +use vm::types::{ + FunctionArg, FunctionType, PrincipalData, QualifiedContractIdentifier, TupleData, + TypeSignature, NONE, +}; use vm::{ast, eval_all, ClarityName, SymbolicExpression, Value}; +use vm::types::signatures::{FunctionSignature, TupleTypeSignature}; + type Result = std::result::Result; pub const CLARITY_MEMORY_LIMIT: u64 = 100 * 1000 * 1000; +lazy_static! { + static ref COST_TUPLE_TYPE_SIGNATURE: TypeSignature = TypeSignature::TupleType( + TupleTypeSignature::try_from(vec![ + ("runtime".into(), TypeSignature::UIntType), + ("write_length".into(), TypeSignature::UIntType), + ("write_count".into(), TypeSignature::UIntType), + ("read_count".into(), TypeSignature::UIntType), + ("read_length".into(), TypeSignature::UIntType), + ]) + .expect("BUG: failed to construct type signature for cost tuple") + ); +} + pub fn runtime_cost, C: CostTracker>( cost_function: ClarityCostFunction, tracker: &mut C, input: T, ) -> Result<()> { let size: u64 = input.try_into().map_err(|_| CostErrors::CostOverflow)?; - let cost = tracker.compute_cost(cost_function, size)?; + let cost = tracker.compute_cost(cost_function, &[size])?; tracker.add_cost(cost) } @@ -67,7 +87,7 @@ pub fn analysis_typecheck_cost( let t2_size = t2.type_size().map_err(|_| CostErrors::CostOverflow)?; let cost = track.compute_cost( ClarityCostFunction::AnalysisTypeCheck, - cmp::max(t1_size, t2_size) as u64, + &[cmp::max(t1_size, t2_size) as u64], )?; track.add_cost(cost) } @@ -86,7 +106,7 @@ pub trait CostTracker { fn compute_cost( &mut self, cost_function: ClarityCostFunction, - input: u64, + input: &[u64], ) -> Result; fn add_cost(&mut self, cost: ExecutionCost) -> Result<()>; fn add_memory(&mut self, memory: u64) -> Result<()>; @@ -108,7 +128,7 @@ impl CostTracker for () { fn compute_cost( &mut self, _cost_function: ClarityCostFunction, - _input: u64, + _input: &[u64], ) -> std::result::Result { Ok(ExecutionCost::zero()) } @@ -151,6 +171,57 @@ impl ClarityCostFunctionReference { } } +#[derive(Debug, Clone)] +pub struct CostStateSummary { + pub contract_call_circuits: + HashMap<(QualifiedContractIdentifier, ClarityName), ClarityCostFunctionReference>, + pub cost_function_references: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SerializedCostStateSummary { + contract_call_circuits: Vec<( + (QualifiedContractIdentifier, ClarityName), + ClarityCostFunctionReference, + )>, + cost_function_references: Vec<(ClarityCostFunction, ClarityCostFunctionReference)>, +} + +impl From for SerializedCostStateSummary { + fn from(other: CostStateSummary) -> SerializedCostStateSummary { + let CostStateSummary { + contract_call_circuits, + cost_function_references, + } = other; + SerializedCostStateSummary { + contract_call_circuits: contract_call_circuits.into_iter().collect(), + cost_function_references: cost_function_references.into_iter().collect(), + } + } +} + +impl From for CostStateSummary { + fn from(other: SerializedCostStateSummary) -> CostStateSummary { + let SerializedCostStateSummary { + contract_call_circuits, + cost_function_references, + } = other; + CostStateSummary { + contract_call_circuits: contract_call_circuits.into_iter().collect(), + cost_function_references: cost_function_references.into_iter().collect(), + } + } +} + +impl CostStateSummary { + pub fn empty() -> CostStateSummary { + CostStateSummary { + contract_call_circuits: HashMap::new(), + cost_function_references: HashMap::new(), + } + } +} + #[derive(Clone)] pub struct LimitedCostTracker { cost_function_references: HashMap<&'static ClarityCostFunction, ClarityCostFunctionReference>, @@ -164,6 +235,20 @@ pub struct LimitedCostTracker { free: bool, } +#[cfg(test)] +impl LimitedCostTracker { + pub fn contract_call_circuits( + &self, + ) -> HashMap<(QualifiedContractIdentifier, ClarityName), ClarityCostFunctionReference> { + self.contract_call_circuits.clone() + } + pub fn cost_function_references( + &self, + ) -> HashMap<&'static ClarityCostFunction, ClarityCostFunctionReference> { + self.cost_function_references.clone() + } +} + impl fmt::Debug for LimitedCostTracker { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LimitedCostTracker") @@ -194,6 +279,294 @@ pub enum CostErrors { CostContractLoadFailure, } +fn load_state_summary(clarity_db: &mut ClarityDatabase) -> Result { + let last_processed_at = match clarity_db.get_value( + "vm-costs::last-processed-at-height", + &TypeSignature::UIntType, + ) { + Some(v) => u32::try_from(v.expect_u128()).expect("Block height overflowed u32"), + None => return Ok(CostStateSummary::empty()), + }; + + let metadata_result = clarity_db + .fetch_metadata_manual::( + last_processed_at, + &STACKS_BOOT_COST_VOTE_CONTRACT, + "::state_summary", + ) + .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))?; + let serialized: SerializedCostStateSummary = match metadata_result { + Some(serialized) => serde_json::from_str(&serialized).unwrap(), + None => return Ok(CostStateSummary::empty()), + }; + Ok(CostStateSummary::from(serialized)) +} + +fn store_state_summary( + clarity_db: &mut ClarityDatabase, + to_store: &CostStateSummary, +) -> Result<()> { + let block_height = clarity_db.get_current_block_height(); + + clarity_db.put( + "vm-costs::last-processed-at-height", + &Value::UInt(block_height as u128), + ); + let serialized_summary = + serde_json::to_string(&SerializedCostStateSummary::from(to_store.clone())) + .expect("BUG: failure to serialize cost state summary struct"); + clarity_db.set_metadata( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "::state_summary", + &serialized_summary, + ); + + Ok(()) +} + +/// +/// This method loads a cost state summary structure from the currently open stacks chain tip +/// In doing so, it reads from the cost-voting contract to find any newly confirmed proposals, +/// checks those proposals for validity, and then applies those changes to the cached set +/// of cost functions. +/// +/// `apply_updates` - tells this function to look for any changes in the cost voting contract +/// which would need to be applied. if `false`, just load the last computed cost state in this +/// fork. +/// +fn load_cost_functions( + clarity_db: &mut ClarityDatabase, + apply_updates: bool, +) -> Result { + let last_processed_count = clarity_db + .get_value("vm-costs::last_processed_count", &TypeSignature::UIntType) + .unwrap_or(Value::UInt(0)) + .expect_u128(); + let cost_voting_contract = &STACKS_BOOT_COST_VOTE_CONTRACT; + let confirmed_proposals_count = clarity_db + .lookup_variable(&cost_voting_contract, "confirmed-proposal-count") + .map_err(|e| CostErrors::CostComputationFailed(e.to_string()))? + .expect_u128(); + debug!("Check cost voting contract"; + "confirmed_proposal_count" => confirmed_proposals_count, + "last_processed_count" => last_processed_count); + + // we need to process any confirmed proposals in the range [fetch-start, fetch-end) + let (fetch_start, fetch_end) = (last_processed_count, confirmed_proposals_count); + let mut state_summary = load_state_summary(clarity_db)?; + if !apply_updates { + return Ok(state_summary); + } + + for confirmed_proposal in fetch_start..fetch_end { + // fetch the proposal data + let entry = clarity_db + .fetch_entry( + &cost_voting_contract, + "confirmed-proposals", + &Value::from( + TupleData::from_data(vec![( + "confirmed-id".into(), + Value::UInt(confirmed_proposal), + )]) + .expect("BUG: failed to construct simple tuple"), + ), + ) + .expect("BUG: Failed querying confirmed-proposals") + .expect_optional() + .expect("BUG: confirmed-proposal-count exceeds stored proposals") + .expect_tuple(); + let target_contract = match entry + .get("function-contract") + .expect("BUG: malformed cost proposal tuple") + .clone() + .expect_principal() + { + PrincipalData::Contract(contract_id) => contract_id, + _ => { + warn!("Confirmed cost proposal invalid: function-contract is not a contract principal"; + "confirmed_proposal_id" => confirmed_proposal); + continue; + } + }; + let target_function = match ClarityName::try_from( + entry + .get("function-name") + .expect("BUG: malformed cost proposal tuple") + .clone() + .expect_ascii(), + ) { + Ok(x) => x, + Err(_) => { + warn!("Confirmed cost proposal invalid: function-name is not a valid function name"; + "confirmed_proposal_id" => confirmed_proposal); + continue; + } + }; + let cost_contract = match entry + .get("cost-function-contract") + .expect("BUG: malformed cost proposal tuple") + .clone() + .expect_principal() + { + PrincipalData::Contract(contract_id) => contract_id, + _ => { + warn!("Confirmed cost proposal invalid: cost-function-contract is not a contract principal"; + "confirmed_proposal_id" => confirmed_proposal); + continue; + } + }; + + let cost_function = match ClarityName::try_from( + entry + .get_owned("cost-function-name") + .expect("BUG: malformed cost proposal tuple") + .expect_ascii(), + ) { + Ok(x) => x, + Err(_) => { + warn!("Confirmed cost proposal invalid: cost-function-name is not a valid function name"; + "confirmed_proposal_id" => confirmed_proposal); + continue; + } + }; + + // Here is where we perform the required validity checks for a confirmed proposal: + // * Replaced contract-calls _must_ be `define-read-only` _or_ refer to one of the boot code + // cost functions + // * cost-function contracts must be arithmetic only + + // make sure the contract is "cost contract eligible" via the + // arithmetic-checking analysis pass + let (cost_func_ref, cost_func_type) = match clarity_db + .load_contract_analysis(&cost_contract) + { + Some(c) => { + if !c.is_cost_contract_eligible { + warn!("Confirmed cost proposal invalid: cost-function-contract uses non-arithmetic or otherwise illegal operations"; + "confirmed_proposal_id" => confirmed_proposal, + "contract_name" => %cost_contract, + ); + continue; + } + + if let Some(FunctionType::Fixed(cost_function_type)) = c + .read_only_function_types + .get(&cost_function) + .or_else(|| c.private_function_types.get(&cost_function)) + { + if !cost_function_type.returns.eq(&COST_TUPLE_TYPE_SIGNATURE) { + warn!("Confirmed cost proposal invalid: cost-function-name does not return a cost tuple"; + "confirmed_proposal_id" => confirmed_proposal, + "contract_name" => %cost_contract, + "function_name" => %cost_function, + "return_type" => %cost_function_type.returns, + ); + continue; + } + if !cost_function_type.args.len() == 1 + || cost_function_type.args[0].signature != TypeSignature::UIntType + { + warn!("Confirmed cost proposal invalid: cost-function-name args should be length-1 and only uint"; + "confirmed_proposal_id" => confirmed_proposal, + "contract_name" => %cost_contract, + "function_name" => %cost_function, + ); + continue; + } + ( + ClarityCostFunctionReference { + contract_id: cost_contract, + function_name: cost_function.to_string(), + }, + cost_function_type.clone(), + ) + } else { + warn!("Confirmed cost proposal invalid: cost-function-name not defined"; + "confirmed_proposal_id" => confirmed_proposal, + "contract_name" => %cost_contract, + "function_name" => %cost_function, + ); + continue; + } + } + None => { + warn!("Confirmed cost proposal invalid: cost-function-contract is not a published contract"; + "confirmed_proposal_id" => confirmed_proposal, + "contract_name" => %cost_contract, + ); + continue; + } + }; + + if target_contract == *STACKS_BOOT_COST_CONTRACT { + // refering to one of the boot code cost functions + let target = match ClarityCostFunction::lookup_by_name(&target_function) { + Some(cost_func) => cost_func, + None => { + warn!("Confirmed cost proposal invalid: function-name does not reference a Clarity cost function"; + "confirmed_proposal_id" => confirmed_proposal, + "cost_function" => %target_function); + continue; + } + }; + state_summary + .cost_function_references + .insert(target, cost_func_ref); + } else { + // referring to a user-defined function + match clarity_db.load_contract_analysis(&target_contract) { + Some(c) => { + if let Some(Fixed(tf)) = c.read_only_function_types.get(&target_function) { + if cost_func_type.args.len() != tf.args.len() { + warn!("Confirmed cost proposal invalid: cost-function contains the wrong number of arguments"; + "confirmed_proposal_id" => confirmed_proposal, + "target_contract_name" => %target_contract, + "target_function_name" => %target_function, + ); + continue; + } + for arg in &cost_func_type.args { + if &arg.signature != &TypeSignature::UIntType { + warn!("Confirmed cost proposal invalid: contains non uint argument"; + "confirmed_proposal_id" => confirmed_proposal, + ); + continue; + } + } + } else { + warn!("Confirmed cost proposal invalid: function-name not defined or is not read-only"; + "confirmed_proposal_id" => confirmed_proposal, + "target_contract_name" => %target_contract, + "target_function_name" => %target_function, + ); + continue; + } + } + None => { + warn!("Confirmed cost proposal invalid: contract-name not a published contract"; + "confirmed_proposal_id" => confirmed_proposal, + "target_contract_name" => %target_contract, + ); + continue; + } + } + state_summary + .contract_call_circuits + .insert((target_contract, target_function), cost_func_ref); + } + } + if confirmed_proposals_count > last_processed_count { + store_state_summary(clarity_db, &state_summary)?; + clarity_db.put( + "vm-costs::last_processed_count", + &Value::UInt(confirmed_proposals_count), + ); + } + + Ok(state_summary) +} + impl LimitedCostTracker { pub fn new( limit: ExecutionCost, @@ -209,35 +582,34 @@ impl LimitedCostTracker { memory: 0, free: false, }; - cost_tracker.load_boot_costs(clarity_db)?; + assert!(clarity_db.is_stack_empty()); + cost_tracker.load_costs(clarity_db, true)?; Ok(cost_tracker) } - pub fn new_max_limit(clarity_db: &mut ClarityDatabase) -> Result { - LimitedCostTracker::new(ExecutionCost::max_value(), clarity_db) - } - #[cfg(test)] - pub fn new_max_limit_with_circuits( + pub fn new_mid_block( + limit: ExecutionCost, clarity_db: &mut ClarityDatabase, - circuits: Vec<( - (QualifiedContractIdentifier, ClarityName), - ClarityCostFunctionReference, - )>, ) -> Result { let mut cost_tracker = LimitedCostTracker { cost_function_references: HashMap::new(), cost_contracts: HashMap::new(), - contract_call_circuits: circuits.into_iter().collect(), - limit: ExecutionCost::max_value(), + contract_call_circuits: HashMap::new(), + limit, memory_limit: CLARITY_MEMORY_LIMIT, total: ExecutionCost::zero(), memory: 0, free: false, }; - cost_tracker.load_boot_costs(clarity_db)?; + cost_tracker.load_costs(clarity_db, false)?; Ok(cost_tracker) } + pub fn new_max_limit(clarity_db: &mut ClarityDatabase) -> Result { + assert!(clarity_db.is_stack_empty()); + LimitedCostTracker::new(ExecutionCost::max_value(), clarity_db) + } + pub fn new_free() -> LimitedCostTracker { LimitedCostTracker { cost_function_references: HashMap::new(), @@ -250,31 +622,46 @@ impl LimitedCostTracker { free: true, } } - pub fn load_boot_costs(&mut self, clarity_db: &mut ClarityDatabase) -> Result<()> { + + /// `apply_updates` - tells this function to look for any changes in the cost voting contract + /// which would need to be applied. if `false`, just load the last computed cost state in this + /// fork. + fn load_costs(&mut self, clarity_db: &mut ClarityDatabase, apply_updates: bool) -> Result<()> { let boot_costs_id = (*STACKS_BOOT_COST_CONTRACT).clone(); clarity_db.begin(); + let CostStateSummary { + contract_call_circuits, + mut cost_function_references, + } = load_cost_functions(clarity_db, apply_updates).map_err(|e| { + clarity_db.roll_back(); + e + })?; + + self.contract_call_circuits = contract_call_circuits; let mut cost_contracts = HashMap::new(); let mut m = HashMap::new(); for f in ClarityCostFunction::ALL.iter() { - m.insert( - f, - ClarityCostFunctionReference::new(boot_costs_id.clone(), f.get_name()), - ); - if !cost_contracts.contains_key(&boot_costs_id) { - let contract_context = match clarity_db.get_contract(&boot_costs_id) { + let cost_function_ref = cost_function_references.remove(&f).unwrap_or_else(|| { + ClarityCostFunctionReference::new(boot_costs_id.clone(), f.get_name()) + }); + if !cost_contracts.contains_key(&cost_function_ref.contract_id) { + let contract_context = match clarity_db.get_contract(&cost_function_ref.contract_id) + { Ok(contract) => contract.contract_context, Err(e) => { error!("Failed to load intended Clarity cost contract"; - "contract" => %boot_costs_id.to_string(), - "error" => %format!("{:?}", e)); + "contract" => %cost_function_ref.contract_id, + "error" => ?e); clarity_db.roll_back(); return Err(CostErrors::CostContractLoadFailure); } }; - cost_contracts.insert(boot_costs_id.clone(), contract_context); + cost_contracts.insert(cost_function_ref.contract_id.clone(), contract_context); } + + m.insert(f, cost_function_ref); } for (_, circuit_target) in self.contract_call_circuits.iter() { @@ -296,7 +683,11 @@ impl LimitedCostTracker { self.cost_function_references = m; self.cost_contracts = cost_contracts; - clarity_db.roll_back(); + if apply_updates { + clarity_db.commit(); + } else { + clarity_db.roll_back(); + } return Ok(()); } @@ -361,7 +752,7 @@ fn parse_cost( fn compute_cost( cost_tracker: &mut LimitedCostTracker, cost_function_reference: ClarityCostFunctionReference, - input_size: u64, + input_sizes: &[u64], ) -> Result { let mut null_store = NullBackingStore::new(); let conn = null_store.as_clarity_db(); @@ -375,10 +766,15 @@ fn compute_cost( &cost_function_reference )))?; - let program = vec![ - SymbolicExpression::atom(cost_function_reference.function_name[..].into()), - SymbolicExpression::atom_value(Value::UInt(input_size.into())), - ]; + let mut program = vec![SymbolicExpression::atom( + cost_function_reference.function_name[..].into(), + )]; + + for input_size in input_sizes.iter() { + program.push(SymbolicExpression::atom_value(Value::UInt( + *input_size as u128, + ))); + } let function_invocation = [SymbolicExpression::list(program.into_boxed_slice())]; @@ -422,7 +818,7 @@ impl CostTracker for LimitedCostTracker { fn compute_cost( &mut self, cost_function: ClarityCostFunction, - input: u64, + input: &[u64], ) -> std::result::Result { if self.free { return Ok(ExecutionCost::zero()); @@ -473,8 +869,7 @@ impl CostTracker for LimitedCostTracker { // grr, if HashMap::get didn't require Borrow, we wouldn't need this cloning. let lookup_key = (contract.clone(), function.clone()); if let Some(cost_function) = self.contract_call_circuits.get(&lookup_key).cloned() { - let input_size = input.iter().fold(0, |agg, cur| agg + cur); - compute_cost(self, cost_function, input_size)?; + compute_cost(self, cost_function, input)?; Ok(true) } else { Ok(false) @@ -486,7 +881,7 @@ impl CostTracker for &mut LimitedCostTracker { fn compute_cost( &mut self, cost_function: ClarityCostFunction, - input: u64, + input: &[u64], ) -> std::result::Result { LimitedCostTracker::compute_cost(self, cost_function, input) } @@ -512,23 +907,6 @@ impl CostTracker for &mut LimitedCostTracker { } } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub enum CostFunctions { - Constant(u64), - Linear(u64, u64), - NLogN(u64, u64), - LogN(u64, u64), -} - -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -pub struct SimpleCostSpecification { - pub write_count: CostFunctions, - pub write_length: CostFunctions, - pub read_count: CostFunctions, - pub read_length: CostFunctions, - pub runtime: CostFunctions, -} - #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct ExecutionCost { pub write_length: u64, @@ -677,63 +1055,6 @@ fn int_log2(input: u64) -> Option { }) } -impl CostFunctions { - pub fn compute_cost(&self, input: u64) -> Result { - match self { - CostFunctions::Constant(val) => Ok(*val), - CostFunctions::Linear(a, b) => a.cost_overflow_mul(input)?.cost_overflow_add(*b), - CostFunctions::LogN(a, b) => { - // a*log(input)) + b - // and don't do log(0). - int_log2(cmp::max(input, 1)) - .ok_or_else(|| CostErrors::CostOverflow)? - .cost_overflow_mul(*a)? - .cost_overflow_add(*b) - } - CostFunctions::NLogN(a, b) => { - // a*input*log(input)) + b - // and don't do log(0). - int_log2(cmp::max(input, 1)) - .ok_or_else(|| CostErrors::CostOverflow)? - .cost_overflow_mul(input)? - .cost_overflow_mul(*a)? - .cost_overflow_add(*b) - } - } - } -} - -impl SimpleCostSpecification { - pub fn compute_cost(&self, input: u64) -> Result { - Ok(ExecutionCost { - write_length: self.write_length.compute_cost(input)?, - write_count: self.write_count.compute_cost(input)?, - read_count: self.read_count.compute_cost(input)?, - read_length: self.read_length.compute_cost(input)?, - runtime: self.runtime.compute_cost(input)?, - }) - } -} - -impl From for SimpleCostSpecification { - fn from(value: ExecutionCost) -> SimpleCostSpecification { - let ExecutionCost { - write_length, - write_count, - read_count, - read_length, - runtime, - } = value; - SimpleCostSpecification { - write_length: CostFunctions::Constant(write_length), - write_count: CostFunctions::Constant(write_count), - read_length: CostFunctions::Constant(read_length), - read_count: CostFunctions::Constant(read_count), - runtime: CostFunctions::Constant(runtime), - } - } -} - #[cfg(test)] mod unit_tests { use super::*; @@ -748,10 +1069,6 @@ mod unit_tests { u64::max_value().cost_overflow_mul(2), Err(CostErrors::CostOverflow) ); - assert_eq!( - CostFunctions::NLogN(1, 1).compute_cost(u64::max_value()), - Err(CostErrors::CostOverflow) - ); } #[test] diff --git a/src/vm/database/clarity_db.rs b/src/vm/database/clarity_db.rs index 6b20702b99..ba54d5eca1 100644 --- a/src/vm/database/clarity_db.rs +++ b/src/vm/database/clarity_db.rs @@ -38,6 +38,7 @@ use chainstate::stacks::{StacksAddress, StacksBlockId}; use util::db::{DBConn, FromRow}; use util::hash::{to_hex, Hash160, Sha256Sum, Sha512Trunc256Sum}; +use vm::analysis::{AnalysisDatabase, ContractAnalysis}; use vm::costs::CostOverflowingMath; use vm::database::structures::{ ClarityDeserializable, ClaritySerializable, ContractMetadata, DataMapMetadata, @@ -367,6 +368,10 @@ impl<'a> ClarityDatabase<'a> { pub fn initialize(&mut self) {} + pub fn is_stack_empty(&self) -> bool { + self.store.depth() == 0 + } + pub fn begin(&mut self) { self.store.nest(); } @@ -463,6 +468,15 @@ impl<'a> ClarityDatabase<'a> { .flatten() } + pub fn set_metadata( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + key: &str, + data: &str, + ) { + self.store.insert_metadata(contract_identifier, key, data); + } + fn insert_metadata( &mut self, contract_identifier: &QualifiedContractIdentifier, @@ -493,6 +507,36 @@ impl<'a> ClarityDatabase<'a> { .map(|x_opt| x_opt.map(|x| T::deserialize(&x))) } + pub fn fetch_metadata_manual( + &mut self, + at_height: u32, + contract_identifier: &QualifiedContractIdentifier, + key: &str, + ) -> Result> + where + T: ClarityDeserializable, + { + self.store + .get_metadata_manual(at_height, contract_identifier, key) + .map(|x_opt| x_opt.map(|x| T::deserialize(&x))) + } + + // load contract analysis stored by an analysis_db instance. + // in unit testing, where the interpreter is invoked without + // an analysis pass, this function will fail to find contract + // analysis data + pub fn load_contract_analysis( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + ) -> Option { + self.store + .get_metadata(contract_identifier, AnalysisDatabase::storage_key()) + // treat NoSuchContract error thrown by get_metadata as an Option::None -- + // the analysis will propagate that as a CheckError anyways. + .ok()? + .map(|x| ContractAnalysis::deserialize(&x)) + } + pub fn get_contract_size( &mut self, contract_identifier: &QualifiedContractIdentifier, @@ -1003,14 +1047,12 @@ impl<'a> ClarityDatabase<'a> { self.insert_metadata(contract_identifier, &key, &data); // total supply _is_ included in the consensus hash - if total_supply.is_some() { - let supply_key = ClarityDatabase::make_key_for_trip( - contract_identifier, - StoreType::CirculatingSupply, - token_name, - ); - self.put(&supply_key, &(0 as u128)); - } + let supply_key = ClarityDatabase::make_key_for_trip( + contract_identifier, + StoreType::CirculatingSupply, + token_name, + ); + self.put(&supply_key, &(0 as u128)); } fn load_ft( @@ -1056,29 +1098,52 @@ impl<'a> ClarityDatabase<'a> { ) -> Result<()> { let descriptor = self.load_ft(contract_identifier, token_name)?; - if let Some(total_supply) = descriptor.total_supply { - let key = ClarityDatabase::make_key_for_trip( - contract_identifier, - StoreType::CirculatingSupply, - token_name, - ); - let current_supply: u128 = self - .get(&key) - .expect("ERROR: Clarity VM failed to track token supply."); + let key = ClarityDatabase::make_key_for_trip( + contract_identifier, + StoreType::CirculatingSupply, + token_name, + ); + let current_supply: u128 = self + .get(&key) + .expect("ERROR: Clarity VM failed to track token supply."); - let new_supply = current_supply - .checked_add(amount) - .ok_or(RuntimeErrorType::ArithmeticOverflow)?; + let new_supply = current_supply + .checked_add(amount) + .ok_or(RuntimeErrorType::ArithmeticOverflow)?; + if let Some(total_supply) = descriptor.total_supply { if new_supply > total_supply { - Err(RuntimeErrorType::SupplyOverflow(new_supply, total_supply).into()) - } else { - self.put(&key, &new_supply); - Ok(()) + return Err(RuntimeErrorType::SupplyOverflow(new_supply, total_supply).into()); } - } else { - Ok(()) } + + self.put(&key, &new_supply); + Ok(()) + } + + pub fn checked_decrease_token_supply( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + token_name: &str, + amount: u128, + ) -> Result<()> { + let key = ClarityDatabase::make_key_for_trip( + contract_identifier, + StoreType::CirculatingSupply, + token_name, + ); + let current_supply: u128 = self + .get(&key) + .expect("ERROR: Clarity VM failed to track token supply."); + + if amount > current_supply { + return Err(RuntimeErrorType::SupplyUnderflow(current_supply, amount).into()); + } + + let new_supply = current_supply - amount; + + self.put(&key, &new_supply); + Ok(()) } pub fn get_ft_balance( @@ -1121,6 +1186,22 @@ impl<'a> ClarityDatabase<'a> { Ok(()) } + pub fn get_ft_supply( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + token_name: &str, + ) -> Result { + let key = ClarityDatabase::make_key_for_trip( + contract_identifier, + StoreType::CirculatingSupply, + token_name, + ); + let supply = self + .get(&key) + .expect("ERROR: Clarity VM failed to track token supply."); + Ok(supply) + } + pub fn get_nft_owner( &mut self, contract_identifier: &QualifiedContractIdentifier, @@ -1139,8 +1220,18 @@ impl<'a> ClarityDatabase<'a> { asset.serialize(), ); - let result = self.get(&key); - result.ok_or(RuntimeErrorType::NoSuchToken.into()) + let value: Option = self.get(&key); + let owner = match value { + Some(owner) => owner.expect_optional(), + None => return Err(RuntimeErrorType::NoSuchToken.into()), + }; + + let principal = match owner { + Some(value) => value.expect_principal(), + None => return Err(RuntimeErrorType::NoSuchToken.into()), + }; + + Ok(principal) } pub fn get_nft_key_type( @@ -1171,8 +1262,31 @@ impl<'a> ClarityDatabase<'a> { asset.serialize(), ); - self.put(&key, principal); + let value = Value::some(Value::Principal(principal.clone()))?; + self.put(&key, &value); + + Ok(()) + } + pub fn burn_nft( + &mut self, + contract_identifier: &QualifiedContractIdentifier, + asset_name: &str, + asset: &Value, + ) -> Result<()> { + let descriptor = self.load_nft(contract_identifier, asset_name)?; + if !descriptor.key_type.admits(asset) { + return Err(CheckErrors::TypeValueError(descriptor.key_type, (*asset).clone()).into()); + } + + let key = ClarityDatabase::make_key_for_quad( + contract_identifier, + StoreType::NonFungibleToken, + asset_name, + asset.serialize(), + ); + + self.put(&key, &(Value::none())); Ok(()) } } diff --git a/src/vm/database/key_value_wrapper.rs b/src/vm/database/key_value_wrapper.rs index 5a210a786c..680f338fbd 100644 --- a/src/vm/database/key_value_wrapper.rs +++ b/src/vm/database/key_value_wrapper.rs @@ -433,6 +433,35 @@ impl<'a> RollbackWrapper<'a> { } } + // Throws a NoSuchContract error if contract doesn't exist, + // returns None if there is no such metadata field. + pub fn get_metadata_manual( + &mut self, + at_height: u32, + contract: &QualifiedContractIdentifier, + key: &str, + ) -> Result> { + self.stack + .last() + .expect("ERROR: Clarity VM attempted GET on non-nested context."); + + // This is THEORETICALLY a spurious clone, but it's hard to turn something like + // (&A, &B) into &(A, B). + let metadata_key = (contract.clone(), key.to_string()); + let lookup_result = if self.query_pending_data { + self.metadata_lookup_map + .get(&metadata_key) + .and_then(|x| x.last().cloned()) + } else { + None + }; + + match lookup_result { + Some(x) => Ok(Some(x)), + None => self.store.get_metadata_manual(at_height, contract, key), + } + } + pub fn has_entry(&mut self, key: &str) -> bool { self.stack .last() diff --git a/src/vm/database/marf.rs b/src/vm/database/marf.rs index 808868b702..d3a2c5a9d7 100644 --- a/src/vm/database/marf.rs +++ b/src/vm/database/marf.rs @@ -139,6 +139,22 @@ pub trait ClarityBackingStore { .get_metadata(&bhh, &contract.to_string(), key)) } + fn get_metadata_manual( + &mut self, + at_height: u32, + contract: &QualifiedContractIdentifier, + key: &str, + ) -> Result> { + let bhh = self.get_block_at_height(at_height) + .ok_or_else(|| { + warn!("Unknown block height when manually querying metadata"; "block_height" => at_height); + RuntimeErrorType::BadBlockHeight(at_height.to_string()) + })?; + Ok(self + .get_side_store() + .get_metadata(&bhh, &contract.to_string(), key)) + } + fn put_all_metadata( &mut self, mut items: Vec<((QualifiedContractIdentifier, String), String)>, diff --git a/src/vm/docs/mod.rs b/src/vm/docs/mod.rs index a800d2a3c6..8058c04714 100644 --- a/src/vm/docs/mod.rs +++ b/src/vm/docs/mod.rs @@ -986,15 +986,15 @@ is untyped, you should use `unwrap-panic` or `unwrap-err-panic` instead of `matc (match x value (+ 10 value) 10)) -(add-10 (some 5)) ;; returns 15 -(add-10 none) ;; returns 10 +(add-10 (some 5)) ;; Returns 15 +(add-10 none) ;; Returns 10 (define-private (add-or-pass-err (x (response int (string-ascii 10))) (to-add int)) (match x value (+ to-add value) err-value (err err-value))) -(add-or-pass-err (ok 5) 20) ;; returns 25 -(add-or-pass-err (err \"ERROR\") 20) ;; returns (err \"ERROR\") +(add-or-pass-err (ok 5) 20) ;; Returns 25 +(add-or-pass-err (err \"ERROR\") 20) ;; Returns (err \"ERROR\") ", }; @@ -1188,7 +1188,7 @@ definition (i.e., you cannot put a define statement in the middle of a function ", example: " (define-constant four (+ 2 2)) -(+ 4 four) ;; returns 8 +(+ 4 four) ;; Returns 8 " }; @@ -1209,7 +1209,7 @@ Private functions may return any type.", (if (> i1 i2) i1 i2)) -(max-of 4 6) ;; returns 6 +(max-of 4 6) ;; Returns 6 " }; @@ -1351,7 +1351,7 @@ returns `(ok true)`. ", example: " (define-fungible-token stackaroo) -(ft-mint? stackaroo u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (ok true) +(ft-mint? stackaroo u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) " }; @@ -1371,7 +1371,7 @@ Otherwise, on successfuly mint, it returns `(ok true)`. ", example: " (define-non-fungible-token stackaroo (string-ascii 40)) -(nft-mint? stackaroo \"Roo\" 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (ok true) +(nft-mint? stackaroo \"Roo\" 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) " }; @@ -1399,7 +1399,7 @@ The token type must have been defined using `define-fungible-token`.", example: " (define-fungible-token stackaroo) (ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) -(ft-get-balance stackaroo 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; returns u100 +(ft-get-balance stackaroo 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns u100 ", }; @@ -1420,8 +1420,8 @@ one of the following error codes: example: " (define-fungible-token stackaroo) (ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) -(ft-transfer? stackaroo u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (ok true) -(ft-transfer? stackaroo u60 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (err u1) +(ft-transfer? stackaroo u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) +(ft-transfer? stackaroo u60 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (err u1) " }; @@ -1443,9 +1443,61 @@ one of the following error codes: example: " (define-non-fungible-token stackaroo (string-ascii 40)) (nft-mint? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) -(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (ok true) -(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (err u1) -(nft-transfer? stackaroo \"Stacka\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; returns (err u3) +(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) +(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (err u1) +(nft-transfer? stackaroo \"Stacka\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (err u3) +" +}; + +const GET_TOKEN_SUPPLY: SpecialAPI = SpecialAPI { + input_type: "TokenName", + output_type: "uint", + signature: "(ft-get-supply token-name)", + description: "`ft-get-balance` returns `token-name` circulating supply. +The token type must have been defined using `define-fungible-token`.", + example: " +(define-fungible-token stackaroo) +(ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) +(ft-get-supply stackaroo) ;; Returns u100 +", +}; + +const BURN_TOKEN: SpecialAPI = SpecialAPI { + input_type: "TokenName, uint, principal", + output_type: "(response bool uint)", + signature: "(ft-burn? token-name amount sender)", + description: "`ft-burn?` is used to decrease the token balance for the `sender` principal for a token +type defined using `define-fungible-token`. The decreased token balance is _not_ transfered to another principal, but +rather destroyed, reducing the circulating supply. + +If a non-positive amount is provided to burn, this function returns `(err 1)`. Otherwise, on successfuly burn, it +returns `(ok true)`. +", + example: " +(define-fungible-token stackaroo) +(ft-mint? stackaroo u100 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) +(ft-burn? stackaroo u50 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) +" +}; + +const BURN_ASSET: SpecialAPI = SpecialAPI { + input_type: "AssetName, A, principal", + output_type: "(response bool uint)", + signature: "(nft-burn? asset-class asset-identifier recipient)", + description: "`nft-burn?` is used to burn an asset and remove that asset's owner from the `recipient` principal. +The asset must have been defined using `define-non-fungible-token`, and the supplied `asset-identifier` must be of the same type specified in +that definition. + +If an asset identified by `asset-identifier` _doesn't exist_, this function will return an error with the following error code: + +`(err u1)` + +Otherwise, on successfuly burn, it returns `(ok true)`. +", + example: " +(define-non-fungible-token stackaroo (string-ascii 40)) +(nft-mint? stackaroo \"Roo\" 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) +(nft-burn? stackaroo \"Roo\" 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF) ;; Returns (ok true) " }; @@ -1458,8 +1510,8 @@ This function returns the STX balance of the `owner` principal. In the event tha principal isn't materialized, it returns 0. ", example: " -(stx-get-balance 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; returns u0 -(stx-get-balance (as-contract tx-sender)) ;; returns u10000 +(stx-get-balance 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns u0 +(stx-get-balance (as-contract tx-sender)) ;; Returns u1000 ", }; @@ -1479,9 +1531,9 @@ one of the following error codes: ", example: " (as-contract - (stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; returns (ok true) + (stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; Returns (ok true) (as-contract - (stx-transfer? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR tx-sender)) ;; returns (err u4) + (stx-transfer? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR tx-sender)) ;; Returns (err u4) " }; @@ -1500,9 +1552,9 @@ one of the following error codes: ", example: " (as-contract - (stx-burn? u60 tx-sender)) ;; returns (ok true) + (stx-burn? u60 tx-sender)) ;; Returns (ok true) (as-contract - (stx-burn? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; returns (err u4) + (stx-burn? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; Returns (err u4) " }; @@ -1585,6 +1637,9 @@ fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { GetAssetOwner => make_for_special(&GET_OWNER, name), TransferToken => make_for_special(&TOKEN_TRANSFER, name), TransferAsset => make_for_special(&ASSET_TRANSFER, name), + BurnToken => make_for_special(&BURN_TOKEN, name), + BurnAsset => make_for_special(&BURN_ASSET, name), + GetTokenSupply => make_for_special(&GET_TOKEN_SUPPLY, name), AtBlock => make_for_special(&AT_BLOCK, name), GetStxBalance => make_for_simple_native(&STX_GET_BALANCE, &GetStxBalance, name), StxTransfer => make_for_simple_native(&STX_TRANSFER, &StxTransfer, name), diff --git a/src/vm/errors.rs b/src/vm/errors.rs index 7e7fffe3fe..33027757cf 100644 --- a/src/vm/errors.rs +++ b/src/vm/errors.rs @@ -71,6 +71,7 @@ pub enum RuntimeErrorType { ArithmeticOverflow, ArithmeticUnderflow, SupplyOverflow(u128, u128), + SupplyUnderflow(u128, u128), DivisionByZero, // error in parsing types ParseError(String), diff --git a/src/vm/functions/assets.rs b/src/vm/functions/assets.rs index 99934bf87f..b7917f6530 100644 --- a/src/vm/functions/assets.rs +++ b/src/vm/functions/assets.rs @@ -48,6 +48,16 @@ enum TransferTokenErrorCodes { SENDER_IS_RECIPIENT = 2, NON_POSITIVE_AMOUNT = 3, } + +enum BurnAssetErrorCodes { + NOT_OWNED_BY = 1, + DOES_NOT_EXIST = 3, +} +enum BurnTokenErrorCodes { + NOT_ENOUGH_BALANCE = 1, + NON_POSITIVE_AMOUNT = 3, +} + enum StxErrorCodes { NOT_ENOUGH_BALANCE = 1, SENDER_IS_RECIPIENT = 2, @@ -550,3 +560,161 @@ pub fn special_get_owner( Err(e) => Err(e), } } + +pub fn special_get_token_supply( + args: &[SymbolicExpression], + env: &mut Environment, + _context: &LocalContext, +) -> Result { + check_argument_count(1, args)?; + + runtime_cost(ClarityCostFunction::FtSupply, env, 0)?; + + let token_name = args[0].match_atom().ok_or(CheckErrors::BadTokenName)?; + + let supply = env + .global_context + .database + .get_ft_supply(&env.contract_context.contract_identifier, token_name)?; + Ok(Value::UInt(supply)) +} + +pub fn special_burn_token( + args: &[SymbolicExpression], + env: &mut Environment, + context: &LocalContext, +) -> Result { + check_argument_count(3, args)?; + + runtime_cost(ClarityCostFunction::FtBurn, env, 0)?; + + let token_name = args[0].match_atom().ok_or(CheckErrors::BadTokenName)?; + + let amount = eval(&args[1], env, context)?; + let from = eval(&args[2], env, context)?; + + if let (Value::UInt(amount), Value::Principal(ref burner)) = (amount, from) { + if amount <= 0 { + return clarity_ecode!(MintTokenErrorCodes::NON_POSITIVE_AMOUNT); + } + + let burner_bal = env.global_context.database.get_ft_balance( + &env.contract_context.contract_identifier, + token_name, + burner, + )?; + + if amount > burner_bal { + return clarity_ecode!(BurnTokenErrorCodes::NOT_ENOUGH_BALANCE); + } + + env.global_context.database.checked_decrease_token_supply( + &env.contract_context.contract_identifier, + token_name, + amount, + )?; + + let final_burner_bal = burner_bal - amount; + + env.global_context.database.set_ft_balance( + &env.contract_context.contract_identifier, + token_name, + burner, + final_burner_bal, + )?; + + let asset_identifier = AssetIdentifier { + contract_identifier: env.contract_context.contract_identifier.clone(), + asset_name: token_name.clone(), + }; + env.register_ft_burn_event(burner.clone(), amount, asset_identifier)?; + + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(TypeSignature::UIntType.size() as u64)?; + + env.global_context.log_token_transfer( + burner, + &env.contract_context.contract_identifier, + token_name, + amount, + )?; + + Ok(Value::okay_true()) + } else { + Err(CheckErrors::BadBurnFTArguments.into()) + } +} + +pub fn special_burn_asset( + args: &[SymbolicExpression], + env: &mut Environment, + context: &LocalContext, +) -> Result { + check_argument_count(3, args)?; + + runtime_cost(ClarityCostFunction::NftBurn, env, 0)?; + + let asset_name = args[0].match_atom().ok_or(CheckErrors::BadTokenName)?; + + let asset = eval(&args[1], env, context)?; + let sender = eval(&args[2], env, context)?; + + let expected_asset_type = env + .global_context + .database + .get_nft_key_type(&env.contract_context.contract_identifier, asset_name)?; + + runtime_cost( + ClarityCostFunction::NftBurn, + env, + expected_asset_type.size(), + )?; + + if !expected_asset_type.admits(&asset) { + return Err(CheckErrors::TypeValueError(expected_asset_type, asset).into()); + } + + if let Value::Principal(ref sender_principal) = sender { + let owner = match env.global_context.database.get_nft_owner( + &env.contract_context.contract_identifier, + asset_name, + &asset, + ) { + Err(Error::Runtime(RuntimeErrorType::NoSuchToken, _)) => { + return clarity_ecode!(BurnAssetErrorCodes::DOES_NOT_EXIST) + } + Ok(owner) => Ok(owner), + Err(e) => Err(e), + }?; + + if &owner != sender_principal { + return clarity_ecode!(BurnAssetErrorCodes::NOT_OWNED_BY); + } + + env.add_memory(TypeSignature::PrincipalType.size() as u64)?; + env.add_memory(expected_asset_type.size() as u64)?; + + env.global_context.database.burn_nft( + &env.contract_context.contract_identifier, + asset_name, + &asset, + )?; + + env.global_context.log_asset_transfer( + sender_principal, + &env.contract_context.contract_identifier, + asset_name, + asset.clone(), + ); + + let asset_identifier = AssetIdentifier { + contract_identifier: env.contract_context.contract_identifier.clone(), + asset_name: asset_name.clone(), + }; + env.register_nft_burn_event(sender_principal.clone(), asset, asset_identifier)?; + + Ok(Value::okay_true()) + } else { + Err(CheckErrors::TypeValueError(TypeSignature::PrincipalType, sender).into()) + } +} diff --git a/src/vm/functions/database.rs b/src/vm/functions/database.rs index 07b3840ac5..a117f28bfa 100644 --- a/src/vm/functions/database.rs +++ b/src/vm/functions/database.rs @@ -51,15 +51,13 @@ pub fn special_contract_call( runtime_cost(ClarityCostFunction::ContractCall, env, 0)?; let function_name = args[1].match_atom().ok_or(CheckErrors::ExpectedName)?; - let rest_args: Vec<_> = { - let rest_args = &args[2..]; - let rest_args: Result> = rest_args.iter().map(|x| eval(x, env, context)).collect(); - let mut rest_args = rest_args?; - rest_args - .drain(..) - .map(|x| SymbolicExpression::atom_value(x)) - .collect() - }; + let mut rest_args = vec![]; + let mut rest_args_sizes = vec![]; + for arg in args[2..].iter() { + let evaluated_arg = eval(arg, env, context)?; + rest_args_sizes.push(evaluated_arg.size() as u64); + rest_args.push(SymbolicExpression::atom_value(evaluated_arg)); + } let (contract_identifier, type_returns_constraint) = match &args[0].expr { SymbolicExpressionType::LiteralValue(Value::Principal(PrincipalData::Contract( @@ -162,14 +160,17 @@ pub fn special_contract_call( )); let mut nested_env = env.nest_with_caller(contract_principal.clone()); - let result = - if nested_env.short_circuit_contract_call(&contract_identifier, function_name, &[])? { - nested_env.run_free(|free_env| { - free_env.execute_contract(&contract_identifier, function_name, &rest_args, false) - }) - } else { - nested_env.execute_contract(&contract_identifier, function_name, &rest_args, false) - }?; + let result = if nested_env.short_circuit_contract_call( + &contract_identifier, + function_name, + &rest_args_sizes, + )? { + nested_env.run_free(|free_env| { + free_env.execute_contract(&contract_identifier, function_name, &rest_args, false) + }) + } else { + nested_env.execute_contract(&contract_identifier, function_name, &rest_args, false) + }?; // Ensure that the expected type from the trait spec admits // the type of the value returned by the dynamic dispatch. diff --git a/src/vm/functions/mod.rs b/src/vm/functions/mod.rs index f1a098ae00..f20c165024 100644 --- a/src/vm/functions/mod.rs +++ b/src/vm/functions/mod.rs @@ -126,6 +126,9 @@ define_named_enum!(NativeFunctions { TransferAsset("nft-transfer?"), MintAsset("nft-mint?"), MintToken("ft-mint?"), + GetTokenSupply("ft-get-supply"), + BurnToken("ft-burn?"), + BurnAsset("nft-burn?"), GetStxBalance("stx-get-balance"), StxTransfer("stx-transfer?"), StxBurn("stx-burn?"), @@ -384,6 +387,12 @@ pub fn lookup_reserved_functions(name: &str) -> Option { } GetTokenBalance => SpecialFunction("special_get_balance", &assets::special_get_balance), GetAssetOwner => SpecialFunction("special_get_owner", &assets::special_get_owner), + BurnAsset => SpecialFunction("special_burn_asset", &assets::special_burn_asset), + BurnToken => SpecialFunction("special_burn_token", &assets::special_burn_token), + GetTokenSupply => SpecialFunction( + "special_get_token_supply", + &assets::special_get_token_supply, + ), AtBlock => SpecialFunction("special_at_block", &database::special_at_block), GetStxBalance => SpecialFunction("special_stx_balance", &assets::special_stx_balance), StxTransfer => SpecialFunction("special_stx_transfer", &assets::special_stx_transfer), diff --git a/src/vm/representations.rs b/src/vm/representations.rs index 54c8cc7cf8..fbb1f0e8c7 100644 --- a/src/vm/representations.rs +++ b/src/vm/representations.rs @@ -76,6 +76,12 @@ macro_rules! guarded_string { Self::try_from(value.to_string()).unwrap() } } + + impl fmt::Display for $Name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } + } }; } diff --git a/src/vm/tests/assets.rs b/src/vm/tests/assets.rs index cc25376b37..81ba003ee5 100644 --- a/src/vm/tests/assets.rs +++ b/src/vm/tests/assets.rs @@ -30,6 +30,8 @@ use vm::types::{AssetIdentifier, PrincipalData, QualifiedContractIdentifier, Res const FIRST_CLASS_TOKENS: &str = "(define-fungible-token stackaroos) (define-read-only (my-ft-get-balance (account principal)) (ft-get-balance stackaroos account)) + (define-read-only (get-total-supply) + (ft-get-supply stackaroos)) (define-public (my-token-transfer (to principal) (amount uint)) (ft-transfer? stackaroos amount tx-sender to)) (define-public (faucet) @@ -39,6 +41,8 @@ const FIRST_CLASS_TOKENS: &str = "(define-fungible-token stackaroos) (if (>= block-height block-to-release) (faucet) (err \"must be in the future\"))) + (define-public (burn (amount uint)) + (ft-burn? stackaroos amount tx-sender)) (begin (ft-mint? stackaroos u10000 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) (ft-mint? stackaroos u200 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G) (ft-mint? stackaroos u4 .tokens))"; @@ -70,6 +74,8 @@ const ASSET_NAMES: &str = (define-public (force-mint (name int)) (nft-mint? names name tx-sender)) + (define-public (force-burn (name int)) + (nft-burn? names name tx-sender)) (define-public (try-bad-transfers) (begin (contract-call? .tokens my-token-transfer burn-address u50000) @@ -530,6 +536,11 @@ fn test_simple_token_system(owned_env: &mut OwnedEnvironment) { _ => panic!(), }; + let p2_principal = match p2 { + Value::Principal(ref data) => data.clone(), + _ => panic!(), + }; + let token_contract_id = QualifiedContractIdentifier::new(p1_principal.clone(), "tokens".into()); let token_identifier = AssetIdentifier { @@ -693,6 +704,71 @@ fn test_simple_token_system(owned_env: &mut OwnedEnvironment) { assert_eq!(result, Value::UInt(1003)); + // Get the total supply - Total minted so far = 10204 + let (result, _asset_map, _events) = execute_transaction( + owned_env, + p1.clone(), + &token_contract_id.clone(), + "get-total-supply", + &symbols_from_values(vec![]), + ) + .unwrap(); + assert_eq!(result, Value::UInt(10204)); + + // Burn 100 tokens from p2's balance (out of 9200) + let (result, asset_map, _events) = execute_transaction( + owned_env, + p2.clone(), + &token_contract_id.clone(), + "burn", + &symbols_from_values(vec![Value::UInt(100)]), + ) + .unwrap(); + + let asset_map = asset_map.to_table(); + assert!(is_committed(&result)); + println!("{:?}", asset_map); + assert_eq!( + asset_map[&p2_principal][&token_identifier], + AssetMapEntry::Token(100) + ); + + // Get p2's balance we should get 9200 - 100 = 9100 + let (result, _asset_map, _events) = execute_transaction( + owned_env, + p1.clone(), + &token_contract_id.clone(), + "my-ft-get-balance", + &symbols_from_values(vec![p2.clone()]), + ) + .unwrap(); + + assert_eq!(result, Value::UInt(9100)); + + // Get the new total supply + let (result, _asset_map, _events) = execute_transaction( + owned_env, + p1.clone(), + &token_contract_id.clone(), + "get-total-supply", + &symbols_from_values(vec![]), + ) + .unwrap(); + assert_eq!(result, Value::UInt(10104)); + + // Burn 9101 tokens from p2's balance (out of 9100) - Should fail with error code 1 + let (result, _asset_map, _events) = execute_transaction( + owned_env, + p2.clone(), + &token_contract_id.clone(), + "burn", + &symbols_from_values(vec![Value::UInt(9101)]), + ) + .unwrap(); + + assert!(!is_committed(&result)); + assert!(is_err_code(&result, 1)); + let (result, asset_map, _events) = execute_transaction( owned_env, p1.clone(), @@ -835,6 +911,11 @@ fn test_simple_naming_system(owned_env: &mut OwnedEnvironment) { _ => panic!(), }; + let p2_principal = match p2 { + Value::Principal(ref data) => data.clone(), + _ => panic!(), + }; + let tokens_contract_id = QualifiedContractIdentifier::new(p1_principal.clone(), "tokens".into()); @@ -1104,6 +1185,71 @@ fn test_simple_naming_system(owned_env: &mut OwnedEnvironment) { // preorder must exist! assert!(is_err_code(&result, 5)); + + // p1 burning 5 should fail (not owner anymore). + let (result, asset_map, _events) = execute_transaction( + owned_env, + p1.clone(), + &names_contract_id, + "force-burn", + &symbols_from_values(vec![Value::Int(5)]), + ) + .unwrap(); + + assert!(!is_committed(&result)); + assert!(is_err_code(&result, 1)); + + // p2 burning 5 should succeed. + let (result, asset_map, _events) = execute_transaction( + owned_env, + p2.clone(), + &names_contract_id, + "force-burn", + &symbols_from_values(vec![Value::Int(5)]), + ) + .unwrap(); + + let asset_map = asset_map.to_table(); + + assert!(is_committed(&result)); + assert_eq!( + asset_map[&p2_principal][&names_identifier], + AssetMapEntry::Asset(vec![Value::Int(5)]) + ); + + // p2 re-burning 5 should succeed. + let (result, asset_map, _events) = execute_transaction( + owned_env, + p2.clone(), + &names_contract_id, + "force-burn", + &symbols_from_values(vec![Value::Int(5)]), + ) + .unwrap(); + assert!(!is_committed(&result)); + assert!(is_err_code(&result, 3)); + + // p1 re-minting 5 should succeed + let (result, asset_map, _events) = execute_transaction( + owned_env, + p1.clone(), + &names_contract_id, + "force-mint", + &symbols_from_values(vec![Value::Int(5)]), + ) + .unwrap(); + + assert!(is_committed(&result)); + assert_eq!(asset_map.to_table().len(), 0); + + { + let mut env = owned_env.get_exec_environment(None); + assert_eq!( + env.eval_read_only(&names_contract_id.clone(), "(nft-get-owner? names 5)") + .unwrap(), + Value::some(p1.clone()).unwrap() + ); + } } #[test] diff --git a/src/vm/tests/costs.rs b/src/vm/tests/costs.rs index 4bf5a44ffa..ec325e5c9e 100644 --- a/src/vm/tests/costs.rs +++ b/src/vm/tests/costs.rs @@ -29,6 +29,7 @@ use vm::tests::{ use vm::types::{AssetIdentifier, PrincipalData, QualifiedContractIdentifier, ResponseData, Value}; use chainstate::burn::BlockHeaderHash; +use chainstate::stacks::boot::{STACKS_BOOT_COST_CONTRACT, STACKS_BOOT_COST_VOTE_CONTRACT}; use chainstate::stacks::events::StacksTransactionEvent; use chainstate::stacks::index::storage::TrieFileStorage; use chainstate::stacks::index::MarfTrieId; @@ -42,6 +43,7 @@ use vm::database::{ use chainstate::stacks::StacksBlockHeader; use core::FIRST_BURNCHAIN_CONSENSUS_HASH; use core::FIRST_STACKS_BLOCK_HASH; +use vm::costs::cost_functions::ClarityCostFunction; pub fn get_simple_test(function: &NativeFunctions) -> &'static str { use vm::functions::NativeFunctions::*; @@ -121,6 +123,9 @@ pub fn get_simple_test(function: &NativeFunctions) -> &'static str { GetAssetOwner => "(nft-get-owner? nft-foo 1)", TransferToken => "(ft-transfer? ft-foo u1 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", TransferAsset => "(nft-transfer? nft-foo 1 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", + BurnToken => "(ft-burn? ft-foo u1 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", + BurnAsset => "(nft-burn? nft-foo 1 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", + GetTokenSupply => "(ft-get-supply ft-foo)", AtBlock => "(at-block 0x55c9861be5cff984a20ce6d99d4aa65941412889bdc665094136429b84f8c2ee 1)", // first stacksblockid GetStxBalance => "(stx-get-balance 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", StxTransfer => "(stx-transfer? u1 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)", @@ -254,15 +259,7 @@ fn test_cost_contract_short_circuits() { ) .commit_block(); - let mut marf_kv = clarity_instance.destroy(); - - marf_kv.begin( - &StacksBlockHeader::make_index_block_hash( - &FIRST_BURNCHAIN_CONSENSUS_HASH, - &FIRST_STACKS_BLOCK_HASH, - ), - &StacksBlockId([1 as u8; 32]), - ); + let marf_kv = clarity_instance.destroy(); let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); let p2 = execute("'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G"); @@ -277,9 +274,16 @@ fn test_cost_contract_short_circuits() { let intercepted = QualifiedContractIdentifier::new(p1_principal.clone(), "intercepted".into()); let caller = QualifiedContractIdentifier::new(p1_principal.clone(), "caller".into()); - { - let mut owned_env = OwnedEnvironment::new_max_limit( - marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), + let mut marf_kv = { + let mut clarity_inst = ClarityInstance::new(marf_kv, ExecutionCost::max_value()); + let mut block_conn = clarity_inst.begin_block( + &StacksBlockHeader::make_index_block_hash( + &FIRST_BURNCHAIN_CONSENSUS_HASH, + &FIRST_STACKS_BLOCK_HASH, + ), + &StacksBlockId([1 as u8; 32]), + &NULL_HEADER_DB, + &NULL_BURN_STATE_DB, ); let cost_definer_src = " @@ -302,20 +306,27 @@ fn test_cost_contract_short_circuits() { (ok (contract-call? .intercepted intercepted-function a))) "; - owned_env - .initialize_contract(cost_definer.clone(), cost_definer_src) - .unwrap(); - owned_env - .initialize_contract(intercepted.clone(), intercepted_src) - .unwrap(); - owned_env - .initialize_contract(caller.clone(), caller_src) - .unwrap(); - - let (_db, _tracker) = owned_env.destruct().unwrap(); - } + for (contract_name, contract_src) in [ + (&cost_definer, cost_definer_src), + (&intercepted, intercepted_src), + (&caller, caller_src), + ] + .iter() + { + block_conn.as_transaction(|tx| { + let (ast, analysis) = tx + .analyze_smart_contract(contract_name, contract_src) + .unwrap(); + tx.initialize_smart_contract(contract_name, &ast, contract_src, |_, _| false) + .unwrap(); + tx.save_analysis(contract_name, &analysis).unwrap(); + }); + } + + block_conn.commit_block(); + clarity_inst.destroy() + }; - marf_kv.test_commit(); marf_kv.begin(&StacksBlockId([1 as u8; 32]), &StacksBlockId([2 as u8; 32])); let without_interposing_5 = { @@ -356,25 +367,39 @@ fn test_cost_contract_short_circuits() { tracker.get_total() }; + { + let mut db = marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB); + db.begin(); + db.set_variable( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "confirmed-proposal-count", + Value::UInt(1), + ) + .unwrap(); + let value = format!( + "{{ function-contract: '{}, + function-name: {}, + cost-function-contract: '{}, + cost-function-name: {}, + confirmed-height: u1 }}", + intercepted, "\"intercepted-function\"", cost_definer, "\"cost-definition\"" + ); + db.set_entry( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "confirmed-proposals", + execute("{ confirmed-id: u0 }"), + execute(&value), + ) + .unwrap(); + db.commit(); + } + marf_kv.test_commit(); marf_kv.begin(&StacksBlockId([2 as u8; 32]), &StacksBlockId([3 as u8; 32])); let with_interposing_5 = { - let cost_tracker = LimitedCostTracker::new_max_limit_with_circuits( - &mut marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), - vec![( - (intercepted.clone(), "intercepted-function".into()), - ClarityCostFunctionReference { - contract_id: cost_definer.clone(), - function_name: "cost-definition".into(), - }, - )], - ) - .unwrap(); - - let mut owned_env = OwnedEnvironment::new_cost_limited( + let mut owned_env = OwnedEnvironment::new_max_limit( marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), - cost_tracker, ); execute_transaction( @@ -392,21 +417,8 @@ fn test_cost_contract_short_circuits() { }; let with_interposing_10 = { - let cost_tracker = LimitedCostTracker::new_max_limit_with_circuits( - &mut marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), - vec![( - (intercepted.clone(), "intercepted-function".into()), - ClarityCostFunctionReference { - contract_id: cost_definer.clone(), - function_name: "cost-definition".into(), - }, - )], - ) - .unwrap(); - - let mut owned_env = OwnedEnvironment::new_cost_limited( + let mut owned_env = OwnedEnvironment::new_max_limit( marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), - cost_tracker, ); execute_transaction( @@ -429,3 +441,390 @@ fn test_cost_contract_short_circuits() { assert_eq!(with_interposing_5, with_interposing_10); assert!(without_interposing_5 != without_interposing_10); } + +#[test] +fn test_cost_voting_integration() { + let marf_kv = MarfedKV::temporary(); + let mut clarity_instance = ClarityInstance::new(marf_kv, ExecutionCost::max_value()); + clarity_instance + .begin_test_genesis_block( + &StacksBlockId::sentinel(), + &StacksBlockHeader::make_index_block_hash( + &FIRST_BURNCHAIN_CONSENSUS_HASH, + &FIRST_STACKS_BLOCK_HASH, + ), + &NULL_HEADER_DB, + &NULL_BURN_STATE_DB, + ) + .commit_block(); + + let marf_kv = clarity_instance.destroy(); + + let p1 = execute("'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR"); + let p2 = execute("'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G"); + + let p1_principal = match p1 { + Value::Principal(PrincipalData::Standard(ref data)) => data.clone(), + _ => panic!(), + }; + + let cost_definer = + QualifiedContractIdentifier::new(p1_principal.clone(), "cost-definer".into()); + let bad_cost_definer = + QualifiedContractIdentifier::new(p1_principal.clone(), "bad-cost-definer".into()); + let bad_cost_args_definer = + QualifiedContractIdentifier::new(p1_principal.clone(), "bad-cost-args-definer".into()); + let intercepted = QualifiedContractIdentifier::new(p1_principal.clone(), "intercepted".into()); + let caller = QualifiedContractIdentifier::new(p1_principal.clone(), "caller".into()); + + let mut marf_kv = { + let mut clarity_inst = ClarityInstance::new(marf_kv, ExecutionCost::max_value()); + let mut block_conn = clarity_inst.begin_block( + &StacksBlockHeader::make_index_block_hash( + &FIRST_BURNCHAIN_CONSENSUS_HASH, + &FIRST_STACKS_BLOCK_HASH, + ), + &StacksBlockId([1 as u8; 32]), + &NULL_HEADER_DB, + &NULL_BURN_STATE_DB, + ); + + let cost_definer_src = " + (define-read-only (cost-definition (size uint)) + { + runtime: u1, write_length: u1, write_count: u1, read_count: u1, read_length: u1 + }) + (define-read-only (cost-definition-le (size uint)) + { + runtime: u0, write_length: u0, write_count: u0, read_count: u0, read_length: u0 + }) + (define-read-only (cost-definition-multi-arg (a uint) (b uint) (c uint)) + { + runtime: u1, write_length: u0, write_count: u0, read_count: u0, read_length: u0 + }) + + "; + + let bad_cost_definer_src = " + (define-data-var my-var uint u10) + (define-read-only (cost-definition (size uint)) + { + runtime: (var-get my-var), write_length: u1, write_count: u1, read_count: u1, read_length: u1 + }) + "; + + let bad_cost_args_definer_src = " + (define-read-only (cost-definition (a uint) (b uint)) + { + runtime: u1, write_length: u1, write_count: u1, read_count: u1, read_length: u1 + }) + "; + + let intercepted_src = " + (define-read-only (intercepted-function (a uint)) + (if (>= a u10) + (+ (+ a a) (+ a a) + (+ a a) (+ a a)) + u0)) + + (define-read-only (intercepted-function2 (a uint) (b uint) (c uint)) + (- (+ a b) c)) + + (define-public (non-read-only) (ok (+ 1 2 3))) + "; + + let caller_src = " + (define-public (execute (a uint)) + (ok (contract-call? .intercepted intercepted-function a))) + (define-public (execute-2 (a uint)) + (ok (< a a))) + "; + + for (contract_name, contract_src) in [ + (&cost_definer, cost_definer_src), + (&intercepted, intercepted_src), + (&caller, caller_src), + (&bad_cost_definer, bad_cost_definer_src), + (&bad_cost_args_definer, bad_cost_args_definer_src), + ] + .iter() + { + block_conn.as_transaction(|tx| { + let (ast, analysis) = tx + .analyze_smart_contract(contract_name, contract_src) + .unwrap(); + tx.initialize_smart_contract(contract_name, &ast, contract_src, |_, _| false) + .unwrap(); + tx.save_analysis(contract_name, &analysis).unwrap(); + }); + } + + block_conn.commit_block(); + clarity_inst.destroy() + }; + + marf_kv.begin(&StacksBlockId([1 as u8; 32]), &StacksBlockId([2 as u8; 32])); + + let bad_cases = vec![ + // non existent "replacement target" + ( + PrincipalData::from(QualifiedContractIdentifier::local("non-existent").unwrap()), + "non-existent-func", + PrincipalData::from(cost_definer.clone()), + "cost-definition", + ), + // replacement target isn't a contract principal + ( + p1_principal.clone().into(), + "non-existent-func", + cost_definer.clone().into(), + "cost-definition", + ), + // cost defining contract isn't a contract principal + ( + intercepted.clone().into(), + "intercepted-function", + p1_principal.clone().into(), + "cost-definition", + ), + // replacement function doesn't exist + ( + intercepted.clone().into(), + "non-existent-func", + cost_definer.clone().into(), + "cost-definition", + ), + // replacement function isn't read-only + ( + intercepted.clone().into(), + "non-read-only", + cost_definer.clone().into(), + "cost-definition", + ), + // "boot cost" function doesn't exist + ( + (*STACKS_BOOT_COST_CONTRACT).clone().into(), + "non-existent-func", + cost_definer.clone().into(), + "cost-definition", + ), + // cost defining contract doesn't exist + ( + intercepted.clone().into(), + "intercepted-function", + QualifiedContractIdentifier::local("non-existent") + .unwrap() + .into(), + "cost-definition", + ), + // cost defining function doesn't exist + ( + intercepted.clone().into(), + "intercepted-function", + cost_definer.clone().into(), + "cost-definition-2", + ), + // cost defining contract isn't arithmetic-only + ( + intercepted.clone().into(), + "intercepted-function", + bad_cost_definer.clone().into(), + "cost-definition", + ), + // cost defining contract has incorrect number of arguments + ( + intercepted.clone().into(), + "intercepted-function", + bad_cost_args_definer.clone().into(), + "cost-definition", + ), + ]; + + let bad_proposals = bad_cases.len(); + + { + let mut db = marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB); + db.begin(); + + db.set_variable( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "confirmed-proposal-count", + Value::UInt(bad_proposals as u128), + ) + .unwrap(); + + for (ix, (intercepted_ct, intercepted_f, cost_ct, cost_f)) in + bad_cases.into_iter().enumerate() + { + let value = format!( + "{{ function-contract: '{}, + function-name: \"{}\", + cost-function-contract: '{}, + cost-function-name: \"{}\", + confirmed-height: u1 }}", + intercepted_ct, intercepted_f, cost_ct, cost_f + ); + db.set_entry( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "confirmed-proposals", + execute(&format!("{{ confirmed-id: u{} }}", ix)), + execute(&value), + ) + .unwrap(); + } + db.commit(); + } + + marf_kv.test_commit(); + marf_kv.begin(&StacksBlockId([2 as u8; 32]), &StacksBlockId([3 as u8; 32])); + + let le_cost_without_interception = { + let mut owned_env = OwnedEnvironment::new_max_limit( + marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), + ); + + execute_transaction( + &mut owned_env, + p2.clone(), + &caller, + "execute-2", + &symbols_from_values(vec![Value::UInt(5)]), + ) + .unwrap(); + + let (_db, tracker) = owned_env.destruct().unwrap(); + + assert!( + tracker.contract_call_circuits().is_empty(), + "No contract call circuits should have been processed" + ); + for (target, referenced_function) in tracker.cost_function_references().into_iter() { + assert_eq!( + &referenced_function.contract_id, &*STACKS_BOOT_COST_CONTRACT, + "All cost functions should still point to the boot costs" + ); + assert_eq!( + &referenced_function.function_name, + target.get_name_str(), + "All cost functions should still point to the boot costs" + ); + } + + tracker.get_total() + }; + + let good_cases = vec![ + ( + intercepted.clone(), + "intercepted-function", + cost_definer.clone(), + "cost-definition", + ), + ( + (*STACKS_BOOT_COST_CONTRACT).clone(), + "cost_le", + cost_definer.clone(), + "cost-definition-le", + ), + ( + intercepted.clone(), + "intercepted-function2", + cost_definer.clone(), + "cost-definition-multi-arg", + ), + ]; + + marf_kv.test_commit(); + marf_kv.begin(&StacksBlockId([3 as u8; 32]), &StacksBlockId([4 as u8; 32])); + + { + let mut db = marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB); + db.begin(); + + let good_proposals = good_cases.len() as u128; + db.set_variable( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "confirmed-proposal-count", + Value::UInt(bad_proposals as u128 + good_proposals), + ) + .unwrap(); + + for (ix, (intercepted_ct, intercepted_f, cost_ct, cost_f)) in + good_cases.into_iter().enumerate() + { + let value = format!( + "{{ function-contract: '{}, + function-name: \"{}\", + cost-function-contract: '{}, + cost-function-name: \"{}\", + confirmed-height: u1 }}", + intercepted_ct, intercepted_f, cost_ct, cost_f + ); + db.set_entry( + &STACKS_BOOT_COST_VOTE_CONTRACT, + "confirmed-proposals", + execute(&format!("{{ confirmed-id: u{} }}", ix + bad_proposals)), + execute(&value), + ) + .unwrap(); + } + db.commit(); + } + + marf_kv.test_commit(); + marf_kv.begin(&StacksBlockId([4 as u8; 32]), &StacksBlockId([5 as u8; 32])); + + { + let mut owned_env = OwnedEnvironment::new_max_limit( + marf_kv.as_clarity_db(&NULL_HEADER_DB, &NULL_BURN_STATE_DB), + ); + + execute_transaction( + &mut owned_env, + p2.clone(), + &caller, + "execute-2", + &symbols_from_values(vec![Value::UInt(5)]), + ) + .unwrap(); + + let (_db, tracker) = owned_env.destruct().unwrap(); + + // cost of `le` should be less now, because the proposal made it free + assert!(le_cost_without_interception.exceeds(&tracker.get_total())); + + let circuits = tracker.contract_call_circuits(); + assert_eq!(circuits.len(), 2); + + let circuit1 = circuits.get(&(intercepted.clone(), "intercepted-function".into())); + let circuit2 = circuits.get(&(intercepted.clone(), "intercepted-function2".into())); + + assert!(circuit1.is_some()); + assert!(circuit2.is_some()); + + assert_eq!(circuit1.unwrap().contract_id, cost_definer); + assert_eq!(circuit1.unwrap().function_name, "cost-definition"); + + assert_eq!(circuit2.unwrap().contract_id, cost_definer); + assert_eq!(circuit2.unwrap().function_name, "cost-definition-multi-arg"); + + for (target, referenced_function) in tracker.cost_function_references().into_iter() { + if target == &ClarityCostFunction::Le { + assert_eq!(&referenced_function.contract_id, &cost_definer); + assert_eq!(&referenced_function.function_name, "cost-definition-le"); + } else { + assert_eq!( + &referenced_function.contract_id, &*STACKS_BOOT_COST_CONTRACT, + "Cost function should still point to the boot costs" + ); + assert_eq!( + &referenced_function.function_name, + target.get_name_str(), + "Cost function should still point to the boot costs" + ); + } + } + }; + + marf_kv.test_commit(); +} diff --git a/src/vm/types/mod.rs b/src/vm/types/mod.rs index 52ba124ed5..762726e0a3 100644 --- a/src/vm/types/mod.rs +++ b/src/vm/types/mod.rs @@ -67,7 +67,7 @@ pub struct ListData { pub type_signature: ListTypeData, } -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)] +#[derive(Clone, Eq, PartialEq, Hash, Serialize, Deserialize, PartialOrd, Ord)] pub struct StandardPrincipalData(pub u8, pub [u8; 20]); impl StandardPrincipalData { @@ -824,6 +824,15 @@ impl Value { )))) } + pub fn expect_ascii(self) -> String { + if let Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { data }))) = self { + String::from_utf8(data).unwrap() + } else { + error!("Value '{:?}' is not an ASCII string", &self); + panic!(); + } + } + pub fn expect_u128(self) -> u128 { if let Value::UInt(inner) = self { inner @@ -1122,6 +1131,13 @@ impl fmt::Display for StandardPrincipalData { } } +impl fmt::Debug for StandardPrincipalData { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let c32_str = self.to_address(); + write!(f, "StandardPrincipalData({})", c32_str) + } +} + impl fmt::Display for PrincipalData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { diff --git a/src/vm/types/signatures.rs b/src/vm/types/signatures.rs index 3417617f67..eec60c09d6 100644 --- a/src/vm/types/signatures.rs +++ b/src/vm/types/signatures.rs @@ -62,7 +62,7 @@ impl AssetIdentifier { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TupleTypeSignature { type_map: BTreeMap, } @@ -1271,6 +1271,16 @@ impl fmt::Display for TupleTypeSignature { } } +impl fmt::Debug for TupleTypeSignature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "TupleTypeSignature {{")?; + for (field_name, field_type) in self.type_map.iter() { + write!(f, " \"{}\": {},", &**field_name, field_type)?; + } + write!(f, "}}") + } +} + impl fmt::Display for AssetIdentifier { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( diff --git a/testnet/stacks-node/src/burnchains/mocknet_controller.rs b/testnet/stacks-node/src/burnchains/mocknet_controller.rs index 054df4790e..d8e5f60ab8 100644 --- a/testnet/stacks-node/src/burnchains/mocknet_controller.rs +++ b/testnet/stacks-node/src/burnchains/mocknet_controller.rs @@ -12,8 +12,8 @@ use stacks::burnchains::{ }; use stacks::chainstate::burn::db::sortdb::{PoxId, SortitionDB, SortitionHandleTx}; use stacks::chainstate::burn::operations::{ - BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, StackStxOp, - TransferStxOp, UserBurnSupportOp, + leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS, BlockstackOperationType, LeaderBlockCommitOp, + LeaderKeyRegisterOp, PreStxOp, StackStxOp, TransferStxOp, UserBurnSupportOp, }; use stacks::chainstate::burn::BlockSnapshot; use stacks::util::get_epoch_time_secs; @@ -171,6 +171,11 @@ impl BurnchainController for MocknetController { txid, vtxindex: vtxindex, block_height: next_block_header.block_height, + burn_parent_modulus: if next_block_header.block_height > 0 { + (next_block_header.block_height - 1) % BURN_BLOCK_MINED_AT_MODULUS + } else { + BURN_BLOCK_MINED_AT_MODULUS - 1 + } as u8, burn_header_hash: next_block_header.block_hash, }) } diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index d4d6fa1da7..23db914c2f 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -236,7 +236,7 @@ impl ConfigFile { rpc_port: Some(18332), peer_port: Some(18333), peer_host: Some("bitcoind.xenon.blockstack.org".to_string()), - magic_bytes: Some("Xe".into()), + magic_bytes: Some("X2".into()), ..BurnchainConfigFile::default() }; diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 381a2a42b2..aa9d5dfb01 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -421,6 +421,13 @@ impl EventDispatcher { &mut dispatch_matrix, ); } + StacksTransactionEvent::NFTEvent(NFTEventType::NFTBurnEvent(event_data)) => { + self.update_dispatch_matrix_if_observer_subscribed( + &event_data.asset_identifier, + i, + &mut dispatch_matrix, + ); + } StacksTransactionEvent::FTEvent(FTEventType::FTTransferEvent(event_data)) => { self.update_dispatch_matrix_if_observer_subscribed( &event_data.asset_identifier, @@ -435,6 +442,13 @@ impl EventDispatcher { &mut dispatch_matrix, ); } + StacksTransactionEvent::FTEvent(FTEventType::FTBurnEvent(event_data)) => { + self.update_dispatch_matrix_if_observer_subscribed( + &event_data.asset_identifier, + i, + &mut dispatch_matrix, + ); + } } events.push((!receipt.post_condition_aborted, tx_hash, event)); for o_i in &self.any_event_observers_lookup { diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 89b7786303..33000caca0 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -13,8 +13,8 @@ use std::{thread, thread::JoinHandle}; use stacks::burnchains::{Burnchain, BurnchainHeaderHash, BurnchainParameters, Txid}; use stacks::chainstate::burn::db::sortdb::{SortitionDB, SortitionId}; use stacks::chainstate::burn::operations::{ - leader_block_commit::RewardSetInfo, BlockstackOperationType, LeaderBlockCommitOp, - LeaderKeyRegisterOp, + leader_block_commit::{RewardSetInfo, BURN_BLOCK_MINED_AT_MODULUS}, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use stacks::chainstate::burn::BlockSnapshot; use stacks::chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed}; @@ -237,8 +237,10 @@ fn inner_generate_block_commit_op( vrf_seed: VRFSeed, commit_outs: Vec, sunset_burn: u64, + current_burn_height: u64, ) -> BlockstackOperationType { let (parent_block_ptr, parent_vtxindex) = (parent_burnchain_height, parent_winning_vtx); + let burn_parent_modulus = (current_burn_height % BURN_BLOCK_MINED_AT_MODULUS) as u8; BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { sunset_burn, @@ -256,6 +258,7 @@ fn inner_generate_block_commit_op( txid: Txid([0u8; 32]), block_height: 0, burn_header_hash: BurnchainHeaderHash::zero(), + burn_parent_modulus, commit_outs, }) } @@ -1516,7 +1519,9 @@ impl InitializedNeonNode { let sunset_burn = burnchain.expected_sunset_burn(burn_block.block_height + 1, burn_fee_cap); let rest_commit = burn_fee_cap - sunset_burn; - let commit_outs = if burn_block.block_height + 1 < burnchain.pox_constants.sunset_end { + let commit_outs = if burn_block.block_height + 1 < burnchain.pox_constants.sunset_end + && !burnchain.is_in_prepare_phase(burn_block.block_height + 1) + { RewardSetInfo::into_commit_outs(recipients, false) } else { vec![StacksAddress::burn_address(false)] @@ -1535,6 +1540,7 @@ impl InitializedNeonNode { VRFSeed::from_proof(&vrf_proof), commit_outs, sunset_burn, + burn_block.block_height, ); let mut op_signer = keychain.generate_op_signer(); debug!( diff --git a/testnet/stacks-node/src/node.rs b/testnet/stacks-node/src/node.rs index 10cef85201..789d10c121 100644 --- a/testnet/stacks-node/src/node.rs +++ b/testnet/stacks-node/src/node.rs @@ -12,8 +12,8 @@ use std::{thread, thread::JoinHandle, time}; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::operations::{ - leader_block_commit::RewardSetInfo, BlockstackOperationType, LeaderBlockCommitOp, - LeaderKeyRegisterOp, + leader_block_commit::{RewardSetInfo, BURN_BLOCK_MINED_AT_MODULUS}, + BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use stacks::chainstate::burn::{BlockHeaderHash, ConsensusHash, VRFSeed}; use stacks::chainstate::stacks::db::{ @@ -772,7 +772,17 @@ impl Node { ), }; - let commit_outs = RewardSetInfo::into_commit_outs(None, false); + let burnchain = Burnchain::regtest(&self.config.get_burn_db_path()); + let commit_outs = if burnchain_tip.block_snapshot.block_height + 1 + < burnchain.pox_constants.sunset_end + && !burnchain.is_in_prepare_phase(burnchain_tip.block_snapshot.block_height + 1) + { + RewardSetInfo::into_commit_outs(None, false) + } else { + vec![StacksAddress::burn_address(false)] + }; + let burn_parent_modulus = + (burnchain_tip.block_snapshot.block_height % BURN_BLOCK_MINED_AT_MODULUS) as u8; BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { sunset_burn: 0, @@ -791,6 +801,7 @@ impl Node { commit_outs, block_height: 0, burn_header_hash: BurnchainHeaderHash::zero(), + burn_parent_modulus, }) } diff --git a/testnet/stacks-node/src/run_loop/mod.rs b/testnet/stacks-node/src/run_loop/mod.rs index 510bbbe4a5..a016473bc2 100644 --- a/testnet/stacks-node/src/run_loop/mod.rs +++ b/testnet/stacks-node/src/run_loop/mod.rs @@ -110,7 +110,7 @@ impl RunLoopCallbacks { TransactionAuth::Standard(TransactionSpendingCondition::Singlesig(auth)) => { println!( "-> Tx issued by {:?} (fee: {}, nonce: {})", - auth.signer, auth.fee_rate, auth.nonce + auth.signer, auth.tx_fee, auth.nonce ) } _ => println!("-> Tx {:?}", tx.auth), diff --git a/testnet/stacks-node/src/tests/integrations.rs b/testnet/stacks-node/src/tests/integrations.rs index d74bc827a8..39227e9d38 100644 --- a/testnet/stacks-node/src/tests/integrations.rs +++ b/testnet/stacks-node/src/tests/integrations.rs @@ -773,9 +773,7 @@ fn contract_stx_transfer() { .unwrap_err() { MemPoolRejection::ConflictingNonceInMempool => (), - e => { - panic!("{:?}", e) - } + e => panic!("{:?}", e), }; } diff --git a/testnet/stacks-node/src/tests/mempool.rs b/testnet/stacks-node/src/tests/mempool.rs index 13ac31956a..22c8f16761 100644 --- a/testnet/stacks-node/src/tests/mempool.rs +++ b/testnet/stacks-node/src/tests/mempool.rs @@ -40,7 +40,7 @@ const BAD_TRAIT_CONTRACT: &'static str = "(define-public (foo-bar) (ok u1))"; pub fn make_bad_stacks_transfer( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, recipient: &PrincipalData, amount: u64, ) -> Vec { @@ -51,7 +51,7 @@ pub fn make_bad_stacks_transfer( TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private(sender)) .expect("Failed to create p2pkh spending condition from public key."); spending_condition.set_nonce(nonce); - spending_condition.set_fee_rate(fee_rate); + spending_condition.set_tx_fee(tx_fee); let auth = TransactionAuth::Standard(spending_condition); let mut unsigned_tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload); diff --git a/testnet/stacks-node/src/tests/mod.rs b/testnet/stacks-node/src/tests/mod.rs index 32b1a05f9d..99003320b1 100644 --- a/testnet/stacks-node/src/tests/mod.rs +++ b/testnet/stacks-node/src/tests/mod.rs @@ -68,13 +68,13 @@ pub fn serialize_sign_standard_single_sig_tx( payload: TransactionPayload, sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, ) -> Vec { serialize_sign_standard_single_sig_tx_anchor_mode( payload, sender, nonce, - fee_rate, + tx_fee, TransactionAnchorMode::OnChainOnly, ) } @@ -83,14 +83,14 @@ pub fn serialize_sign_standard_single_sig_tx_anchor_mode( payload: TransactionPayload, sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, anchor_mode: TransactionAnchorMode, ) -> Vec { let mut spending_condition = TransactionSpendingCondition::new_singlesig_p2pkh(StacksPublicKey::from_private(sender)) .expect("Failed to create p2pkh spending condition from public key."); spending_condition.set_nonce(nonce); - spending_condition.set_fee_rate(fee_rate); + spending_condition.set_tx_fee(tx_fee); let auth = TransactionAuth::Standard(spending_condition); let mut unsigned_tx = StacksTransaction::new(TransactionVersion::Testnet, auth, payload); unsigned_tx.anchor_mode = anchor_mode; @@ -112,7 +112,7 @@ pub fn serialize_sign_standard_single_sig_tx_anchor_mode( pub fn make_contract_publish( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, contract_name: &str, contract_content: &str, ) -> Vec { @@ -121,13 +121,13 @@ pub fn make_contract_publish( let payload = TransactionSmartContract { name, code_body }; - serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate) + serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee) } pub fn make_contract_publish_microblock_only( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, contract_name: &str, contract_content: &str, ) -> Vec { @@ -140,7 +140,7 @@ pub fn make_contract_publish_microblock_only( payload.into(), sender, nonce, - fee_rate, + tx_fee, TransactionAnchorMode::OffChainOnly, ) } @@ -186,19 +186,19 @@ pub fn to_addr(sk: &StacksPrivateKey) -> StacksAddress { pub fn make_stacks_transfer( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, recipient: &PrincipalData, amount: u64, ) -> Vec { let payload = TransactionPayload::TokenTransfer(recipient.clone(), amount, TokenTransferMemo([0; 34])); - serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate) + serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee) } pub fn make_stacks_transfer_mblock_only( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, recipient: &PrincipalData, amount: u64, ) -> Vec { @@ -208,7 +208,7 @@ pub fn make_stacks_transfer_mblock_only( payload.into(), sender, nonce, - fee_rate, + tx_fee, TransactionAnchorMode::OffChainOnly, ) } @@ -216,23 +216,23 @@ pub fn make_stacks_transfer_mblock_only( pub fn make_poison( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, header_1: StacksMicroblockHeader, header_2: StacksMicroblockHeader, ) -> Vec { let payload = TransactionPayload::PoisonMicroblock(header_1, header_2); - serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate) + serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee) } -pub fn make_coinbase(sender: &StacksPrivateKey, nonce: u64, fee_rate: u64) -> Vec { +pub fn make_coinbase(sender: &StacksPrivateKey, nonce: u64, tx_fee: u64) -> Vec { let payload = TransactionPayload::Coinbase(CoinbasePayload([0; 32])); - serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate) + serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee) } pub fn make_contract_call( sender: &StacksPrivateKey, nonce: u64, - fee_rate: u64, + tx_fee: u64, contract_addr: &StacksAddress, contract_name: &str, function_name: &str, @@ -248,7 +248,7 @@ pub fn make_contract_call( function_args: function_args.iter().map(|x| x.clone()).collect(), }; - serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, fee_rate) + serialize_sign_standard_single_sig_tx(payload.into(), sender, nonce, tx_fee) } fn make_microblock( diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index f6f1a80a5a..9dc6db8a60 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -62,7 +62,7 @@ fn neon_integration_test_conf() -> (Config, StacksAddress) { let magic_bytes = Config::from_config_file(ConfigFile::xenon()) .burnchain .magic_bytes; - assert_eq!(magic_bytes.as_bytes(), &['X' as u8, 'e' as u8]); + assert_eq!(magic_bytes.as_bytes(), &['X' as u8, '2' as u8]); conf.burnchain.magic_bytes = magic_bytes; conf.burnchain.poll_time_secs = 1; conf.node.pox_sync_sample_secs = 1; @@ -1270,8 +1270,20 @@ fn pox_integration_test() { .expect("Failed starting bitcoind"); let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new(10, 5, 4, 5, 15, 239, 250); - burnchain_config.pox_constants = pox_constants; + + // reward cycle length = 15, so 10 reward cycle slots + 5 prepare-phase burns + let reward_cycle_len = 15; + let prepare_phase_len = 5; + let pox_constants = PoxConstants::new( + reward_cycle_len, + prepare_phase_len, + 4 * prepare_phase_len / 5, + 5, + 15, + (16 * reward_cycle_len - 1).into(), + (17 * reward_cycle_len).into(), + ); + burnchain_config.pox_constants = pox_constants.clone(); let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( conf.clone(), @@ -1382,7 +1394,7 @@ fn pox_integration_test() { eprintln!("Sort height: {}", sort_height); // now let's mine until the next reward cycle starts ... - while sort_height < 222 { + while sort_height < ((14 * pox_constants.reward_cycle_length) + 1).into() { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); sort_height = channel.get_sortitions_processed(); eprintln!("Sort height: {}", sort_height); @@ -1417,8 +1429,11 @@ fn pox_integration_test() { let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); let parsed = >::deserialize(&raw_result[2..]); + // should unlock at height 300 (we're in reward cycle 13, lockup starts in reward cycle + // 14, and goes for 6 blocks, so we unlock in reward cycle 20, which with a reward + // cycle length of 15 blocks, is a burnchain height of 300) assert_eq!(parsed.to_string(), - format!("(ok (tuple (lock-amount u1000000000000000) (stacker {}) (unlock-burn-height u270)))", + format!("(ok (tuple (lock-amount u1000000000000000) (stacker {}) (unlock-burn-height u300)))", &spender_addr)); tested = true; } @@ -1519,7 +1534,7 @@ fn pox_integration_test() { // mine until the end of the current reward cycle. sort_height = channel.get_sortitions_processed(); - while sort_height < 229 { + while sort_height < ((15 * pox_constants.reward_cycle_length) - 1).into() { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); sort_height = channel.get_sortitions_processed(); eprintln!("Sort height: {}", sort_height); @@ -1538,7 +1553,7 @@ fn pox_integration_test() { // before sunset // mine until the end of the next reward cycle, // the participation threshold now should be met. - while sort_height < 239 { + while sort_height < ((16 * pox_constants.reward_cycle_length) - 1).into() { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); sort_height = channel.get_sortitions_processed(); eprintln!("Sort height: {}", sort_height); @@ -1575,10 +1590,11 @@ fn pox_integration_test() { .json::() .unwrap(); + eprintln!("Stacks tip is now {}", tip_info.stacks_tip_height); assert_eq!(tip_info.stacks_tip_height, 36); // now let's mine into the sunset - while sort_height < 249 { + while sort_height < ((17 * pox_constants.reward_cycle_length) - 1).into() { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); sort_height = channel.get_sortitions_processed(); eprintln!("Sort height: {}", sort_height); @@ -1593,7 +1609,8 @@ fn pox_integration_test() { .json::() .unwrap(); - assert_eq!(tip_info.stacks_tip_height, 46); + eprintln!("Stacks tip is now {}", tip_info.stacks_tip_height); + assert_eq!(tip_info.stacks_tip_height, 51); let utxos = btc_regtest_controller.get_all_utxos(&pox_2_pubkey); @@ -1606,7 +1623,7 @@ fn pox_integration_test() { ); // and after sunset - while sort_height < 259 { + while sort_height < ((18 * pox_constants.reward_cycle_length) - 1).into() { next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); sort_height = channel.get_sortitions_processed(); eprintln!("Sort height: {}", sort_height); @@ -1632,7 +1649,8 @@ fn pox_integration_test() { .json::() .unwrap(); - assert_eq!(tip_info.stacks_tip_height, 56); + eprintln!("Stacks tip is now {}", tip_info.stacks_tip_height); + assert_eq!(tip_info.stacks_tip_height, 66); test_observer::clear(); channel.stop_chains_coordinator(); @@ -1949,7 +1967,7 @@ fn atlas_integration_test() { // Poll GET v2/attachments/ for i in 1..10 { let mut attachments_did_sync = false; - let mut timeout = 30; + let mut timeout = 60; while attachments_did_sync != true { let zonefile_hex = hex_bytes(&format!("facade0{}", i)).unwrap(); let hashed_zonefile = Hash160::from_data(&zonefile_hex); @@ -1971,7 +1989,7 @@ fn atlas_integration_test() { } else { timeout -= 1; if timeout == 0 { - panic!("Failed syncing 9 attachments between 2 neon runloops within 30s - Something is wrong"); + panic!("Failed syncing 9 attachments between 2 neon runloops within 60s - Something is wrong"); } eprintln!("Attachment {} not sync'd yet", bytes_to_hex(&zonefile_hex)); thread::sleep(Duration::from_millis(1000)); @@ -1987,57 +2005,151 @@ fn atlas_integration_test() { channel.stop_chains_coordinator(); }); - let follower_node_thread = thread::spawn(move || { - // Start the attached observer - test_observer::spawn(); + // Start the attached observer + test_observer::spawn(); - // The bootstrap node mined a few blocks and is ready, let's setup this node. - match follower_node_rx.recv() { - Ok(Signal::BootstrapNodeReady) => { - println!("Booting follower node..."); - } - _ => panic!("Bootstrap node could nod boot. Aborting test."), - }; + // The bootstrap node mined a few blocks and is ready, let's setup this node. + match follower_node_rx.recv() { + Ok(Signal::BootstrapNodeReady) => { + println!("Booting follower node..."); + } + _ => panic!("Bootstrap node could nod boot. Aborting test."), + }; - let burnchain_config = Burnchain::regtest(&conf_follower_node.get_burn_db_path()); - let http_origin = format!("http://{}", &conf_follower_node.node.rpc_bind); + let burnchain_config = Burnchain::regtest(&conf_follower_node.get_burn_db_path()); + let http_origin = format!("http://{}", &conf_follower_node.node.rpc_bind); - eprintln!("Chain bootstrapped..."); + eprintln!("Chain bootstrapped..."); - let mut run_loop = neon::RunLoop::new(conf_follower_node.clone()); - let blocks_processed = run_loop.get_blocks_processed_arc(); - let client = reqwest::blocking::Client::new(); - let channel = run_loop.get_coordinator_channel().unwrap(); + let mut run_loop = neon::RunLoop::new(conf_follower_node.clone()); + let blocks_processed = run_loop.get_blocks_processed_arc(); + let client = reqwest::blocking::Client::new(); + let channel = run_loop.get_coordinator_channel().unwrap(); - thread::spawn(move || run_loop.start(0, Some(burnchain_config))); + thread::spawn(move || run_loop.start(0, Some(burnchain_config))); - // give the run loop some time to start up! + // give the run loop some time to start up! + wait_for_runloop(&blocks_processed); + + // Follower node is ready, the bootstrap node will now handover + bootstrap_node_tx + .send(Signal::ReplicatingAttachmentsStartTest1) + .expect("Unable to send signal"); + + // The bootstrap node published and mined a transaction that includes an attachment. + // Lets observe the attachments replication kicking in. + let target_height = match follower_node_rx.recv() { + Ok(Signal::ReplicatingAttachmentsCheckTest1(target_height)) => target_height, + _ => panic!("Bootstrap node could nod boot. Aborting test."), + }; + + let mut sort_height = channel.get_sortitions_processed(); + while sort_height < target_height { wait_for_runloop(&blocks_processed); + sort_height = channel.get_sortitions_processed(); + } - // Follower node is ready, the bootstrap node will now handover - bootstrap_node_tx - .send(Signal::ReplicatingAttachmentsStartTest1) - .expect("Unable to send signal"); + // Now wait for the node to sync the attachment + let mut attachments_did_sync = false; + let mut timeout = 60; + while attachments_did_sync != true { + let zonefile_hex = "facade00"; + let hashed_zonefile = Hash160::from_data(&hex_bytes(zonefile_hex).unwrap()); + let path = format!( + "{}/v2/attachments/{}", + &http_origin, + hashed_zonefile.to_hex() + ); + let res = client + .get(&path) + .header("Content-Type", "application/json") + .send() + .unwrap(); + eprintln!("{:#?}", res); + if res.status().is_success() { + eprintln!("Success syncing attachment - {}", res.text().unwrap()); + attachments_did_sync = true; + } else { + timeout -= 1; + if timeout == 0 { + panic!("Failed syncing 1 attachments between 2 neon runloops within 60s - Something is wrong"); + } + eprintln!("Attachment {} not sync'd yet", zonefile_hex); + thread::sleep(Duration::from_millis(1000)); + } + } - // The bootstrap node published and mined a transaction that includes an attachment. - // Lets observe the attachments replication kicking in. - let target_height = match follower_node_rx.recv() { - Ok(Signal::ReplicatingAttachmentsCheckTest1(target_height)) => target_height, - _ => panic!("Bootstrap node could nod boot. Aborting test."), + // Test 2: 9 transactions are posted to the follower. + // We want to make sure that the miner is able to + // 1) mine these transactions + // 2) retrieve the attachments staged on the follower node. + // 3) ensure that the follower is also instanciating the attachments after + // executing the transactions, once mined. + let namespace = "passport"; + for i in 1..10 { + let user = StacksPrivateKey::new(); + let zonefile_hex = format!("facade0{}", i); + let hashed_zonefile = Hash160::from_data(&hex_bytes(&zonefile_hex).unwrap()); + let name = format!("johndoe{}", i); + let tx = make_contract_call( + &user_1, + 2 + i, + 500, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "bns", + "name-import", + &[ + Value::buff_from(namespace.as_bytes().to_vec()).unwrap(), + Value::buff_from(name.as_bytes().to_vec()).unwrap(), + Value::Principal(to_addr(&user).into()), + Value::buff_from(hashed_zonefile.as_bytes().to_vec()).unwrap(), + ], + ); + + let body = { + let content = PostTransactionRequestBody { + tx: bytes_to_hex(&tx), + attachment: Some(zonefile_hex.to_string()), + }; + serde_json::to_vec(&json!(content)).unwrap() }; - let mut sort_height = channel.get_sortitions_processed(); - while sort_height < target_height { - wait_for_runloop(&blocks_processed); - sort_height = channel.get_sortitions_processed(); + let path = format!("{}/v2/transactions", &http_origin); + let res = client + .post(&path) + .header("Content-Type", "application/json") + .body(body) + .send() + .unwrap(); + eprintln!("{:#?}", res); + if !res.status().is_success() { + eprintln!("{}", res.text().unwrap()); + panic!(""); } + } + + bootstrap_node_tx + .send(Signal::ReplicatingAttachmentsStartTest2) + .expect("Unable to send signal"); + + let target_height = match follower_node_rx.recv() { + Ok(Signal::ReplicatingAttachmentsCheckTest2(target_height)) => target_height, + _ => panic!("Bootstrap node could nod boot. Aborting test."), + }; + + let mut sort_height = channel.get_sortitions_processed(); + while sort_height < target_height { + wait_for_runloop(&blocks_processed); + sort_height = channel.get_sortitions_processed(); + } - // Now wait for the node to sync the attachment + // Poll GET v2/attachments/ + for i in 1..10 { let mut attachments_did_sync = false; let mut timeout = 30; while attachments_did_sync != true { - let zonefile_hex = "facade00"; - let hashed_zonefile = Hash160::from_data(&hex_bytes(zonefile_hex).unwrap()); + let zonefile_hex = hex_bytes(&format!("facade0{}", i)).unwrap(); + let hashed_zonefile = Hash160::from_data(&zonefile_hex); let path = format!( "{}/v2/attachments/{}", &http_origin, @@ -2050,121 +2162,24 @@ fn atlas_integration_test() { .unwrap(); eprintln!("{:#?}", res); if res.status().is_success() { - eprintln!("Success syncing attachment - {}", res.text().unwrap()); + let attachment_response: GetAttachmentResponse = res.json().unwrap(); + assert_eq!(attachment_response.attachment.content, zonefile_hex); attachments_did_sync = true; } else { timeout -= 1; if timeout == 0 { - panic!("Failed syncing 1 attachments between 2 neon runloops within 30s - Something is wrong"); + panic!("Failed syncing 9 attachments between 2 neon runloops within 30s - Something is wrong"); } - eprintln!("Attachment {} not sync'd yet", zonefile_hex); + eprintln!("Attachment {} not sync'd yet", bytes_to_hex(&zonefile_hex)); thread::sleep(Duration::from_millis(1000)); } } + } - // Test 2: 9 transactions are posted to the follower. - // We want to make sure that the miner is able to - // 1) mine these transactions - // 2) retrieve the attachments staged on the follower node. - // 3) ensure that the follower is also instanciating the attachments after - // executing the transactions, once mined. - let namespace = "passport"; - for i in 1..10 { - let user = StacksPrivateKey::new(); - let zonefile_hex = format!("facade0{}", i); - let hashed_zonefile = Hash160::from_data(&hex_bytes(&zonefile_hex).unwrap()); - let name = format!("johndoe{}", i); - let tx = make_contract_call( - &user_1, - 2 + i, - 500, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "bns", - "name-import", - &[ - Value::buff_from(namespace.as_bytes().to_vec()).unwrap(), - Value::buff_from(name.as_bytes().to_vec()).unwrap(), - Value::Principal(to_addr(&user).into()), - Value::buff_from(hashed_zonefile.as_bytes().to_vec()).unwrap(), - ], - ); - - let body = { - let content = PostTransactionRequestBody { - tx: bytes_to_hex(&tx), - attachment: Some(zonefile_hex.to_string()), - }; - serde_json::to_vec(&json!(content)).unwrap() - }; - - let path = format!("{}/v2/transactions", &http_origin); - let res = client - .post(&path) - .header("Content-Type", "application/json") - .body(body) - .send() - .unwrap(); - eprintln!("{:#?}", res); - if !res.status().is_success() { - eprintln!("{}", res.text().unwrap()); - panic!(""); - } - } - - bootstrap_node_tx - .send(Signal::ReplicatingAttachmentsStartTest2) - .expect("Unable to send signal"); - - let target_height = match follower_node_rx.recv() { - Ok(Signal::ReplicatingAttachmentsCheckTest2(target_height)) => target_height, - _ => panic!("Bootstrap node could nod boot. Aborting test."), - }; - - let mut sort_height = channel.get_sortitions_processed(); - while sort_height < target_height { - wait_for_runloop(&blocks_processed); - sort_height = channel.get_sortitions_processed(); - } - - // Poll GET v2/attachments/ - for i in 1..10 { - let mut attachments_did_sync = false; - let mut timeout = 30; - while attachments_did_sync != true { - let zonefile_hex = hex_bytes(&format!("facade0{}", i)).unwrap(); - let hashed_zonefile = Hash160::from_data(&zonefile_hex); - let path = format!( - "{}/v2/attachments/{}", - &http_origin, - hashed_zonefile.to_hex() - ); - let res = client - .get(&path) - .header("Content-Type", "application/json") - .send() - .unwrap(); - eprintln!("{:#?}", res); - if res.status().is_success() { - let attachment_response: GetAttachmentResponse = res.json().unwrap(); - assert_eq!(attachment_response.attachment.content, zonefile_hex); - attachments_did_sync = true; - } else { - timeout -= 1; - if timeout == 0 { - panic!("Failed syncing 9 attachments between 2 neon runloops within 30s - Something is wrong"); - } - eprintln!("Attachment {} not sync'd yet", bytes_to_hex(&zonefile_hex)); - thread::sleep(Duration::from_millis(1000)); - } - } - } - - // Ensure that we the attached sidecar was able to receive a total of 10 attachments - assert_eq!(test_observer::get_attachments().len(), 10); - test_observer::clear(); - channel.stop_chains_coordinator(); - }); + // Ensure that we the attached sidecar was able to receive a total of 10 attachments + assert_eq!(test_observer::get_attachments().len(), 10); + test_observer::clear(); + channel.stop_chains_coordinator(); bootstrap_node_thread.join().unwrap(); - follower_node_thread.join().unwrap(); }