Just want to spitball and create some discussion around a potential DoS vector that can affect some Smart Contracts. At the very least, I think it’s a point that’s worth noting (and accommodating) for contract devs as it’s not immediately obvious.
- Imagine a two-party contract where the second party joins the contract later and provides a payout address (as LockingBytecode) as DATA in some form (not as an output at this point).
- The contract is setup to settle later and the contract will have two outputs:
a) To 1st party’s provided locking bytecode
b) To 2nd party’s provided locking bytecode
Example just to demonstrate the concept:
contract ExampleContract (bytes partyALockingBytecode) {
function joinContract(bytes partyBLockingBytecode) {
// Require that the value of the commitment is B's Locking Bytecode.
require(tx.outputs[0].nftCommitment == partyBLockingBytecode);
// Require that it pays back to itself (for simplicity).
require(tx.outputs[0].lockingBytecode == tx.inputs[0].lockingBytecode);
}
function payout() {
// Get the amount available in contract.
int amountAvailableInContract = tx.inputs[0].value - 1000;
// Get Party B Locking Bytecode from NFT commitment.
bytes partyBLockingBytecode = tx.inputs[0].nftCommitment;
// Funds are then returned 50-50 to each party.
require(tx.outputs[0].value == amountAvailableInContract / 2);
require(tx.outputs[0].lockingBytecode == partyALockingBytecode);
require(tx.outputs[1].value == amountAvailableInContract / 2);
require(tx.outputs[1].lockingBytecode == partyBLockingBytecode);
}
}
- The DoS vector here is that the 2nd party can provide arbitrary locking bytecode (in
joinContract()
) that does not meet lockscript standardness (e.g.0xbabecafe
). Thus, when the contract is attempting to settle (viapayout()
), it’ll be rejected by the network due to this non-standard lockscript - thereby preventing Party A from ever claiming their funds (the tx is DoS’d).
For such contracts, Lockscript Standardness should probably be enforced in the contract itself. Currently, this can be done by doing something like the following in joinContract()
(to support P2PKH or P2SH32):
// Require that the Locking Bytecode is P2SH32 if it is 35 bytes long.
// OP_HASH256[0xaa] OP_DATA_32[0x20] <32 bytes> OP_EQUAL[0x87]
if(lockingBytecode.length == 35) {
require(lockingBytecode.split(2)[0] == 0xaa20);
require(lockingBytecode.split(34)[1] == 0x87);
}
// Otherwise, require that the Locking Bytecode is P2PKH.
// OP_DUP[0x76] OP_HASH160[0xa9] OP_DATA_20[0x14] <20 bytes> OP_EQUALVERIFY[0x88] OP_CHECKSIG[0xac]
else {
require(lockingBytecode.split(3)[0] == 0x76a914);
require(lockingBytecode.split(23)[1] == 0x88ac);
}
To allow ANY kind of Lockscript that meets standardness would be cumbersome (and OP_CODE expensive though). I don’t have a good solution to this, but just want to put this out there to maybe spur some ideas (and also as a caution to devs).
To spitball a few ideas briefly:
- Reconsider lockscript standardness constraints (doing this could introduce even worse side-effects).
- Consider an OP_CODE that can check if the lockscript type is supported (I don’t know if this is a good idea unless the lockscript standardness rules make it into consensus).
- Short term, consider a CashScript wrapper function to check this (isP2PKHLockingBytecode, isP2SH32LockingBytecode, etc). This doesn’t really solve anything, but might make it a bit easier for Contract Devs (less chance of manual error in the checks).
- For now though, it’s probably advisable to do checking similar to the above and having contracts explicitly whitelist the Lockscript types it wants to support.