variable pow

#1 · closed · 13 comments

View on GitHub ↗

ajtowns

https://github.com/theStack/bitcoin-inquisition/blob/81f7845d676c17dee3774c2d4a83954a59d4bf31/test/functional/feature_opcat.py#L141-L147 To turn this into variable pow, I think it's probably easiest to just hard code the delay and pow requirement: if you start at difficulty 2**46, that's higher than mainnet, then halve every 10 blocks, you're down to difficulty 1 after about three days. Halving the difficulty is a little complicated: you need `H = HA || HB || HC` where HC is all zeroes, HB is one byte, not negative, and less than or equal to [127, 63, 31, 15, 7, 3, 1, 0]. ```python def faucat_script(n): assert 0 <= n < 47 delay = (47-n)*10 zeroes = b'\0' * (n//8 + 4) prefix = [127, 63, 31, 15, 7, 3, 1, 0][n%8] return CScript([ delay, OP_CSV, OP_DROP, # check delay (256*n + HC) OP_DUP, prefix, OP_LESSTHANOREQUAL, OP_VERIFY, # prefix is below targer OP_DUP, OP_SIZE, 1, OP_EQUALVERIFY, 0, OP_GREATERTHANOREQUAL, OP_VERIFY, # no funny business! zeroes, OP_CAT, OP_CAT, OP_TOALTSTACK, # save H = HA || HB || HC OP_OVER, OP_SIZE, 64, OP_EQUALVERIFY # grab signature, check it's 64B OP_CAT, OP_SHA256, # re-calculate hash, , i.e. sha256(nonce || sig) == H OP_FROMALTSTACK, OP_EQUALVERIFY, # check PoW OP_SWAP, OP_SIZE, 32, OP_EQUALVERIFY, # check pubkey is 32B, no funny business! OP_CHECKSIG, # verify signature ]) ``` Then construct a taptree with 47 elements and you have a static address. There's probably silly bugs in the above, but in theory it commits to the difficulty target, so to spend these funds, you'd first sign, then pick an `n`, then mine. Would probably make sense to have a balanced taptree of 64 leaves and allow dropping to a lower difficulty than 1. One thing that might be interesting: if you construct the data being hashed as "X SIG Y", and used "HASH256", people could make X be 4 bytes, and Y be 12 bytes, and probably would then be able to use regular mining software/hardware to grind an answer, treating the signature as the prev-block-hash and merkle-tree-root value.

Comments

ajtowns

Oh, if you're requiring sighash_default and using different tapscripts for each difficulty, the signature will commit to the target difficulty automatically and can't be reused against a different difficulty in case you're lucky and got a particularly good result.

theStack

Thanks, this is fun! :cat: Pushed some changes based on your suggestions: * "no funny business" checks: enforce 32-bytes pubkey size, 64-byte signature size (i.e. SIGHASH_DEFAULT); * fixed a bug in script-path spending, where the sign bit in the control block / leaf version byte wasn't taken into account, so every now and then a "witness program hash mismatch" error appeared (wasted many hours on this, searching on the wrong place, d'oh! :man_facepalming: ) * took over the enhanced script in the opening post for variable pow, with the script builder There is still tons of TODOs and it desperately needs more tests (it's just trying the success case with fixed n=17 now, where n=0 means all hashes pass for regtest purposes), but it's a start. I thought that H_B needs to be either zero or one bytes, as the integer 0 has to be encoded as empty byte-string (minimal-encoding rules), but as I'm writing this I'm not so sure anymore. Maybe the prefix table just has to start with value 255 and it's just fine. Will give it more thoughts tomorrow and possibly address other suggestions (being able to use regular mining software would be awesome), further feedback appreciated of course.

ajtowns

So, umm, :sweat_smile: > I thought that H_B needs to be either zero or one bytes, My theory was that you'd just choose a difficulty where your H_B was the first non-zero, but obviously committing to the difficulty in the signature breaks that. So here's a script that calculates difficulty by hand. Top of stack is "difficulty", a CScriptNum from 0 to 63, second from top is your first non-zero byte, third from top is the rest. ``` DUP DUP ADD DUP DUP ADD DUP DUP ADD ADD # multiply top of stack by 10 640 SWAP SUB # subtract from 640 CHECKSEQUENCEVERIFY DROP # wait that many blocks # put the 00-bytes onto the alt stack, one zero byte for every multiple of 8 in the difficulty 0 TOALTSTACK DUP 32 GREATERTHANOREQUAL IF 32 SUB 0x0400000000 FROMALTSTACK CAT TOALTSTACK ENDIF DUP 16 GREATERTHANOREQUAL IF 16 SUB 0x020000 FROMALTSTACK CAT TOALTSTACK ENDIF DUP 8 GREATERTHANOREQUAL IF 8 SUB 0x0100 FROMALTSTACK CAT TOALTSTACK ENDIF # difficulty should be 0-7 now; 0 means below 0x80, 1 means below 0x40, .., 6 means below 0x02, 7 means 0x00 exactly DUP 0 EQUAL IF 127 TOALTSTACK ENDIF 1SUB DUP 0 EQUAL IF 63 TOALTSTACK ENDIF 1SUB DUP 0 EQUAL IF 31 TOALTSTACK ENDIF 1SUB DUP 0 EQUAL IF 15 TOALTSTACK ENDIF 1SUB DUP 0 EQUAL IF 7 TOALTSTACK ENDIF 1SUB DUP 0 EQUAL IF 3 TOALTSTACK ENDIF 1SUB DUP 0 EQUAL IF 1 TOALTSTACK ENDIF 1SUB 0 GREATERTHANOREQUAL IF # avoid dealing with negatives when the first non-zero byte is 0x80-0xFF # by accepting a 0x00 byte exactly SIZE 1 EQUALVERIFY DUP 0x00 EQUALVERIFY ELSE SIZE 1 EQUALVERIFY DUP 0 GREATERTHAN VERIFY DUP FROMALTSTACK LESSTHANOREQUAL VERIFY ENDIF FROMALTSTACK CAT CAT ``` That leaves you with the merged hash on the top of the stack, having verified that an appropriate CSV delay has completed, but you still need to put the preimage of the hash together.

theStack

Alright, another try with the new suggestion: https://github.com/theStack/bitcoin-inquisition/commit/e3cabf0303bb0ffca267f5f896d628a4a028232a > Oh, if you're requiring sighash_default and using different tapscripts for each difficulty, the signature will commit to the target difficulty automatically and can't be reused against a different difficulty in case you're lucky and got a particularly good result. But hmm, I think even if the difficulty is not hard-coded in the witness script, the signature still commits to the difficulty by the input's nSequence field that needs to be set for OP_CSV. If one got lucky and has a better result than intended, the whole point of that would be to claim the funds faster, which means setting a lower nSequence, resulting in a different signature hash. The "dealing with negatives" part, i.e. converting single byte elements in the range 0x80-0xff to their corresponding CScriptNum elements as I understand it, could be as simple as: `OP_DUP OP_0 OP_LESSTHAN OP_IF OP_ABS 0x80 OP_ADD OP_ENDIF` Using that would be slightly easier to reason about I think, as then H_B can indeed be the first non-zero? If I'm not mistaken, there's also an off-by-one error; if the remainder of the difficulty value is zero (i.e. D % 8 == 0), then all values should be good for H_B I guess (rather than < 128), as the difficulty requirement is already satisfied by the on-the-fly generated all-zeros H_C. But independently on the script's details, I should really start to write proper tests.

ajtowns

> But hmm, I think even if the difficulty is not hard-coded in the witness script, the signature still commits to the difficulty by the input's nSequence field that needs to be set for OP_CSV. Ha, quite right. One day I'll be able to keep that in mind, but it's not this day :( So I guess the idea is that you point your hashpower at whichever utxo is oldest, sign with the maximum CSV that will get it into the next block, and try hashing to satisfy PoW; if that utxo gets spent, you target the next oldest utxo and generate a new signature; and every 10 blocks you generate another new signature with increased CSV and reduced target. You can use a single use private key that's never written to disk for each signature, so you don't have to put meaningful private keys online to do those signatures. > `OP_DUP OP_0 OP_LESSTHAN OP_IF OP_ABS 0x80 OP_ADD OP_ENDIF` Yeah, I just assumed negatives were hard and didn't try them. Also, didn't know `OP_ABS` existed! `0x80` is just negative zero, you'd need 0x028000 (push 2 bytes, 0x80 then 0x00) in the notation ParseScript uses.

theStack

> So I guess the idea is that you point your hashpower at whichever utxo is oldest, sign with the maximum CSV that will get it into the next block, and try hashing to satisfy PoW; if that utxo gets spent, you target the next oldest utxo and generate a new signature; and every 10 blocks you generate another new signature with increased CSV and reduced target. You can use a single use private key that's never written to disk for each signature, so you don't have to put meaningful private keys online to do those signatures. Sounds right. Btw, I think it would be quite interesting to also bring in faucaet UTXO's nValue as additional condition on what can be claimed depending on the difficulty (the more PoW, the more sats to claim). But being able to do that seems to be many soft-forks away, [64-bit arithmetics](https://delvingbitcoin.org/t/64-bit-arithmetic-soft-fork/397) probably being one of them. And of course, it would be even harder to stop people from thinking signet coins have value :sweat_smile: > `0x80` is just negative zero, you'd need 0x028000 (push 2 bytes, 0x80 then 0x00) in the notation ParseScript uses. Good to know. Being spoiled I just relied on the test framework's `CScript` class here (and the notation looks accordingly) which luckily just takes care of the right minimal-encoded push for bare integers in the list, e.g.: ``` $ python3 ... >>> from test_framework.script import CScript, OP_ABS, OP_ADD >>> CScript([OP_ABS, 0x80, OP_ADD]).hex() '9002800093' ```

ajtowns

> Btw, I think it would be quite interesting to also bring in faucaet UTXO's nValue as additional condition on what can be claimed depending on the difficulty I think you could do that on inquisition now: basically construct a tree of transactions with precommitted APO sigs, so that you've got: - nvalue = 0.001 BTC -> can only claim everything - nvalue = 0.002 BTC -> can claim everything with tapscript path A, with path B, have to comply with a `ANYPREVOUTANYSCRIPT | SINGLE` signature that commits to an output with nvalue = 0.001,000,00, allowing you to claim half the funds - nvalue = 0.003 BTC -> path A = claim everything, path B = APOAS|SINGLE to 0.002, path C = APOAS|SINGLE to 0.001 etc. I don't really think it's worth doing though; if you just have the faucet generate utxos with 1.0, 0.5, 0.25, 0.1, 0.05, 0.025, 0.01, etc each block with the same proof of work script, then the high value ones will get picked off first by miners with high hashpower, letting the lower value ones reduce in difficulty until lower-hashrate miners who are more desperate pick them up. In theory anyway? Here's my current thought on the script: ``` DUP 0 GREATERTHANOREQUAL VERIFY DUP 64 LESSTHAN VERIFY DUP DUP ADD DUP DUP ADD DUP ADD ADD 640 SWAP SUB CHECKSEQUENCEVERIFY DROP FALSE TOALTSTACK DUP 32 GREATERTHANOREQUAL IF FROMALTSTACK 0x00000000 CAT TOALTSTACK 32 SUB ENDIF DUP 16 GREATERTHANOREQUAL IF FROMALTSTACK 0x0000 CAT TOALTSTACK 16 SUB ENDIF DUP 8 GREATERTHANOREQUAL IF FROMALTSTACK 0x00 CAT TOALTSTACK 8 SUB ENDIF SWAP SIZE 1 EQUALVERIFY DUP TOALTSTACK 1 CAT 256 SUB SWAP DUP 0 EQUAL IF 256 ELSE DUP 1 EQUAL IF 128 ELSE DUP 2 EQUAL IF 64 ELSE DUP 3 EQUAL IF 32 ELSE DUP 4 EQUAL IF 16 ELSE DUP 5 EQUAL IF 8 ELSE DUP 6 EQUAL IF 4 ELSE 2 ENDIF ENDIF ENDIF ENDIF ENDIF ENDIF ENDIF NIP LESSTHAN VERIFY FROMALTSTACK FROMALTSTACK CAT CAT TOALTSTACK // sig pubkey prefix suffix 2OVER SIZE 32 EQUALVERIFY DROP SIZE 64 EQUALVERIFY SWAP CAT CAT HASH256 FROMALTSTACK EQUALVERIFY CHECKSIG ``` EDIT: I don't entirely like the "prefix + sig + postfix" approach; it lets an attacker select a different portion of the preimage as the signature. Probably impossible to attack, but I also think you can fix that pretty easily: replace `SWAP CAT CAT HASH256 FROMALTSTACK EQUALVERIFY` with ``` SWAP SIZE SWAP CAT CAT SWAP SIZE SWAP CAT SWAP CAT HASH256 FROMALTSTACK EQUALVERIFY ``` which just puts the sizes in front of them. Then you can make the prefix be 3 bytes, and the suffix be 11 bytes, and it will look a lot like a block header.

ajtowns

> EDIT: I don't entirely like the "prefix + sig + postfix" approach; it lets an attacker select a different portion of the preimage as the signature. Probably impossible to attack, Ah, not impossible to attack. If you make the preimage be "sig1+sig2+sig3+sig4+nonce" you can do work on multiple coins at once, then use the same pow to claim each coin by changing what the prefix/postfix is for each coin.

ajtowns

How about this: https://github.com/ajtowns/bitcoin/tree/202405-inq27-powcoins ``` $ cd src/ $ make $ ./src//bitcoind -signet -addnode=inquisition.bitcoin-signet.net -daemon $ cd contrib/signet $ ./powcoins setup-wallet $ ./powcoins --debug claim --max-diff=29 tb1qzv66dscl3uauh5jesfem33wws747naapcgt4c5 DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'decodescript', '51205d21df7d162147d4c071d79abfce36a7b160a05ad2d9c21984a0b2173a2e2903'] DEBUG:root:Faucet address tb1pt5sa7lgky9rafsr367dtln3k57ckpgz66tvuyxvy5zepww3w9yps348a9q (delay=10) DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'decodescript', '51201037da469c419050b6d3f1113c2f19ad18dfceaf6b31df9dd13888bc73ae0e58'] DEBUG:root:Faucet address tb1pzqma535ugxg9pdkn7ygnctce45vdln40dvcal8w38zytcuawpevqg75lcj (delay=4) DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'decodescript', '5120b0037812bd4c186640d7ba13587cdfb62f7c0a34948c89296ae8d157c976977c'] DEBUG:root:Faucet address tb1pkqphsy4afsvxvsxhhgf4slxlkchhcz35jjxgj2t2arg40jtkja7qlkv2hl (delay=1) DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', '-rpcwallet=varpow', 'listunspent'] DEBUG:root:nbits = 01000021 difficulty = 16 csv = 48 invdiff = 48 delay = 1 DEBUG:root:GRIND 0300000041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c0b0000000100002100000000 DEBUG:root:GROUND 0300000041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c0b0000000100002146fa0000 DEBUG:root:tx 02000000000101f9de43e6712e7194d89bfd2847c3d967275d9b2106fa456a172e1fd07b6cae96030000000030000000011095be00000000001600141335a6c31f8f3bcbd2598273b8c5ce87abe9f7a1094041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798030000000b0000000100002146fa00001d4fa2d7ce8d2836e796e0b30af29ddb5c3064b3468614914f068c522fa5012b0110a27651a2697601409f697601407c94b275006b760120a2636c04000000007e6b012094687660a2636c0200007e6b6094687658a2636c01007e6b5894687c825188766b517e020001947c7600876302000167765187630280006776528763014067765387630120677654876360677655876358677656876354675268686868686868779f696c6c7e7e6b708201208875820140887c827c7e7e7c827c7e7c7eaa6c88ac21c04a981eda2acd9bf5d789fa1a9b7f95677776d99fd13ec0f658f0e4dbc5b5c87800000000 DEBUG:root:Calling bitcoin-cli: ['bitcoin-cli', '-signet', 'sendrawtransaction', '02000000000101f9de43e6712e7194d89bfd2847c3d967275d9b2106fa456a172e1fd07b6cae96030000000030000000011095be00000000001600141335a6c31f8f3bcbd2598273b8c5ce87abe9f7a1094041609574014206a229f43cb71635ad3b666525a4a759370d6b471af85b7148c5781196a0abe8ca7266d647a70423a385a65abdae625ab2091871ac24d3f8744c2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798030000000b0000000100002146fa00001d4fa2d7ce8d2836e796e0b30af29ddb5c3064b3468614914f068c522fa5012b0110a27651a2697601409f697601407c94b275006b760120a2636c04000000007e6b012094687660a2636c0200007e6b6094687658a2636c01007e6b5894687c825188766b517e020001947c7600876302000167765187630280006776528763014067765387630120677654876360677655876358677656876354675268686868686868779f696c6c7e7e6b708201208875820140887c827c7e7e7c827c7e7c7eaa6c88ac21c04a981eda2acd9bf5d789fa1a9b7f95677776d99fd13ec0f658f0e4dbc5b5c87800000000'] INFO:root:sendrawtransaction result 0193ae789de9f3426521e3cc2a4698e0d9f5a5351dfcb95c018cf7817029b584 ``` which is then pushed over the network and [eventually mined](https://mempool.space/signet/tx/0193ae789de9f3426521e3cc2a4698e0d9f5a5351dfcb95c018cf7817029b584) I did three different scripts -- one that halves the difficulty every block, one every 4 blocks, and one every 10 blocks; and am sending 0.125 sBTC, 1 sBTC and 2 sBTC to them respectively each block. It's using `bitcoin-util grind` to solve the nonce, setting the fake block header's prevhash and merkleroot to the signature, and setting nBits to match the desired difficulty, so I think in theory you could replace that with an interface to a mining pool. I think I did the math right so that it'll always go for whichever coin gives you the most value for your hashpower.

theStack

Neat! I haven't had a chance yet to look closer at the latest proposal of the script, but using that powcoins tool from your branch worked flawlessly with `--max-diff=29`: https://mempool.space/signet/tx/95e85e4a2509ee01af974f0576717a4616d940f781764e13ef370adbded62737 As a small side-detail, in a "real-world scenario" (if that even makes sense) I think the internal pubkey should be provably unspendable, to not disincentivize claimers from doing PoW? (maybe it already is, but at least `ipk` doesn't seem to be NUMS_H)

ajtowns

> Neat! I haven't had a chance yet to look closer at the latest proposal of the script, but using that powcoins tool from your branch worked flawlessly with `--max-diff=29`: https://mempool.space/signet/tx/95e85e4a2509ee01af974f0576717a4616d940f781764e13ef370adbded62737 As a small side-detail, in a "real-world scenario" (if that even makes sense) I think the internal pubkey should be provably unspendable, to not disincentivize claimers from doing PoW? (maybe it already is, but at least `ipk` doesn't seem to be NUMS_H) I set ipk to a pubkey from my signet wallet, so that in theory if I messed up the script I could reclaim the funds. Switching to `NUMS_H` seems like a good idea once it's a bit more tested. After talking with @kallewoof I've changed the difficulty range from 1-64 to 16-80; 80 is about 220k times more difficult than finding a block on mainnet, so should be good for a while :)

ajtowns

> One thing that might be interesting: if you construct the data being hashed as "X SIG Y", and used "HASH256", people could make X be 4 bytes, and Y be 12 bytes, and probably would then be able to use regular mining software/hardware to grind an answer, Looking into [stratumv1 code](https://github.com/skot/ESP-Miner/blob/master/components/stratum/mining.c), it seems like this was a bit too simplistic, and you actually want to provide a coinbase tx and so on; so I think a better construction would be: * version genesis-hash merkle-root timestamp nbits nonce (80 byte header, hash of this must meet proof of work target) * merkle-root = sha256d( 32byte "coinbase" hash, sha256( sig ) ) So the data you provide is: * pubkey, sig (96 bytes) * prefix = version, genesis-hash (36 bytes) * suffix = timestamp nbits nonce (12 bytes) * cbhash = "coinbase" hash (32 bytes) You could perhaps use G/2 as the pubkey and move its zero-bytes to the end and reuse it as a fake block hash to save ~26 bytes while still looking pretty legit (ie `"0000000000000000000000" "3b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63" 2DUP SWAP CAT TOALT CAT` or so).

ajtowns

Apart from compat with stratumv1, this is all done on https://github.com/ajtowns/powcoins/ I think