[NIP] Allow some Output Lock Scripts to Validate

[NIP] Allow some Output Lock Scripts to Validate

Meta

For context, NIP stands for Nervos Improvement Proposal. This is part of a list of changes that we’d like to see on L1. This stems from our experience on the field, building protocols on top of L1.

Context

Lock Script vs Type Scripts

In the Nervos Network, the distinction between type scripts and lock scripts is fundamental to understanding how transactions are validated:

  1. Lock Scripts: These are primarily responsible for defining the conditions under which a specific output can be spent. They act as a form of access control, determining who can use the funds associated with a particular output.
  2. Type Scripts: In contrast, type scripts are more versatile and are executed on outputs. They can contain logic that not only validates transactions but also defines how the output can be used in future transactions.

In summary, lock scripts are about access control, while type scripts enable more complex interactions and state changes.

L2 are Not the Solution

For example, on one side, L1 lacks a Shared-State Execution Layer, so there is no efficient shared-state execution layer on Nervos.

On the other side, while the discussion is still very much open, it seems that generic L2 solutions are currently not viable:

  1. Historical Failures: Past L2 initiatives have not achieved sustainable development due to timing issues and insufficient developer interest, leading to skepticism about future L2 efforts.

  2. Complex Implementation: Developing a new L2 with shared state is complex and resource-intensive. The effort required may not yield better results than enhancing existing solutions on L1.

  3. Ecosystem Readiness: The Nervos ecosystem is not thriving enough to support a generic L2. There is a need for focused solutions that address specific use cases right on L1 rather than a broad, generic approach.

In summary, the combination of these factors makes L2 solutions currently impractical and less appealing compared to enhancing the L1 capabilities.

Proposal: Allow some Output Lock Scripts to Validate

In light of building directly on top of L1 and enhancing its capabilities, I believe that we should talk more openly how to improve the current L1.

I’d like to extend the functionalities of Lock Scripts to closely follow those those of Type Scripts.

Blasphemy!!!

Okay, well, traditionally Lock Scripts are primarily responsible for defining who can spend a cell, so the inclusion of a lock exclusively as output currently means:

  1. The lock will not be needed as CellDep in that tx
  2. The lock will not be able to validate the output cell

Which brings:

  1. Output cell lock may be unwittingly invalid, for example in its format.
  2. Output cell lock may be maliciously invalid, for example forging cells.

The first problem usually is not an issue. Sure, it may irremediably lock up user funds, but usually enough testing before production can iron out these problems.

The second use-case is much deeper and problematic, leading to attacks that are difficult to discover by reviews & audits and even more difficult to prevent.

The current best practice is: use Type Scripts to validate protocol logic.

And that’s sounds okayish until you discover that:

  1. Type Script of cells are already occupied by UDT Types Script. (While xUDT attempted to allow custom logic execution, it ultimately failed in this regard. Also not all Types are xUDT.)
  2. A user lock on a cell fully prevent the cell from being unlocked with custom logic.
  3. A protocol must not be designed against a specific lock, even when this lock would allow a secondary unlock.
  4. CoBuild may alleviate this problem, but Cobuild may come or not, it is not a given. Also, it may or not fully solve this kind of issues as the advice would still be: design protocols that can work with or without CoBuild.

Which points to: Lock Script must be able to safely validate protocol logic, even when only included as output.

Well, putting religious judgment aside and understood that L2 will not save us all, this is already creating issues for securing L1 protocols and so it must change as soon as possible.

Implementation Ideas

I dream for all output locks on Nervos L1 to be free of their shakles!!

Since that may not be realistic, because old Lock Scripts may throw errors in this new environment, we need to design a way that allow selected Lock Script to execute as output while keeping the compatibility with old locks and changing as little as possible the current infrastructure.

A few ideas that come to mind are:

  1. Reserve one bit in the type of Script. Now we have type, data, data1, data2.... I’d like to propose type+x, data+x, data1+x, data2+x.... Ideally over time, most references would migrate to the ...+x variant as it’s just safer.
  2. All new type of Script always execute also when included output lock, so for example all new data3... where all cell scripts execute always. Nice, but reference by type are always upgraded to the latest VM version, so this change could cause major breakages unless we also change the behavior of references by type (which we should BTW).

Feedback

Do you have any doubt or question? Please feel free to ask and provide any feedback!!

@janx @xxuejie we already talked about this once, not sure where or when. Which roadblocks do you see in implementing this change of requirements?

Love & Peace, Phroi

5 Likes

You can do this right now without any protocol additions:

  • Build your own script containing all the validation logic, on any outputs or the whole transaction as you wish. For clarity, we name this script as validating lock.
  • At the end of validating lock, instead of returning 0 as status code to denote execution success(in terms all the validation passes), the validating lock uses exec to call into a normal lock script, which does the signature validation work.
  • A slight planning is required between validating lock, and normal lock: both might require fields in the witness part to keep certain user inputs. One design can be like this: put signatures in the lock field of WitnessArgs structure in the first witness of the script group, just like what you would do when normal lock is used directly as a cell’s lock. Then use one witness whose index is bigger than the length of input cells(e.g., if the current transaction has 5 input cells, you can use the witness field at index 5), you can put the user input data required by validating lock in this particular witness.

This way you have a lock script that does additional validation, but is also compatible with almost any existing locks out there. To me it fulfills the purposes, though I cannot say the same thing about others. Of course some work might be required in wallets since a cell really uses different lock script, but that will be required in any case.

3 Likes

Hey @xxuejie, thank you for taking your time to reply :pray:

Indeed the biggest issue I see here is wallet adoption: I don’t have any power to ask wallets to support a new signature-based lock. If this could be handled trough CCC, maybe it could work, but it would still require all the non-CCC wallets to opt-in to support the lock…

On my side I solved the problem that this solution solves ages ago, just split the logic into two cells:

  • Cell 1: The lock doesn’t handle anymore signature based unlocking, just protocol logic & unlocking if Cell2 is also present as input.
  • Cell 2: A type into another cell is interconnected to the first lock, this other cell lock holds the real user lock, which can be wallets preferred lock.

The issue with this solution is that Cell 1 can be forged by an attacker, since output locks to not validate. This creates attack vectors that each instantiation of this proposed cell pattern has to prevent.

Your proposed design share the very same weakness: an attacker can create a tx with a non-validating lock as input and a validating lock as output, hence the forgery can occur.

The proposed change would make for stronger building blocks when crafting protocols.

Love & Peace, Phroi

3 Likes

I just realized that the problem here might be the proposal to defined quite vague: so it might be worthwhile to clarity this first: what does Allow some Lock Scripts to Validate Outputs really mean? Specifically, where are those lock scripts? Are they lock scripts from input cells or output cells.

At first I assume you were talking about lock scripts from input cells should also do transaction validation work apart from signature verification. But it does seem you are talking about a different thing.

I personally don’t think we will see lock scripts from output cells to be executed anytime soon. An output cell validation work should always be the job of a type script. Yes right now UDTs pretty much occupy type scripts exclusively, but I see no technical blockers to wrap an existing UDT in a new one, where the new one does additional validation work.

3 Likes

Some more notes on the wallet issue(which might or might not be relavant depending on the actual definition of this proposel): the more I think of it, the less I think of it as an issue. CKB’s flexibility enables 2 different categories of locks:

  • Managed lock by a particular service providers: manual work might certainly be required for locks from those wallet providers(or might be avoided if decent APIs are provided by wallet providers). However, given current state of CKB, it’s not just locks, any apps would require manual integrations. Hopefully a protocol can emerge in the future but we are stuck in this state here;
  • omnilock(or potentially ccc locks I believe) can be integrated with wallets from other chains, such as metamask or unisats. For those wallets, an app can complete the integration by itself, ckb.pw and nervdao are all examples of apps of this category.

So I definitely think an app can start with locks from the second category, and when interests arise, wallet providers from the first category might become more interested.

And this way really you can either add a new validating lock wrapper on an existing lock, or add a new UDT wrapper script on an existing UDT, all can be achieved within a single app, there is not need for persuading work.

2 Likes

See Confusion attack on Limit Order · Issue #19 · ickb/proposal · GitHub

3 Likes

Thank you @xxuejie and @janx for humoring this proposal!! I’m really glad to see you both active :hugs:

You are right, the title was confusing!! I changed it to Allow some Output Lock Scripts to Validate, feel free to provide additional feedback!

For reference, also @janx agree on this::

If we follow A and make lock scripts able to validate transaction outputs, then lock scripts are technically the same as type scripts, CKB cells would essentially have two “type” script slots behaving the same, despite difference on naming. The distinction between the two may be clear at the beginning, but as new use cases coming hackers would start to poking these two slots in unintended way, and because the distinction is merely a convention not a protocol rule, we’ll see similar “slot in use” problem again, e.g. app X uses “lock script” slot, app Y uses “type script” slot, app Z uses both “lock script” and “type script” slots. At that time some convention/standards are still required to avoid slot conflicts and achieve script composability between X/Z, Y/Z, and/or X/Y/Z.

The Weird Solution (that I Like, but would Not Use)

As I told @matt_ckb some time ago, you know what surprised me when I first started using Nervos L1?

  • Having exactly two script slots guarding each cell!
  • Why 2, not 1 nor 3?
  • It seems an artificial limit justified by an arbitrary convention!! :rofl:

In my eyes it could have been reasonable to:

  1. Allow any possible number of (type) script slots per cell (and no lock scripts).
  2. Allow any possible logical arrangement: while right now is an AND of two script slots, it would be reasonable to allow for a chain of OR and AND like expressions in programming languages.
  3. Allow each script to have its own dedicated data, clearly separated from the others.

Which is indeed strikingly similar to:

B.ii (weird but good for brain-storming) create a type/type script composing standard, e.g. abandon lock script completely, use type script for everything, every cell uses a ‘meta’ type script which can read cell data to determine what script/logic should be invoked and invoke them in turn.

It took a few hard-forks, but we are finally getting to this level of expressiveness :tada:

Good news is that spawn has an advantage: we have the possibility of moving all this data from the expensive cell data to the inexpensive witness and anchor this witness data with just an hash in cell data.

That said, it’s a pretty big leap from the current way of developing on Nervos L1, so it’s likely not viable, at least for now.

Create a Lock/Type Script Composing Standard

B.i create a lock/type script composing standard, e.g. is it possible for a lock script to have certain callback functions, and a standard-compliant type script will always spawn those callback functions if they exist in the lock scripts in related cells?

This idea is indeed more approachable and it could lead to a pretty good standard, but it has two main areas that need refinement before calling this a workable standard.

Community-Agreed Standard

If we were to live in a L1 world where all type scripts and lock scripts were already standard-compliant, we would be discussing the obvious as it would be the already widely adopted solution.

While this seems the easy part, there is no Community Agreed Standard and it will likely stay that way for a some time as:

  1. It takes time to agree on such a standard, even between parties who want to opt-in.
  2. It takes much more time to make the other L1 devs understand that they should conform to such standard.

Of course, once we have a basic standard, I can spearhead it by adopting it in my upcoming Warranty Contracts.

Backward Compatibility

When starting to adopt such a standard all pre-existing type scripts and lock scripts will be not compliant and there are already quite a bit of them. This is the second non-naive part of the solution.

Lock Script Compatibility

This seems a naive problem. Lock Scripts are either compatible or not compatible:

  • How to distinguish between compatible locks and non-compatible locks? (Naked spawn call or SSRI, just need to figure out the best practice :white_check_mark:)
  • Given a compatible script, how is this script able to identify its own code hash and reference type? (Just need to figure out the best practice :white_check_mark:)

Type Scripts Compatibility

This is the non-naive problem:

This seems reasonable and actually a pretty good solution, but still has issues:

  1. Who can join these W-UDTs? Are these Wrapped UDTs public? If yes, then the underlying cells are shared among W-UDT holders. Malicious W-UDT holders can DoS the underlying UDT cells. Similar to the Busiwork Attack on iCKB, but worse given the natural liquidity of the wrapped assets :warning:
  2. The very same initial LO confusion attack, just applied to the lock used on the underlying non-compliant UDT cells being wrapped. That said, it should be easier to tackle now, as at each unwrapping we know both token and amount.
  3. Many or one underlying UDT cells? Who is the owner of the CKB used in the state-rent of the underlying UDT cells? What happens to these CKB when the last W-UDT is redeemed?
  4. What’s the relation between W-UDTs and gated-access UDTs? (For example, stable-coins such as USDI and USDT)

These kind of problems need to be considered and solved at least for baby problems before we can call this a workable solution. Allowing some Output Lock Scripts to Validate may very well be Blasphemy, but it doesn’t bring these problems.

@xxuejie @janx feel free to suggest more details, so we can understand it better and possibly integrate it in future protocols implementations :pray:

Love & Peace, Phroi

4 Likes

Just to think out loud on W-UDTs: let’s assume we have a pair of scripts:

  • WUDT lock script
  • WUDT type script

(Actually it’s possible to use a single script both acting as lock & type but that is just one implementation detail, some might like it but some might not, ignore it for a second)

A WUDT lock script holds in its script args the script hash of a particular UDT type script(one particular UDT to wrap), and a pubkey hash identifying owners.

A WUDT type script holds in its script args the script hash of a particular WUDT lock script. The owner lock design is respected here.

As a result, each WUDT type script works on one type of UDT it can wrap.

To issue some WUDTs, the WUDT type script validates that same amounts UDTs are transferred into cells using WUDT lock script as the lock script, with matching script hash of the UDT type script.

To unlock a WUDT lock script(and to move the stored UDTs elsewhere), the WUDT lock script validates that the same amount of WUDTs have been burned.

This way each WUDT will have backing UDT(it does not matter how many WUDT cells there are, what matters is the underlying WUDT amount and UDT amount), and you can also build any validating lock on top of WUDT as you like, and the underlying UDT can use existing standards such as Simple UDTs. Compatibility is also preserved.

2 Likes

We got exactly the same understanding, also helps that this is exactly like the iCKB protocol, but applied to UDTs :grin:

Possible Solutions to the Proposed Problems

So let’s first solve the previously presented problem.

Busywork Attack

This protocol is so liquid that it can be exploited maliciously to DoS the protocol with state contention: wrap, unwrap, wrap, unwrap…

A simple solution would be to apply a minimal lockup period to the the conversion results, so the user cells resulting from wrapping or unwrapping are locked up for some time.

Alternatively, since OTX are still not available, we can assume that we can also develop an intention-based script to carry out wrappings and unwrappings. At that point user signs the intention cell and automated bots take care of getting his intention realized by signing transactions until one goes trough. (Depending on the design, the lock on these intention cells could face Confusion Attack.)

Are there any better alternatives?

Confusion Attack

In my opinion it doesn’t pose issues, cause the protocol know both type and amount of what it needs to unwrap. Say the front-end gets confused and include invalid attacker cells, the WUDT type script would just not validate at unwrapping time, cause it recognizes the unwrapping as invalid.

Ownership of CKB used in the Underlying UDTs

If we assume that everyone can claim the CKB used to store the Underlying UDTs, then we’ll always end up with exactly one cell holding all Underlying UDTs. Let’s assume for example that there are multiple cells storing the Underlying UDTs, then an attacker can merge those cells and keep for himself the difference of CKB.

The only solution that comes to mind is that the CKB used in the Underlying UDTs are exclusively property of the protocol, as such no-one is allowed to claim them.

This bring to the following rule: If N underlying UDT cells exist in input, then the output must have at least N underlying UDT cells. These Underlying UDTs cells can be drained to 0 UDT, but not consumed.

These N underlying output cells should be of similar values, this maximizes the utility of these cells in high concurrency environments, but enforcing this at the script level could become problematic later on when OTX becomes available(?).

Are there any better alternatives?

Wrapping Gated-access UDTs

This probably is not an issue: these UDTs are deployed by type, see USDI, so they can easily adopt this new standard, if so they choose.

Say they choose not to, then they get wrapped and that’s about it. They will handle the blocking of WUDT users in the same way they handle blocking of user funds inside protocol that use custom protocol locks.

WUDT Script design

Say we use sUDT as base then it gets complicated when we want to mint WUDT without an existing cell in input with our owner lock… This is the reason why iCKB design moved to xUDT.

I’d rather use xUDT, as xUDT with the appropriate args flag allows to have owner locks in form of a type script, both in input and output. So we can directly reference the wrapped UDT type in our custom xUDT args.

This is exactly what I did in iCKB, so let’s assume we go with this design.

In single script both acting as lock & type there is no strict need for any lock args:

  • Underlying UDT Lock shares the logic by being the same script as WUDT, so it gets a nice 0x args.
  • WUDT knows the Underlying UDT by its args.

This is @xxuejie stance on non-signature based locks, which gives context to the previous statement:

I personally question the necessity of non-signature based locks at all. If I were doing the design, I would use types solely for non-signature based locks.

I’m not so sure about adding the pubkey hash of owners:

  • Can you explain more your point of view?
  • Including the pubkey hash which issue does it solve here?
  • Also exactly owners of what?
  • In your vision of a WUDT lock how is this pubkey hash used?

In my eyes keeping the pubkey of the original minters in the Underlying UDTs would put this protocol much closer to dCKB and that’s not a working model: dCKB failed due to breaking the redeemability of dCKB for their Underlying NervosDAO deposits.

Conversely, we have the iCKB model, where no pubkey of minters is stored in the Underlying NervosDAO deposits and it is designed for optimal redeemability. In my opinion, given the iCKB model, a particular WUDT instance should have no owners: anyone should be able to join or leave a WUDT, even without being one of the original minters.

From @janx:

a standard-compliant type script will always spawn those callback functions if they exist in the lock scripts in related cells

What if we spawn all the output locks? Not only those in related to our cells. What’s your take?

At this point I’d like to agree on the right fix for the Busywork Attack, then we can move to design the spawn call details.

Love & Peace, Phroi

2 Likes