-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
multi: add new v2 version of GKR based on Pedersen commitments #1290
Conversation
Does Wlog = WLOG? |
internal/pedersen/commitment.go
Outdated
// WithCustomNUMs is a functional option that can be used to set a custom NUMs | ||
// point. | ||
func WithCustomNUMs(h btcec.PublicKey) commitOpt { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WRT NUMS style is traditional acronym style preferred of capitalizing every letter of the acronym?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think this should be WithCustomNUMS
. "Sleeve"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hadn't considered Pedersen commitments as a solution for our need before reviewing this PR. Really interesting subject to review/consider.
I think that we can re-use the existing cryptography functionality to achieve the same goal.
I think we can do:
nonSpendableKey := input.TaprootNUMSKey
if data != nil {
dataHash := chainhash.HashH(data)
tweakedNumsKey := input.TweakPubKeyWithTweak(
&input.TaprootNUMSKey, dataHash[:],
)
nonSpendableKey = *tweakedNumsKey
}
scriptBuilder := txscript.NewScriptBuilder()
scriptBuilder.AddData(schnorr.SerializePubKey(&nonSpendableKey))
scriptBuilder.AddOp(txscript.OP_CHECKSIG)
script, err := scriptBuilder.Script()
IMO we can re-use the existing function input.TweakPubKeyWithTweak
. The function is short, so I'll quote it here with extra comments to show that it does the same thing in terms of the Pedersen commitment formula P = m*G + r*H
.
func TweakPubKeyWithTweak(pubKey *btcec.PublicKey,
tweakBytes []byte) *btcec.PublicKey {
var (
pubKeyJacobian btcec.JacobianPoint
tweakJacobian btcec.JacobianPoint
resultJacobian btcec.JacobianPoint
)
// Form tweak scalar from tweak bytes. This is `m` in the equation
// `P = m*G + r*H`.
tweakKey, _ := btcec.PrivKeyFromBytes(tweakBytes)
// Compute the Jacobian point for the tweak. This is `m*G`.
// Crucially, the base point is the generator point G.
btcec.ScalarBaseMultNonConst(&tweakKey.Key, &tweakJacobian)
// At this point we can just assume `r` is 1, as we're only using `H` as
// the NUMS point. The public key here is the NUMS key.
pubKey.AsJacobian(&pubKeyJacobian)
// Perform the addition: m*G + r*H
// where r = 1 and H is the NUMS point.
btcec.AddNonConst(&pubKeyJacobian, &tweakJacobian, &resultJacobian)
// Return P.
resultJacobian.ToAffine()
return btcec.NewPublicKey(&resultJacobian.X, &resultJacobian.Y)
}
I think P
will remain non-spendable for the reason you've outlined in the PR body. Specifically, the NUMS generator point cannot be expressed as a linear combination of the standard generator point G
.
We already use input.TweakPubKeyWithTweak
in a few places, so re-using it would help keep our cryptography codebase concise and maintainable. Having a single point to test would benefit all callers that depend on it.
Since we won’t be utilizing the privacy feature of Pedersen commitments, I think we should reconsider using Pedersen commitments altogether. From what I understand after reading Non-Interactive and Information-Theoretic Secure Verifiable Secret Sharing, the privacy aspect seems to be a fundamental part of Pedersen commitments. Referring to Pedersen commitments while essentially creating a NUMS key commitment strikes me as unnecessarily complex.
EDIT: Here's a commit showing what I mean re input.TweakPubKeyWithTweak
: d6d7a71
It's certainly similar, but that function was created for a different purpose, namely for the randomized per commitment point for a LN commitment. The goal here with the
A cryptographic commitment can be hiding (no information about the opening revealed from the commitment). It can also be binding (not possbile to commit to one value and prevent an opening of a different value. Both those properties can rely on either a computational or information theoretic assumption. Pedersen commitments are computationally binding (reduces to the DH problem), and information theoretically (perfect) hiding. The mini library I created allows the commitment to be optionally non-hiding. In our case, we don't really need the hiding property as when the commitment is presented, we already know what the opening is (the asset ID).
Not sure what you mean, that's the very construction of a Pedersen commitment. It relies on a aux generator |
b00f5c5
to
f7978c2
Compare
Pull Request Test Coverage Report for Build 12814109365Details
💛 - Coveralls |
@Roasbeef What I mean is that a Pedersen commitment includes the separate component of the unknown discrete log, the blinding factor From my perspective, the only functional change introduced by this PR is the added support for But I do like that the Pedersen commitment library formalises the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we feed the non-spend leaf version from the interface to this point? Do we need to serialise it into the proof group key reveal?
Alternatively, instead of adding a non-spend leaf version, we could, during group key reveal verification just attempt to reconstruct the tapscript root using both non-spend leaf versions. We just want one of them to validate, doesn't matter which.
So the next release could have this functionality:
- Construct group keys with Pedersen commitment.
- Be able to verify group keys with either OP_RETURN or Pedersen commitment leaves. And just tries both verification methods without looking up any non-spend leaf version.
So what I've done here is just inherit the value from the enclosing struct during decoding: https://github.com/lightninglabs/taproot-assets/pull/1290/files#diff-98586afaee9ef4f1d46ba2b4af7ce1fd31d28bc83e6df2117e6483532221f8a7R1388-R1391. They're the exact same version, which is only really used when we need to make the unspendable leaf. |
f7978c2
to
e1b919b
Compare
e1b919b
to
8f212f0
Compare
I've pushed up some fixes and additional commits for what we need for the hardware wallet support.
I think the way the version is encoded now I prefer the more explicit one. And since we already encoded the version anyway, we don't store more data in the proof. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Provided my review in form of direct patches.
8f212f0
to
2389629
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
I think we should change NUMs
to NUMS
throughout.
internal/pedersen/commitment.go
Outdated
// WithCustomNUMs is a functional option that can be used to set a custom NUMs | ||
// point. | ||
func WithCustomNUMs(h btcec.PublicKey) commitOpt { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think this should be WithCustomNUMS
. "Sleeve"
# 2025/01/10 12:23:16.878522 [TestPedersenCommitmentProperties] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} | ||
# 2025/01/10 12:23:16.878526 [TestPedersenCommitmentProperties] [rapid] draw bytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.fail
files don't need to be committed? or do they?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not 100% sure, but this commit makes me think it behaves the same way as fuzz corporae, that if files like that exist they act as a regression test case: flyingmutant/rapid@4db076b
var msg [sha256.Size]byte | ||
copy(msg[:], data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this copy is a bit risky. If the caller passes in data
which is len(data) > sha256.Size
then it will be truncated rather than included in the leaf in totality.
I think it would be best to just change data
in the func sig to:
func NewNonSpendableScriptLeaf(version NonSpendLeafVersion,
data [sha256.Size]byte) (txscript.TapLeaf, error)
That's all we need for now anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a check to the last commit.
@@ -1224,24 +1252,36 @@ func NewGKRCustomSubtreeRootRecord(root *chainhash.Hash) tlv.Record { | |||
// | |||
// [tapscript_root] | |||
// / \ | |||
// [OP_RETURN <genesis asset ID>] [tweaked_custom_branch] | |||
// [non_spend(<genesis asset ID>)] [tweaked_custom_branch] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
references to [OP_RETURN]
above also need to be updated:
// sibling is a non-spendable script leaf containing `[OP_RETURN]`. Its
// If `custom_root_hash` is not provided, it defaults to a bare `[OP_RETURN]` as
maybe others
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also is the tree struct just the genesis asset ID node now for HW signing without custom spend subtree?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the references to [OP_RETURN]
.
Also is the tree struct just the genesis asset ID node now for HW signing without custom spend subtree?
If there are no custom scripts, then yes, there's only a single leaf with the non_spend(<genesis asset ID>)
script. Updated some references in the comments.
// | ||
// NOTE: This field should not be serialized as part of the group key | ||
// reveal. It is included here to ensure it is constructed concurrently | ||
// with the tapscript root, maintaining consistency and minimizing | ||
// errors. | ||
customSubtreeInclusionProof []byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Computing this on demand means that it could theoretically not correspond correctly to what's committed to in customSubtreeRoot
. That's why customSubtreeInclusionProof
was previously stored and was constructed using the same components which also constructed customSubtreeRoot
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But isn't this exactly what the if !g.root.IsEqual(&tapscript.root) {
check is for?
In this commit, we add a new internal package that can be used to create and verify Pedersen commitments. We aim to make the API very simple to use, while providing configuration that's useful for tests and a general set of use cases. We also add property based tests to achieve 100% test coverage of all relevant invariants.
In this commit, we add a new version of the new more flexible GKR type. This version uses an `OP_CHECKSIG` with an un-spendable public key. We derive such a public key by using the Pedersen commitment of the asset ID, or blank message (for the dummy leaf case). As a Pedersen commitment uses an aux generator H, that no one knows the private key to, it's impossible to generate a signature using the commitment point. An informal sketch of a proof by contradiction goes something like: * Starting with a Pedersen commitment, P = m*G + r*H. * Let's assume that we can actually sign with P. This means that there's some x, where P = x*G that we know. * If we re-write x in terms of our commitment, we have: P = m*G + r*H. * Wlog, we'll simplify by removing `r`, the blinding factor, so: P = m*G + H. * In terms of private values we have x*G = m*G + H * If we solve for H, then we have H = (x-m)*G * If this is a Pedersen commitment, then the private key of H must be unknown to all parties. * If we can sign with P, then this means that we know the value (x-m). * However, since this is a Pedersen commitment, it's impossible to know the value `x`, which is a function of the private value H of the pedersen commitment (h*H = (x-m)*G. This is a contradiction, as we see the that if we know x-m, then we also know h.
This asserts that both variants of the leaf we generate are actually nonspendable.
To achieve hardware wallet signing support, we remove any siblings from the tree if there are no custom scripts present. This allows us to use the Pedersen commitment to create a public key miniscript policy which is compatible with certain hardware wallets.
This commit removes a field from the group key reveal V1 that wasn't serialized and calculated on demand if necessary anyway. Re-computing it when it is needed is a smaller cost than the added complexity of having a non-serialized field.
Hardware wallets that support miniscript policies only accept a pk() fragment that specifies an extended key, for example with: pk(@1/<0;1>/*). When signing the transaction the actual derivation path to use (for example 0/0) is defined in the PSBT itself. So the signing policy is generic and the user only has to accept it once. A child key derived from an extended NUMS key is still a NUMS key, as it's just tweaked again. The initial private key is still unknown.
2389629
to
9733b3d
Compare
Really cool stuff, especially the mask being 1. I have no remarks on the code. I do have some problems with the proof sketch.
I find this part of the sketch confusing. The assumption that knowing how to sign with The contradiction arises not from knowing Maybe I'm being too academic. It does not have any implications for the code, which looks sound to me. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
In this PR, we add a new internal package that can be used to create
and verify Pedersen commitments. We aim to make the API very simple to
use, while providing configuration that's useful for tests and a general
set of use cases.
We also add property based tests to achieve 100% test coverage of all
relevant invariants.
We then this that to add a new version of the new more flexible GKR type.
This version uses an
OP_CHECKSIG
with an un-spendable public key. Wederive such a public key by using the Pedersen commitment of the asset
ID, or blank message (for the dummy leaf case). As a Pedersen commitment
uses an aux generator H, that no one knows the private key to, it's
impossible to generate a signature using the commitment point.
An informal sketch of a proof by contradiction goes something like:
P = m*G + r*H
.P
. This means thatthere's some
x
, whereP = x*G
that we know.x
in terms of our commitment, we have:P = m*G + r*H
.r
, the blinding factor, so:P = m*G + H
.x*G = m*G + H
H
, then we haveH = (x-m)*G
H
must beunknown to all parties.
P
, then this means that we know the value(x-m)
.know the value
x
, which is a function of the private valueH
ofthe Pedersen commitment (
P= (x-m)*G
). This is a contradiction, aswe see the that if we know
x-m
, then we also knowh
.