iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 2fa717de7a55f93a763171211ca36ce920fc329c
parent 3a9035707698cae295f928e278ed7582a8cbd35b
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Mon, 31 Aug 2026 22:24:59 +0200

Bind burns to the current chain tip

Diffstat:
Mdocs/protocol.md | 2++
Me2e/snapshots/first-objective-checkpoint/bootstrap/chain.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/bootstrap/ui_data.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/manifest.json | 50+++++++++++++++++++++++++-------------------------
Me2e/snapshots/first-objective-checkpoint/node2/chain.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node2/ui_data.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node3/chain.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node3/ui_data.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node4/chain.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node4/ui_data.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node5/chain.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node5/ui_data.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node6/chain.sqlite3 | 0
Me2e/snapshots/first-objective-checkpoint/node6/ui_data.sqlite3 | 0
Me2e/snapshots/objective-finality/bootstrap/chain.sqlite3 | 0
Me2e/snapshots/objective-finality/bootstrap/ui_data.sqlite3 | 0
Me2e/snapshots/objective-finality/manifest.json | 52++++++++++++++++++++++++++--------------------------
Me2e/snapshots/objective-finality/node2/chain.sqlite3 | 0
Me2e/snapshots/objective-finality/node2/ui_data.sqlite3 | 0
Me2e/snapshots/objective-finality/node3/chain.sqlite3 | 0
Me2e/snapshots/objective-finality/node3/ui_data.sqlite3 | 0
Me2e/snapshots/objective-finality/node4/chain.sqlite3 | 0
Me2e/snapshots/objective-finality/node4/ui_data.sqlite3 | 0
Me2e/snapshots/objective-finality/node5/chain.sqlite3 | 0
Me2e/snapshots/objective-finality/node5/ui_data.sqlite3 | 0
Me2e/snapshots/objective-finality/node6/chain.sqlite3 | 0
Me2e/snapshots/objective-finality/node6/ui_data.sqlite3 | 0
Msrc/adapters/chain_store.rs | 2+-
Msrc/adapters/chain_store/compact.rs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/adapters/http/ui.rs | 4++++
Msrc/adapters/p2p/line_codec.rs | 1+
Msrc/domain.rs | 4++--
Msrc/domain/ledger_apply.rs | 15+++++++++++++++
Msrc/domain/ledger_builders.rs | 2++
Msrc/domain/ledger_mempool.rs | 5+++++
Msrc/domain/ledger_pending.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/domain/ledger_reveal.rs | 1+
Msrc/domain/protocol.rs | 2++
Msrc/domain/ticket.rs | 1+
Msrc/domain/transaction.rs | 44++++++++++++++++++++++++++++++++++++++------
Msrc/domain/validation.rs | 2++
41 files changed, 267 insertions(+), 70 deletions(-)

diff --git a/docs/protocol.md b/docs/protocol.md @@ -150,6 +150,8 @@ From height `300`, fallback finalization invalidates missed ticket opportunities Every normal block must include at least one burn. This keeps the future ticket pool alive even during quiet periods. A node that may finalize prepares a local anchor burn for the next block from the finalizer wallet, and that anchor burn appears directly in the block. +From height `1000`, every burn is bound to the block parent hash. The parent hash is part of the signed transaction payload, and validators require it to equal the `prev_hash` of the block containing the burn. A burn left in a mempool after any normal, fallback, or recovery block therefore expires immediately. An online wallet can sign a replacement against the new tip, while an offline wallet cannot keep creating tickets from previously broadcast burns. Earlier history retains the unanchored burn format. + The anchor burn is not a fairness mechanism. By itself, it would mostly help the current finalizer keep creating future tickets. Fairness against self-serving finalizers comes from the burn inclusion committee described below. ## VDF Timing diff --git a/e2e/snapshots/first-objective-checkpoint/bootstrap/chain.sqlite3 b/e2e/snapshots/first-objective-checkpoint/bootstrap/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/bootstrap/ui_data.sqlite3 b/e2e/snapshots/first-objective-checkpoint/bootstrap/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/manifest.json b/e2e/snapshots/first-objective-checkpoint/manifest.json @@ -2,66 +2,66 @@ "format": 1, "name": "first-objective-checkpoint", "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", "profile_id": "iuna-local-e2e-5s-v1", "target_block_ms": 5000, "source_height": 1002, "nodes": { "bootstrap": { "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", - "updated_at_ms": 1788119608835 + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", + "updated_at_ms": 1788207421380 }, "node2": { "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", - "updated_at_ms": 1788119608835 + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", + "updated_at_ms": 1788207421380 }, "node3": { "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", - "updated_at_ms": 1788119608835 + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", + "updated_at_ms": 1788207421380 }, "node4": { "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", - "updated_at_ms": 1788119608835 + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", + "updated_at_ms": 1788207421380 }, "node5": { "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", - "updated_at_ms": 1788119608835 + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", + "updated_at_ms": 1788207421380 }, "node6": { "height": 1001, - "tip_hash": "80bb3e5d9534459cdff5f1cdd0c2899acdc9f5942192a31e709e3a2c9015e6cd", - "updated_at_ms": 1788119608835 + "tip_hash": "52673c26456682f7b153b40e25b1cd91b4a7ecdff81c5a46bb96c5a1be30923b", + "updated_at_ms": 1788207421380 } }, "sha256": { - "bootstrap/chain.sqlite3": "907f3387061d99eafcc661d08c55354364654777d37396f7b0ad1876913fcbe6", + "bootstrap/chain.sqlite3": "3336dc33f3650d66b5069c75b083d4db1f26a6fe32a1ce1bfc128601a0578007", "bootstrap/config.json": "ee490011fee4bad2a08641cca4710efad5ee0cc7ff0f418e9b6b3b97f6d64c8e", - "bootstrap/ui_data.sqlite3": "7d4867feba2f3bf8d234d83e435ae149a699d3be732eedfe7b46c49bb47f3f4b", + "bootstrap/ui_data.sqlite3": "ad838956f4527735714c5458954b618a6fa6cc7086b55538381ec8e39b014d28", "bootstrap/wallet.json": "9e545dee2d7d8c430a301fd9cc4530467c6ba36787d2407ec31eee96eee88874", - "node2/chain.sqlite3": "907f3387061d99eafcc661d08c55354364654777d37396f7b0ad1876913fcbe6", + "node2/chain.sqlite3": "3336dc33f3650d66b5069c75b083d4db1f26a6fe32a1ce1bfc128601a0578007", "node2/config.json": "5f38677b8e290f412a2ff5833518a1b1fec75d9c8ab9dbc72b1762fd889e33c8", - "node2/ui_data.sqlite3": "7d4867feba2f3bf8d234d83e435ae149a699d3be732eedfe7b46c49bb47f3f4b", + "node2/ui_data.sqlite3": "ad838956f4527735714c5458954b618a6fa6cc7086b55538381ec8e39b014d28", "node2/wallet.json": "80c81d94fa958deb3212c3ae52bcfcdcf1f2ce446ec3c4addcfb8ffa6e669a4b", - "node3/chain.sqlite3": "907f3387061d99eafcc661d08c55354364654777d37396f7b0ad1876913fcbe6", + "node3/chain.sqlite3": "3336dc33f3650d66b5069c75b083d4db1f26a6fe32a1ce1bfc128601a0578007", "node3/config.json": "63a8c0d8b06091dbfb899bba7f6603497c44e7af1bee9f21cc5152a3fafc037d", - "node3/ui_data.sqlite3": "7d4867feba2f3bf8d234d83e435ae149a699d3be732eedfe7b46c49bb47f3f4b", + "node3/ui_data.sqlite3": "ad838956f4527735714c5458954b618a6fa6cc7086b55538381ec8e39b014d28", "node3/wallet.json": "15d4b71da0e31f6045fc6b98b7d3414f7925556932de42b52bacd35e670c1c86", - "node4/chain.sqlite3": "907f3387061d99eafcc661d08c55354364654777d37396f7b0ad1876913fcbe6", + "node4/chain.sqlite3": "3336dc33f3650d66b5069c75b083d4db1f26a6fe32a1ce1bfc128601a0578007", "node4/config.json": "f9bed39ec29fbff17fb6155e718454706991603d5993632c6f6b18cbf3d80fa4", - "node4/ui_data.sqlite3": "7d4867feba2f3bf8d234d83e435ae149a699d3be732eedfe7b46c49bb47f3f4b", + "node4/ui_data.sqlite3": "ad838956f4527735714c5458954b618a6fa6cc7086b55538381ec8e39b014d28", "node4/wallet.json": "204ff6327b0c5da43418f704322fcfe8de09f799239896146bfb0320d42e3780", - "node5/chain.sqlite3": "907f3387061d99eafcc661d08c55354364654777d37396f7b0ad1876913fcbe6", + "node5/chain.sqlite3": "3336dc33f3650d66b5069c75b083d4db1f26a6fe32a1ce1bfc128601a0578007", "node5/config.json": "0bd460df616c8effb1091625887cab053d95e9f30afbc4237e7e5923c0cd6b73", - "node5/ui_data.sqlite3": "7d4867feba2f3bf8d234d83e435ae149a699d3be732eedfe7b46c49bb47f3f4b", + "node5/ui_data.sqlite3": "ad838956f4527735714c5458954b618a6fa6cc7086b55538381ec8e39b014d28", "node5/wallet.json": "4612e7e2143a11b5834b89e1dbaebb30d9c898930bfeb3eeedb347771b827808", - "node6/chain.sqlite3": "907f3387061d99eafcc661d08c55354364654777d37396f7b0ad1876913fcbe6", + "node6/chain.sqlite3": "3336dc33f3650d66b5069c75b083d4db1f26a6fe32a1ce1bfc128601a0578007", "node6/config.json": "1d6aaba498ee1f273c89d7f84806318cf5f15c36088346460236e114a0fd73bb", - "node6/ui_data.sqlite3": "7d4867feba2f3bf8d234d83e435ae149a699d3be732eedfe7b46c49bb47f3f4b", + "node6/ui_data.sqlite3": "ad838956f4527735714c5458954b618a6fa6cc7086b55538381ec8e39b014d28", "node6/wallet.json": "1eaf0a87b89c5a36a3f43162894922176412817f468e907faa5970f1656a04f7" } } diff --git a/e2e/snapshots/first-objective-checkpoint/node2/chain.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node2/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node2/ui_data.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node2/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node3/chain.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node3/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node3/ui_data.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node3/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node4/chain.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node4/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node4/ui_data.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node4/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node5/chain.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node5/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node5/ui_data.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node5/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node6/chain.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node6/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/first-objective-checkpoint/node6/ui_data.sqlite3 b/e2e/snapshots/first-objective-checkpoint/node6/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/bootstrap/chain.sqlite3 b/e2e/snapshots/objective-finality/bootstrap/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/bootstrap/ui_data.sqlite3 b/e2e/snapshots/objective-finality/bootstrap/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/manifest.json b/e2e/snapshots/objective-finality/manifest.json @@ -2,66 +2,66 @@ "format": 1, "name": "objective-finality", "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", "profile_id": "iuna-local-e2e-5s-v1", "target_block_ms": 5000, - "source_height": 1002, + "source_height": 1001, "nodes": { "bootstrap": { "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", - "updated_at_ms": 1788119532744 + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", + "updated_at_ms": 1788207376222 }, "node2": { "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", - "updated_at_ms": 1788119532744 + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", + "updated_at_ms": 1788207376222 }, "node3": { "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", - "updated_at_ms": 1788119532744 + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", + "updated_at_ms": 1788207376222 }, "node4": { "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", - "updated_at_ms": 1788119532744 + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", + "updated_at_ms": 1788207376222 }, "node5": { "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", - "updated_at_ms": 1788119532744 + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", + "updated_at_ms": 1788207376222 }, "node6": { "height": 1000, - "tip_hash": "3019077513cc28f557c7dc667dfc2548f05ed2ee32a3717d00cf9bff69f08867", - "updated_at_ms": 1788119532744 + "tip_hash": "71d11cbb11e5c5f0be26686ca16170a4acb2d7bda23057657279a34e7fcd66e6", + "updated_at_ms": 1788207376222 } }, "sha256": { - "bootstrap/chain.sqlite3": "f033556a11826a58f9148990322e68d66ac3b5272735a1ed1bcbbdb21789e955", + "bootstrap/chain.sqlite3": "968db77500c274d3a377d048c3f3f34ae7467f68ac4e36154112a86e7f1ceec7", "bootstrap/config.json": "ee490011fee4bad2a08641cca4710efad5ee0cc7ff0f418e9b6b3b97f6d64c8e", - "bootstrap/ui_data.sqlite3": "535127daeea2a766f529551884fdddfa436b78c9c8f5db13f767371254d4b869", + "bootstrap/ui_data.sqlite3": "69b49246151d28a807e34a17faacc259c14a0a3d978a2a5cb5b7facd7f33b576", "bootstrap/wallet.json": "9e545dee2d7d8c430a301fd9cc4530467c6ba36787d2407ec31eee96eee88874", - "node2/chain.sqlite3": "f033556a11826a58f9148990322e68d66ac3b5272735a1ed1bcbbdb21789e955", + "node2/chain.sqlite3": "968db77500c274d3a377d048c3f3f34ae7467f68ac4e36154112a86e7f1ceec7", "node2/config.json": "5f38677b8e290f412a2ff5833518a1b1fec75d9c8ab9dbc72b1762fd889e33c8", - "node2/ui_data.sqlite3": "535127daeea2a766f529551884fdddfa436b78c9c8f5db13f767371254d4b869", + "node2/ui_data.sqlite3": "69b49246151d28a807e34a17faacc259c14a0a3d978a2a5cb5b7facd7f33b576", "node2/wallet.json": "80c81d94fa958deb3212c3ae52bcfcdcf1f2ce446ec3c4addcfb8ffa6e669a4b", - "node3/chain.sqlite3": "f033556a11826a58f9148990322e68d66ac3b5272735a1ed1bcbbdb21789e955", + "node3/chain.sqlite3": "968db77500c274d3a377d048c3f3f34ae7467f68ac4e36154112a86e7f1ceec7", "node3/config.json": "63a8c0d8b06091dbfb899bba7f6603497c44e7af1bee9f21cc5152a3fafc037d", - "node3/ui_data.sqlite3": "535127daeea2a766f529551884fdddfa436b78c9c8f5db13f767371254d4b869", + "node3/ui_data.sqlite3": "69b49246151d28a807e34a17faacc259c14a0a3d978a2a5cb5b7facd7f33b576", "node3/wallet.json": "15d4b71da0e31f6045fc6b98b7d3414f7925556932de42b52bacd35e670c1c86", - "node4/chain.sqlite3": "f033556a11826a58f9148990322e68d66ac3b5272735a1ed1bcbbdb21789e955", + "node4/chain.sqlite3": "968db77500c274d3a377d048c3f3f34ae7467f68ac4e36154112a86e7f1ceec7", "node4/config.json": "f9bed39ec29fbff17fb6155e718454706991603d5993632c6f6b18cbf3d80fa4", - "node4/ui_data.sqlite3": "535127daeea2a766f529551884fdddfa436b78c9c8f5db13f767371254d4b869", + "node4/ui_data.sqlite3": "69b49246151d28a807e34a17faacc259c14a0a3d978a2a5cb5b7facd7f33b576", "node4/wallet.json": "204ff6327b0c5da43418f704322fcfe8de09f799239896146bfb0320d42e3780", - "node5/chain.sqlite3": "f033556a11826a58f9148990322e68d66ac3b5272735a1ed1bcbbdb21789e955", + "node5/chain.sqlite3": "968db77500c274d3a377d048c3f3f34ae7467f68ac4e36154112a86e7f1ceec7", "node5/config.json": "0bd460df616c8effb1091625887cab053d95e9f30afbc4237e7e5923c0cd6b73", - "node5/ui_data.sqlite3": "535127daeea2a766f529551884fdddfa436b78c9c8f5db13f767371254d4b869", + "node5/ui_data.sqlite3": "69b49246151d28a807e34a17faacc259c14a0a3d978a2a5cb5b7facd7f33b576", "node5/wallet.json": "4612e7e2143a11b5834b89e1dbaebb30d9c898930bfeb3eeedb347771b827808", - "node6/chain.sqlite3": "f033556a11826a58f9148990322e68d66ac3b5272735a1ed1bcbbdb21789e955", + "node6/chain.sqlite3": "968db77500c274d3a377d048c3f3f34ae7467f68ac4e36154112a86e7f1ceec7", "node6/config.json": "1d6aaba498ee1f273c89d7f84806318cf5f15c36088346460236e114a0fd73bb", - "node6/ui_data.sqlite3": "535127daeea2a766f529551884fdddfa436b78c9c8f5db13f767371254d4b869", + "node6/ui_data.sqlite3": "69b49246151d28a807e34a17faacc259c14a0a3d978a2a5cb5b7facd7f33b576", "node6/wallet.json": "1eaf0a87b89c5a36a3f43162894922176412817f468e907faa5970f1656a04f7" } } diff --git a/e2e/snapshots/objective-finality/node2/chain.sqlite3 b/e2e/snapshots/objective-finality/node2/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node2/ui_data.sqlite3 b/e2e/snapshots/objective-finality/node2/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node3/chain.sqlite3 b/e2e/snapshots/objective-finality/node3/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node3/ui_data.sqlite3 b/e2e/snapshots/objective-finality/node3/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node4/chain.sqlite3 b/e2e/snapshots/objective-finality/node4/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node4/ui_data.sqlite3 b/e2e/snapshots/objective-finality/node4/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node5/chain.sqlite3 b/e2e/snapshots/objective-finality/node5/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node5/ui_data.sqlite3 b/e2e/snapshots/objective-finality/node5/ui_data.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node6/chain.sqlite3 b/e2e/snapshots/objective-finality/node6/chain.sqlite3 Binary files differ. diff --git a/e2e/snapshots/objective-finality/node6/ui_data.sqlite3 b/e2e/snapshots/objective-finality/node6/ui_data.sqlite3 Binary files differ. diff --git a/src/adapters/chain_store.rs b/src/adapters/chain_store.rs @@ -404,7 +404,7 @@ VALUES (1, 0, 'legacy-tip', ?1, 0) fn open_does_not_archive_corrupt_or_future_compact_snapshots() { for (name, blob) in [ ("corrupt", vec![0, 1, 2, 3]), - ("future", [b"IUNA-SNAPSHOT".as_slice(), &[7]].concat()), + ("future", [b"IUNA-SNAPSHOT".as_slice(), &[8]].concat()), ] { let dir = tempdir().unwrap(); let path = dir.path().join(format!("{name}.sqlite3")); diff --git a/src/adapters/chain_store/compact.rs b/src/adapters/chain_store/compact.rs @@ -8,7 +8,8 @@ use crate::domain::{ }; const COMPACT_SNAPSHOT_MAGIC: &[u8] = b"IUNA-SNAPSHOT"; -const COMPACT_SNAPSHOT_VERSION: u8 = 6; +const MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION: u8 = 6; +const COMPACT_SNAPSHOT_VERSION: u8 = 7; const VDF_SOLUTION_PREFIX: &str = "classgroup-wesolowski-bqfc-v1:"; const MAX_COMPACT_GENESIS_ALLOCATIONS: usize = 100_000; const MAX_COMPACT_SNAPSHOT_BLOCKS: usize = 10_000; @@ -21,7 +22,7 @@ pub(super) fn legacy_compact_snapshot_version(bytes: &[u8]) -> Option<u8> { return None; } let version = bytes[version_offset]; - (version < COMPACT_SNAPSHOT_VERSION).then_some(version) + (version < MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION).then_some(version) } #[derive(Clone, Debug, Default)] @@ -148,7 +149,7 @@ pub(super) fn decode_compact_snapshot(bytes: &[u8]) -> Result<ChainSnapshot> { let mut tables = DecodeTables::default(); reader.magic(COMPACT_SNAPSHOT_MAGIC)?; let version = reader.u8()?; - if version != COMPACT_SNAPSHOT_VERSION { + if !(MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION..=COMPACT_SNAPSHOT_VERSION).contains(&version) { bail!("unsupported compact chain snapshot version {version}"); } let genesis_count = @@ -380,6 +381,7 @@ fn encode_transaction( change, amount, fee, + anchor, signature, } => { writer.u8(1); @@ -393,7 +395,7 @@ fn encode_transaction( [output] if output.address == owner => 1, _ => 2, }; - writer.u8(change_mode | (u8::from(genesis) << 2)); + writer.u8(change_mode | (u8::from(genesis) << 2) | (u8::from(anchor.is_some()) << 3)); encode_outpoints(writer, inputs, tables)?; writer.address(owner, tables)?; match change_mode { @@ -404,6 +406,9 @@ fn encode_transaction( } writer.varint(*amount); writer.varint(*fee); + if let Some(anchor) = anchor { + writer.protocol_id_ref(anchor, tables)?; + } if genesis { writer.fixed_hex::<32>(signature, "genesis burn signature")?; } else { @@ -455,10 +460,11 @@ fn decode_transaction( } 1 => { let mode = reader.u8()?; - if mode & !0b111 != 0 || mode & 0b11 > 2 { + if mode & !0b1111 != 0 || mode & 0b11 > 2 { bail!("invalid compact burn mode {mode}"); } let genesis = mode & 0b100 != 0; + let anchored = mode & 0b1000 != 0; let outpoints = decode_outpoints(reader, tables)?; let owner = reader.address(tables)?; let change = match mode & 0b11 { @@ -472,6 +478,11 @@ fn decode_transaction( }; let amount = reader.varint()?; let fee = reader.varint()?; + let anchor = if anchored { + Some(reader.protocol_id_ref(tables)?) + } else { + None + }; let signature = if genesis { reader.fixed_hex::<32>()? } else { @@ -483,6 +494,7 @@ fn decode_transaction( change, amount, fee, + anchor, signature, }) } @@ -916,13 +928,13 @@ mod tests { use super::{ COMPACT_SNAPSHOT_MAGIC, COMPACT_SNAPSHOT_VERSION, CompactReader, CompactWriter, DecodeTables, EncodeTables, MAX_COMPACT_BYTE_FIELD, MAX_COMPACT_GENESIS_ALLOCATIONS, - MAX_COMPACT_SNAPSHOT_BLOCKS, MAX_COMPACT_VEC_ITEMS, decode_block_body, - decode_compact_snapshot, decode_launch_profile, decode_transaction, encode_block_body, - encode_compact_snapshot, encode_launch_profile, encode_transaction, + MAX_COMPACT_SNAPSHOT_BLOCKS, MAX_COMPACT_VEC_ITEMS, MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION, + decode_block_body, decode_compact_snapshot, decode_launch_profile, decode_transaction, + encode_block_body, encode_compact_snapshot, encode_launch_profile, encode_transaction, }; #[test] - fn compact_snapshot_v6_roundtrips_default_and_local_profiles() { + fn compact_snapshot_v7_roundtrips_default_and_local_profiles() { let wallet = Wallet::from_seed("compact-profile-wire-version"); let allocations = BTreeMap::from([(wallet.address().to_string(), MICRO_IUNA)]); let default_snapshot = Ledger::new(allocations.clone(), 1).snapshot(); @@ -1003,6 +1015,7 @@ mod tests { }], amount: 20, fee: 1, + anchor: None, signature: signature.clone(), }; let transfer = Transaction::Transfer { @@ -1030,6 +1043,34 @@ mod tests { } #[test] + fn compact_transaction_roundtrips_tip_bound_burn() { + let owner = "2".repeat(64); + let signature = "3".repeat(128); + let anchor = "4".repeat(64); + let burn = Transaction::Burn { + inputs: vec![input(&owner, &signature)], + change: Vec::new(), + amount: 20, + fee: 1, + anchor: Some(anchor.clone()), + signature, + }; + let mut encode_tables = EncodeTables::default(); + encode_tables.register_protocol_id(&anchor); + let mut writer = CompactWriter::default(); + encode_transaction(&mut writer, &burn, &mut encode_tables).unwrap(); + let bytes = writer.into_inner(); + let mut decode_tables = DecodeTables::default(); + decode_tables.register_protocol_id(&anchor); + let mut reader = CompactReader::new(&bytes); + + let decoded = decode_transaction(&mut reader, &mut decode_tables).unwrap(); + + reader.finish().unwrap(); + assert_eq!(decoded, burn); + } + + #[test] fn repeated_burn_references_shrink_to_small_varints() { let owner = "2".repeat(64); let signature = "3".repeat(128); @@ -1042,6 +1083,7 @@ mod tests { }], amount: 20, fee: 1, + anchor: None, signature, }; let mut tables = EncodeTables::default(); @@ -1062,6 +1104,7 @@ mod tests { change: Vec::new(), amount: 20, fee: 1, + anchor: None, signature, }; let block_with_section = |burn_bundle_section| { @@ -1205,7 +1248,7 @@ mod tests { fn compact_snapshot_decoder_rejects_pre_reset_versions() { let mut bytes = Vec::new(); bytes.extend_from_slice(COMPACT_SNAPSHOT_MAGIC); - bytes.push(COMPACT_SNAPSHOT_VERSION - 1); + bytes.push(MIN_SUPPORTED_COMPACT_SNAPSHOT_VERSION - 1); assert_decode_error_contains(&bytes, "unsupported compact chain snapshot version 5"); } diff --git a/src/adapters/http/ui.rs b/src/adapters/http/ui.rs @@ -100,6 +100,7 @@ pub(super) fn wallet_transaction_row( amount, fee, signature, + .. } if tx.sender() == wallet => Some(WalletTransactionRow { kind: "burn", from: tx.sender().to_string(), @@ -326,6 +327,7 @@ pub(super) fn ui_transaction( amount, fee, signature, + .. } => UiTransaction { kind: "burn", from: transaction.sender().to_string(), @@ -468,6 +470,7 @@ mod tests { }], amount: 1, fee: 1, + anchor: None, signature: signature.to_string(), } } @@ -498,6 +501,7 @@ mod tests { change: Vec::new(), amount: 7, fee: 3, + anchor: None, signature: "burn".to_string(), }; diff --git a/src/adapters/p2p/line_codec.rs b/src/adapters/p2p/line_codec.rs @@ -249,6 +249,7 @@ mod tests { }], amount: 1, fee: 1, + anchor: None, signature: signature.to_string(), } } diff --git a/src/domain.rs b/src/domain.rs @@ -62,8 +62,8 @@ pub use protocol::{ MAX_BLOCK_BYTES, MAX_BURN_BUNDLE_BYTES, MAX_PENDING_TRANSACTIONS, MAX_VDF_ROUNDS, MICRO_IUNA, MINE_ACTIONS_PER_ANCHOR_LIMIT, MINE_DIFFICULTY_BITS, MINE_FINALIZER_FEE, MINE_REWARD, OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, RECOVERY_BLOCK_DELAY_MS, - TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT, TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT, - TransactionSubmitOutcome, VDF_TARGET_BLOCK_MS, + TIP_BOUND_BURN_ACTIVATION_HEIGHT, TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT, + TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT, TransactionSubmitOutcome, VDF_TARGET_BLOCK_MS, }; use protocol::{ BLOCK_MEDIAN_TIME_PAST_WINDOW, DEFAULT_TICKET_EXPIRY_WINDOW, DEFAULT_TICKET_MATURITY_DELAY, diff --git a/src/domain/ledger_apply.rs b/src/domain/ledger_apply.rs @@ -88,6 +88,7 @@ impl Ledger { bail!("duplicate transaction in block"); } self.validate_transaction_terms(tx)?; + self.validate_transaction_anchor_for_height(tx, block.height, &block.prev_hash)?; apply_transaction_with_lineage( tx, block.height, @@ -132,6 +133,13 @@ impl Ledger { !mined_signatures.contains(tx.signature()) && transaction_inputs_available(tx, &available) && self.validate_transaction_terms(tx).is_ok() + && self + .validate_transaction_anchor_for_height( + tx, + self.height().saturating_add(1), + self.tip_hash(), + ) + .is_ok() && tx.verify_signature(&next_signing_domain).is_ok() }) .collect(); @@ -141,6 +149,13 @@ impl Ledger { .filter(|tx| { !mined_signatures.contains(tx.signature()) && self.validate_transaction_terms(tx).is_ok() + && self + .validate_transaction_anchor_for_height( + tx, + self.height().saturating_add(1), + self.tip_hash(), + ) + .is_ok() && tx.verify_signature(&next_signing_domain).is_ok() }) .collect(); diff --git a/src/domain/ledger_builders.rs b/src/domain/ledger_builders.rs @@ -136,6 +136,8 @@ impl Ledger { change, amount, fee, + anchor: (self.height().saturating_add(1) >= super::TIP_BOUND_BURN_ACTIVATION_HEIGHT) + .then(|| self.tip().hash.clone()), } .sign(wallet, &self.transaction_signing_domain())?; self.validate_new_transaction(&transaction)?; diff --git a/src/domain/ledger_mempool.rs b/src/domain/ledger_mempool.rs @@ -69,6 +69,11 @@ impl Ledger { let signing_domain = self.transaction_signing_domain(); transaction.verify_signature(&signing_domain)?; self.validate_transaction_terms(&transaction)?; + self.validate_transaction_anchor_for_height( + &transaction, + self.height().saturating_add(1), + self.tip_hash(), + )?; ensure_transaction_fits_empty_block( compact_block_context(self), &transaction, diff --git a/src/domain/ledger_pending.rs b/src/domain/ledger_pending.rs @@ -53,6 +53,13 @@ impl Ledger { } if transaction_inputs_available(tx, &utxos) && self.validate_transaction_terms(tx).is_ok() + && self + .validate_transaction_anchor_for_height( + tx, + self.height().saturating_add(1), + self.tip_hash(), + ) + .is_ok() && apply_transaction(tx, &mut utxos, &signing_domain).is_ok() { if let Some(anchor) = mine_anchor(tx) { @@ -307,6 +314,11 @@ impl Ledger { pub(super) fn validate_new_transaction(&self, transaction: &Transaction) -> Result<()> { self.validate_transaction_terms(transaction)?; + self.validate_transaction_anchor_for_height( + transaction, + self.height().saturating_add(1), + self.tip_hash(), + )?; ensure_transaction_fits_empty_block( compact_block_context(self), transaction, @@ -403,6 +415,7 @@ impl Ledger { inputs, change, fee, + anchor, signature, .. } => { @@ -411,6 +424,9 @@ impl Ledger { } validate_transaction_inputs(inputs)?; validate_transaction_outputs(change)?; + if let Some(anchor) = anchor { + validate_hash(anchor, "burn transaction anchor")?; + } validate_signature(signature, "transaction signature")?; } Transaction::Mine { @@ -446,6 +462,30 @@ impl Ledger { Ok(()) } + pub(super) fn validate_transaction_anchor_for_height( + &self, + transaction: &Transaction, + height: u64, + parent_hash: &str, + ) -> Result<()> { + if !transaction.is_burn() { + return Ok(()); + } + if height < super::TIP_BOUND_BURN_ACTIVATION_HEIGHT { + if transaction.burn_anchor().is_some() { + bail!("burn transaction anchor is not active yet"); + } + return Ok(()); + } + let anchor = transaction + .burn_anchor() + .context("burn transaction is missing its parent anchor")?; + if anchor != parent_hash { + bail!("burn transaction anchor does not match the block parent"); + } + Ok(()) + } + pub(super) fn utxos_after_valid_pending(&self) -> Result<BTreeMap<OutPoint, TxOutput>> { let mut utxos = self.utxos.clone(); let signing_domain = self.transaction_signing_domain(); @@ -523,6 +563,53 @@ mod tests { } #[test] + fn burns_become_tip_bound_at_activation_and_expire_after_tip_change() { + let wallet = Wallet::from_seed("tip-bound-burn-wallet"); + let mut ledger = Ledger::new(BTreeMap::from([(wallet.address().to_string(), 10)]), 1); + + extend_synthetic_chain_to( + &mut ledger, + super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT - 2, + ); + let legacy_burn = ledger.build_burn(&wallet, 1, 1).unwrap(); + assert_eq!(legacy_burn.burn_anchor(), None); + + extend_synthetic_chain_to( + &mut ledger, + super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT - 1, + ); + let anchored_burn = ledger.build_burn(&wallet, 1, 1).unwrap(); + assert_eq!(anchored_burn.burn_anchor(), Some(ledger.tip_hash())); + assert!(ledger.submit_transaction(anchored_burn.clone()).unwrap()); + assert_eq!(ledger.valid_pending_transactions(), vec![anchored_burn]); + + extend_synthetic_chain_to(&mut ledger, super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT); + assert!(ledger.valid_pending_transactions().is_empty()); + } + + #[test] + fn burn_signature_commits_to_parent_anchor() { + let wallet = Wallet::from_seed("tip-bound-burn-signature-wallet"); + let mut ledger = Ledger::new(BTreeMap::from([(wallet.address().to_string(), 10)]), 1); + extend_synthetic_chain_to( + &mut ledger, + super::super::TIP_BOUND_BURN_ACTIVATION_HEIGHT - 1, + ); + let mut burn = ledger.build_burn(&wallet, 1, 1).unwrap(); + let Transaction::Burn { anchor, .. } = &mut burn else { + unreachable!(); + }; + *anchor = Some("f".repeat(64)); + + assert!( + burn.verify_signature(&ledger.transaction_signing_domain()) + .unwrap_err() + .to_string() + .contains("signature is invalid") + ); + } + + #[test] fn block_selection_reserves_anchor_first_then_orders_remaining_by_fee_rate() { let finalizer = Wallet::from_seed("selection-finalizer"); let high_fee_sender = Wallet::from_seed("selection-high-fee"); diff --git a/src/domain/ledger_reveal.rs b/src/domain/ledger_reveal.rs @@ -502,6 +502,7 @@ mod tests { }], amount: 1, fee, + anchor: None, signature: signature.to_string(), } } diff --git a/src/domain/protocol.rs b/src/domain/protocol.rs @@ -23,6 +23,7 @@ pub const GRINDING_RESISTANCE_ACTIVATION_HEIGHT: u64 = 1_000; pub const TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT: u64 = 1_000; pub const TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT: u64 = 1_000; pub const OBJECTIVE_FINALITY_ACTIVATION_HEIGHT: u64 = 1_000; +pub const TIP_BOUND_BURN_ACTIVATION_HEIGHT: u64 = 1_000; pub const MAX_PENDING_TRANSACTIONS: usize = 10_000; pub(super) const MAX_PENDING_POOL_BYTES: usize = 8 * 1024 * 1024; @@ -75,6 +76,7 @@ mod tests { assert_eq!(TRANSACTION_SIGNING_V1_ACTIVATION_HEIGHT, 1_000); assert_eq!(TRANSACTION_REPLAY_PROTECTION_ACTIVATION_HEIGHT, 1_000); assert_eq!(OBJECTIVE_FINALITY_ACTIVATION_HEIGHT, 1_000); + assert_eq!(TIP_BOUND_BURN_ACTIVATION_HEIGHT, 1_000); assert_eq!(MAX_PENDING_TRANSACTIONS, 10_000); assert_eq!(MAX_PENDING_POOL_BYTES, 8 * 1024 * 1024); assert_eq!(MAX_ORPHAN_TRANSACTIONS, 1_024); diff --git a/src/domain/ticket.rs b/src/domain/ticket.rs @@ -427,6 +427,7 @@ mod tests { change: Vec::new(), amount: 42, fee: 1, + anchor: None, signature: "b".repeat(128), }; diff --git a/src/domain/transaction.rs b/src/domain/transaction.rs @@ -102,6 +102,8 @@ pub enum Transaction { amount: Amount, #[serde(default)] fee: Amount, + #[serde(default, skip_serializing_if = "Option::is_none")] + anchor: Option<String>, signature: String, }, Mine { @@ -162,6 +164,7 @@ impl Transaction { change: change.clone(), amount, fee: 0, + anchor: None, }; let signature = hex_hash(format!("iuna-genesis-burn:{}", unsigned.canonical())); Self::Burn { @@ -169,6 +172,7 @@ impl Transaction { change, amount, fee: 0, + anchor: None, signature, } } @@ -228,6 +232,13 @@ impl Transaction { matches!(self, Self::Burn { .. }) } + pub(super) fn burn_anchor(&self) -> Option<&str> { + match self { + Self::Burn { anchor, .. } => anchor.as_deref(), + Self::Transfer { .. } | Self::Mine { .. } => None, + } + } + pub fn canonical(&self) -> String { format!("{}:{}", self.signing_payload(), self.signature()) } @@ -260,12 +271,14 @@ impl Transaction { change, amount, fee, + anchor, .. } => UnsignedUtxoTransaction::Burn { inputs: unsigned_inputs(inputs), change: change.clone(), amount: *amount, fee: *fee, + anchor: anchor.clone(), } .canonical(), Self::Mine { @@ -393,12 +406,14 @@ impl Transaction { change, amount, fee, + anchor, .. } => UnsignedUtxoTransaction::Burn { inputs: unsigned_inputs(inputs), change: change.clone(), amount: *amount, fee: *fee, + anchor: anchor.clone(), } .signing_bytes(domain), Self::Mine { .. } => unreachable!("mine transactions use proof hashes"), @@ -439,6 +454,7 @@ pub(super) enum UnsignedUtxoTransaction { change: Vec<TxOutput>, amount: Amount, fee: Amount, + anchor: Option<String>, }, } @@ -473,12 +489,14 @@ impl UnsignedUtxoTransaction { change, amount, fee, + anchor, .. } => Transaction::Burn { inputs: signed_inputs, change, amount, fee, + anchor, signature, }, }) @@ -506,11 +524,19 @@ impl UnsignedUtxoTransaction { change, amount, fee, - } => format!( - "utxo-burn:{}:{}:{amount}:{fee}", - canonical_inputs(inputs), - canonical_outputs(change) - ), + anchor, + } => match anchor { + Some(anchor) => format!( + "utxo-burn-v2:{}:{}:{amount}:{fee}:{anchor}", + canonical_inputs(inputs), + canonical_outputs(change) + ), + None => format!( + "utxo-burn:{}:{}:{amount}:{fee}", + canonical_inputs(inputs), + canonical_outputs(change) + ), + }, } } @@ -533,12 +559,18 @@ impl UnsignedUtxoTransaction { change, amount, fee, + anchor, } => { - bytes.push(2); + bytes.push(if anchor.is_some() { 3 } else { 2 }); encode_inputs(&mut bytes, inputs)?; encode_outputs(&mut bytes, change)?; bytes.extend_from_slice(&amount.to_be_bytes()); bytes.extend_from_slice(&fee.to_be_bytes()); + if let Some(anchor) = anchor { + let anchor = decode_canonical_hex_array::<HASH_BYTES>(anchor) + .context("burn anchor is invalid")?; + encode_bytes(&mut bytes, &anchor, "burn anchor")?; + } } } Ok(bytes) diff --git a/src/domain/validation.rs b/src/domain/validation.rs @@ -71,6 +71,7 @@ pub(super) fn canonical_transaction_size_bytes(transaction: &Transaction) -> usi change, amount, fee, + anchor, signature, } => { 1 + compact_len(inputs.len() as u128) @@ -79,6 +80,7 @@ pub(super) fn canonical_transaction_size_bytes(transaction: &Transaction) -> usi + compact_outputs_size_bytes(change) + compact_len(u128::from(*amount)) + compact_len(u128::from(*fee)) + + anchor.as_ref().map_or(0, |anchor| hash_size_bytes(anchor)) + signature_size_bytes(signature) } Transaction::Mine {