Bitcoin Forum
September 08, 2025, 01:31:37 AM *
News: Latest Bitcoin Core release: 29.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: Optional Hourglass is now deployed  (Read 103 times)
stwenhao (OP)
Sr. Member
****
Offline Offline

Activity: 464
Merit: 906


View Profile
August 29, 2025, 09:02:38 AM
 #1

There are many quantum proposals. Some are better, some are worse, but when it comes to deployed things, there is a lot of silence. To change it, I tried to deploy one of competing proposals, in a no-fork way. And it seems that Hourglass can be used right away, without any consensus changes, as long as it will be left optional, and users will voluntarily join, by moving their coins to specific P2WSH scripts.

Here is the optional Hourglass envelope:
Code:
 Input: <signature> <pubkey>
Output: OP_SWAP OP_SIZE OP_DUP OP_ADD OP_DUP OP_ADD OP_CHECKSEQUENCEVERIFY OP_DROP OP_SWAP OP_CODESEPARATOR OP_DUP OP_HASH160 <pubkeyHash> OP_EQUALVERIFY OP_CHECKSIG

Execution:

<signature> <pubkey>
<pubkey> <signature>
<pubkey> <signature> <sigSize>
<pubkey> <signature> <sigSize> <sigSize>
<pubkey> <signature> <sigSize*2>
<pubkey> <signature> <sigSize*2> <sigSize*2>
<pubkey> <signature> <sigSize*4>
<pubkey> <signature> <sigSize*4>
<pubkey> <signature>
<signature> <pubkey>
<signature> <pubkey>
<signature> <pubkey> <pubkey>
<signature> <pubkey> <pubkeyHash>
<signature> <pubkey> <pubkeyHash> <pubkeyHash>
<signature> <pubkey>
OP_TRUE
To make it, all that is needed, is to take existing P2WPKH address, where coins are sent to 160-bit hash of the public key, and wrap it into the Script above. For example, if the private key is equal to one, and the public key is equal to the generator, then it can look like that:
Code:
decodescript 210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac
{
  "asm": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 OP_CHECKSIG",
  "desc": "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)#gn28ywm7",
  "type": "pubkey",
  "p2sh": "2MvVwHhgE2JyjkjQk72CghrhrJsanKfHfqe",
  "segwit": {
    "asm": "0 751e76e8199196d454941c45d1b3a323f1433bd6",
    "desc": "addr(tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx)#0wnhlaqf",
    "hex": "0014751e76e8199196d454941c45d1b3a323f1433bd6",
    "address": "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
    "type": "witness_v0_keyhash",
    "p2sh-segwit": "2NAUYAHhujozruyzpsFRP63mbrdaU5wnEpN"
  }
}
decodescript 7c8276937693b2757cab76a914751e76e8199196d454941c45d1b3a323f1433bd688ac
{
  "asm": "OP_SWAP OP_SIZE OP_DUP OP_ADD OP_DUP OP_ADD OP_CHECKSEQUENCEVERIFY OP_DROP OP_SWAP OP_CODESEPARATOR OP_DUP OP_HASH160 751e76e8199196d454941c45d1b3a323f1433bd6 OP_EQUALVERIFY OP_CHECKSIG",
  "desc": "raw(7c8276937693b2757cab76a914751e76e8199196d454941c45d1b3a323f1433bd688ac)#9hnlfmtv",
  "type": "nonstandard",
  "p2sh": "2Mzw53ZN8wS8FXvNpMFkGjfP3i3hCTAhcbV",
  "segwit": {
    "asm": "0 3062edde70aad88f218ed97aa51f6003baeb94c4c4d876d86ddf8dce6f5d4c06",
    "desc": "addr(tb1qxp3wmhns4tvg7gvwm9a228mqqwawh9xycnv8dkrdm7xuum6afsrqcuzu88)#y06denaf",
    "hex": "00203062edde70aad88f218ed97aa51f6003baeb94c4c4d876d86ddf8dce6f5d4c06",
    "address": "tb1qxp3wmhns4tvg7gvwm9a228mqqwawh9xycnv8dkrdm7xuum6afsrqcuzu88",
    "type": "witness_v0_scripthash",
    "p2sh-segwit": "2N8BaDhNcPriSpqLqNKWBc1uPXhRMDuA7To"
  }
}
And then, coins can be sent for example to tb1qxp3wmhns4tvg7gvwm9a228mqqwawh9xycnv8dkrdm7xuum6afsrqcuzu88. Then, as long as the private key is unknown, everything will work just like for regular P2WPKH, which is tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx in this case. For signatures taking around 72 bytes, coins will be timelocked to 288 blocks (around two days), but other than that, it can work just like its P2WPKH equivalent (and it can be signed in exactly the same way, because of used OP_CODESEPARATOR, so there is no need to change signing code).

However, when the private key will be known, then coins will be protected by a Proof of Work challenge. Lowering signature size by a single byte will allow moving them 4 blocks earlier. Which means, that even if both secp256k1 and SHA-256 will be fully broken, then 9-byte signature will still timelock coins for 36 blocks (around 6 hours). And if for example only secp256k1 will be broken, then some 40-byte signature will still need to wait 160 blocks, so it usually means more than one day of delay.

What do you think about it? Also, as usual, check things in test networks first, before moving any of your mainnet coins to such addresses.

Note: I slightly modified my idea from my mailing list post, because if the public key is wrapped behind OP_HASH160, and separated from the rest of the Script with OP_CODESEPARATOR, then it is more compatible with P2WPKH, and also, revealing the Script under P2WSH can be done, without revealing the corresponding public key.

Proof of Work puzzle in mainnet and testnet4.
BattleDog
Newbie
*
Offline Offline

Activity: 14
Merit: 14


View Profile
September 02, 2025, 12:12:36 AM
Merited by stwenhao (1)
 #2

Neat trick and there's a few caveats imo,

This is not a drop-in P2WPKH
Segwit v0 ignores OP_CODESEPARATOR in the sighash. Changing the script changes the digest. You cannot reuse a plain P2WPKH signer; you need custom wallet logic that signs the exact witnessScript. Most wallets and HWWs will not work out of the box.

If ECDSA or SHA-256 is broken, an attacker who knows the key can deliberately craft tiny DER (eg 9–12 bytes) and get the shortest delay. Defenders must be online, watching, and able to beat that with an even smaller sig. That is a race, not a safety net. Thirty-six blocks is not much of a rescue window against a motivated sweeper.

Very short DER is valid under consensus but can trip standardness policy on some nodes. Expect inconsistent relay until you test across versions. Also double-check your CSV math: OP_CHECKSEQUENCEVERIFY uses BIP68 semantics; ensure you set the type bits for blocks vs time correctly.

CSV enforces the most restrictive sequence across all inputs. If someone mixes one of these with a 0-sequence input, the whole tx will be non-final. You likely need a one-input consolidation flow.

OP_SIZE reads the signature including the sighash byte; OP_CHECKSIG consumes it. You must dup/drop in the right order or your CHECKSIG will see the wrong stack. Unit-test with vectors for 71/72/73-byte sigs and a synthetic 9-byte sig on regtest.

Because it is P2WSH with custom logic, ordinary wallets cannot send or spend without special tooling. Calling it P2WPKH-compatible might confuse users.

If your goal is a no-fork hedge, a boring alternative might just be a vaulty P2WSH: key path with CSV 144–288 to a hot key plus an immediate cold path.
stwenhao (OP)
Sr. Member
****
Offline Offline

Activity: 464
Merit: 906


View Profile
September 02, 2025, 04:02:59 AM
Merited by vapourminer (1), BattleDog (1)
 #3

Quote
You cannot reuse a plain P2WPKH signer
Yes, but Bitcoin Core signer is close enough to be used. For example:
Code:
createrawtransaction '[{"txid":"0b4b0db2652cb346205e685a2b1494188950523ed49cc54beae479afd7831d43","vout":0,"sequence":300}]' '[{"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx":0.00049880}]' 0 true
0200000001431d83d7af79e4ea4bc59cd43e5250891894142b5a685e2046b32c65b20d4b0b00000000002c01000001d8c2000000000000160014751e76e8199196d454941c45d1b3a323f1433bd600000000
signrawtransactionwithkey "0200000001431d83d7af79e4ea4bc59cd43e5250891894142b5a685e2046b32c65b20d4b0b00000000002c01000001d8c2000000000000160014751e76e8199196d454941c45d1b3a323f1433bd600000000" '[" cMahea7zqjxrtgAbB7LSGbcQUr1uX1ojuat9jZodMN87JcbXMTcA"]' '[{"txid":"0b4b0db2652cb346205e685a2b1494188950523ed49cc54beae479afd7831d43","vout":0,"scriptPubKey":"0014751e76e8199196d454941c45d1b3a323f1433bd6","amount":0.00050000}]' "SINGLE|ANYONECANPAY"
{
  "hex": "02000000000101431d83d7af79e4ea4bc59cd43e5250891894142b5a685e2046b32c65b20d4b0b00000000002c01000001d8c2000000000000160014751e76e8199196d454941c45d1b3a323f1433bd602473044022057bd466515b398e3643bd290492e9af1e0a004e58017b7302e0a80b5ff186f0c02205aa84d9468e49c1128ece23dd5474883762e83663c53fa382575585b59dfeb7883210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000",
  "complete": true
}
And then, we can split it into parts:
Code:
txBegin=02000000000101431d83d7af79e4ea4bc59cd43e5250891894142b5a685e2046b32c65b20d4b0b00000000002c01000001d8c2000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6
witnessSize=02
witnessData=473044022057bd466515b398e3643bd290492e9af1e0a004e58017b7302e0a80b5ff186f0c02205aa84d9468e49c1128ece23dd5474883762e83663c53fa382575585b59dfeb7883210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
locktime=00000000
Then, between "witnessData" and "locktime", we have to put our Script, and increase "witnessSize" into "03":
Code:
decoderawtransaction 02000000000101431d83d7af79e4ea4bc59cd43e5250891894142b5a685e2046b32c65b20d4b0b00000000002c01000001d8c2000000000000160014751e76e8199196d454941c45d1b3a323f1433bd603473044022057bd466515b398e3643bd290492e9af1e0a004e58017b7302e0a80b5ff186f0c02205aa84d9468e49c1128ece23dd5474883762e83663c53fa382575585b59dfeb7883210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798237c8276937693b2757cab76a914751e76e8199196d454941c45d1b3a323f1433bd688ac00000000
{
  "txid": "2f118ad613dc03defab7ec6078164357d37e1cfde64883d78de0dd11de9f962a",
  "hash": "1ef40752821f960e5b118394ea10669aa560d28c5fd53a34992cd9684d4b9bb8",
  "version": 2,
  "size": 227,
  "vsize": 119,
  "weight": 473,
  "locktime": 0,
  "vin": [
    {
      "txid": "0b4b0db2652cb346205e685a2b1494188950523ed49cc54beae479afd7831d43",
      "vout": 0,
      "scriptSig": {
        "asm": "",
        "hex": ""
      },
      "txinwitness": [
        "3044022057bd466515b398e3643bd290492e9af1e0a004e58017b7302e0a80b5ff186f0c02205aa84d9468e49c1128ece23dd5474883762e83663c53fa382575585b59dfeb7883",
        "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
        "7c8276937693b2757cab76a914751e76e8199196d454941c45d1b3a323f1433bd688ac"
      ],
      "sequence": 300
    }
  ],
  "vout": [
    {
      "value": 0.00049880,
      "n": 0,
      "scriptPubKey": {
        "asm": "0 751e76e8199196d454941c45d1b3a323f1433bd6",
        "desc": "addr(tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx)#0wnhlaqf",
        "hex": "0014751e76e8199196d454941c45d1b3a323f1433bd6",
        "address": "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
        "type": "witness_v0_keyhash"
      }
    }
  ]
}
So, maybe it is not 1:1 replacement, but it is close enough, to sign something directly from Bitcoin Core, tweak it here and there, and then broadcast. Which means, that potential code changes are minimal, to reach any valid signature. And then, it is all about grinding it further, to beat any attackers. And that grinding is exactly in Hourglass's spirit: in that proposal, miners also could steal coins, but their ability to do that was limited by consensus rules, just like it is here (so, that kind of models can be tested, before any soft-fork will be deployed).

Quote
If ECDSA or SHA-256 is broken, an attacker who knows the key can deliberately craft tiny DER (eg 9–12 bytes) and get the shortest delay.
Crafting 9-12 bytes signature will be extremely hard in practice. If SHA-256 will be broken, then yes, 9-byte signatures will be possible, but then, the whole chain can be overwritten with anything. However, if secp256k1 will be broken, then r-value will have one byte, but grinding s-value will still require gradually breaking SHA-256. Which means, that different miners will still have to compete, and grind smaller and smaller signatures, to get the coins earlier, than the rest. And it still means starting from something like 40-byte signature, so something like 160 block timelock.

Quote
That is a race, not a safety net.
Original Hourglass is also a race.

Quote
Thirty-six blocks is not much of a rescue window against a motivated sweeper.
People can repeat "OP_DUP OP_ADD" as many times, as they want, or add some constant, to make that window bigger.

Quote
ensure you set the type bits for blocks vs time correctly
Type flag is bit number 22. It means 0x00400000, so at least 1 MB signature. It is very hard to achieve that in P2WSH, where you have 520 byte stack size limit. Which means, that you can get something like 2080 block timelock in case of the biggest signature. Which also means, that if you use 2048 as your factor (around two weeks per byte), then it will still be 520*2048=1064960 blocks of delay. Still not enough to reach 4194304, and every value below that is just a timelock, measured in the number of blocks. Also, opcodes like OP_LESSTHAN or OP_WITHIN can restrict things further, if users need it.

Quote
Because it is P2WSH with custom logic, ordinary wallets cannot send or spend without special tooling.
They can get quite close with existing tools, so potential changes are quite small (it is all about adding witness stack push with the Script, and also grinding the signature with Proof of Work).

Quote
Calling it P2WPKH-compatible might confuse users.
Maybe. But it is as close to be compatible, as it could be. If things are separated with OP_CODESEPARATOR, then z-value can be computed by existing wallets, so that users don't have to fight with Segwit format, and they can focus on getting from Bitcoin Core, what they can, and then improve it a little with any external tools.

Proof of Work puzzle in mainnet and testnet4.
BattleDog
Newbie
*
Offline Offline

Activity: 14
Merit: 14


View Profile
September 02, 2025, 09:56:34 AM
Merited by stwenhao (1)
 #4

Great follow-up and nice demo with Core. Yes, segwit v0 ignores CODESEPARATOR in the sighash, so you can sign the P2WSH witnessScript with Core and assemble the witness stack afterward. That path is workable.

I'd suggest a few things to lock down before mainnet coins touch it:

The operand to OP_CHECKSEQUENCEVERIFY uses BIP68 semantics: value is 16 bits, and the type flag is bit 22 (1 << 22 = 0x00400000). So the max enforceable delay is 65535 blocks (~455 days) in block mode. Adding numbers in script will not extend that beyond 65535; the mask clips it.

P2WSH still has MAX_OPS_PER_SCRIPT = 201 (non-push) and 520-byte max per stack item. Building a huge constant with many OP_ADDs will hit the ops limit long before you reach any big multiplier. A more robust pattern is bucketed delays:
OP_SIZE -> compare against a few ranges with OP_WITHIN or OP_LESSTHAN -> choose one of several fixed CSV values. You can cover 4–6 ranges well under the limits.

Very short DER is consensus valid if strictly DER and low-s, but policy varies by version. Expect inconsistent mempool relay for tiny sigs.
I would strongly advise to test against multiple Core versions on regtest and with a couple public nodes.

Consider a PSBT flow that includes the full witnessScript up front. Let Core sign that, then you only modify the witness stack elements, not the script. That reduces footguns compared to splitting and re-gluing blobs by hand.

Even with grinding costs, shortest-sig wins is still a race. Add a base CSV (for example 144 or 288 blocks) and then a small per-bucket increment based on OP_SIZE. That gives defenders a predictable window and still keeps the hourglass flavor.

Perhaps it should be called P2WSH Hourglass, not P2WPKH-compatible. Wallets and HWWs will need custom tooling; being precise here can save user confusion.
Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!