├── README.md ├── hacspec-halfagg ├── Cargo.toml ├── src │ └── halfagg.rs └── tests │ └── tests.rs ├── half-agg-and-adaptor-sigs.md ├── half-aggregation.mediawiki ├── savings.org └── slides └── 2021-Q2-halfagg-impl.org /README.md: -------------------------------------------------------------------------------- 1 | # Cross-Input Signature Aggregation (CISA) 2 | 3 | CISA is a potential Bitcoin softfork that reduces transaction weight. The purpose of this repository is to collect thoughts and resources on signature aggregation schemes themselves and how they could be integrated into Bitcoin. 4 | 5 | ## Contents 6 | 7 | - [Half Aggregation](#half-aggregation) 8 | - [Sigagg Case Study: LN Channel Announcements](#sigagg-case-study-ln-channel-announcements) 9 | - [Integration Into The Bitcoin Protocol](#integration-into-the-bitcoin-protocol) 10 | - [Cross-input-aggregation savings](#cross-input-aggregation-savings) 11 | - [Half Aggregation And Mempool Caching](#half-aggregation-and-mempool-caching) 12 | - [Half Aggregation And Reorgs](#half-aggregation-and-reorgs) 13 | - [Half Aggregation And Adaptor Signatures](#half-aggregation-and-adaptor-signatures) 14 | 15 | ## Half Aggregation 16 | 17 | Half aggregation allows non-interactively aggregating a set of signatures into a single aggregate signature whose size is half of the size of the original signatures. 18 | 19 | See [half-aggregation.mediawiki](half-aggregation.mediawiki) for a detailed description. 20 | There is also a [recording of Implementing Half Aggregation in libsecp256k1-zkp](https://www.youtube.com/watch?v=Dns_9jaNPNk) with accompanying ["slides"](slides/2021-Q2-halfagg-impl.org). 21 | 22 | ## Sigagg Case Study: LN Channel Announcements 23 | 24 | [Channel announcements messages](https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_announcement-message) are gossiped in the Lightning Network to allow nodes to discover routes for payments. 25 | 26 | To prove that a channel between a node with public key `node_1` and a node with public key `node_2` does exist, the announcement contains four signatures. 27 | First, the announcement contains `node_signature_1` and `node_signature_2` which are are signatures over the channel announcement message by `node_1` and `node_2` respectively. 28 | 29 | The channel announcement also proves that the two keys `bitcoin_key_1` and `bitcoin_key_2` contained in the funding output of the funding transaction are owned by `node_1` and `node_2` respectively. 30 | Therefore, it contains signature `bitcoin_signature_1` by `bitcoin_key_1` over `node_1` and `bitcoin_signature_2` by `bitcoin_key_2` over `node_2`. 31 | 32 | 1. Since `node_signature_1` and `node_signature_2` are signatures over the same message, one can use a scheme like [MuSig2](https://eprint.iacr.org/2020/1261.pdf) to replace both signatures with a single multisignature `node_signature` that has the same size as an individual signature. 33 | 2. In order to create a channel announcement message, both nodes need to cooperate. 34 | Therefore, they can interactively fully aggregate the three signatures into a single aggregate signature. 35 | 3. Channel announcements are often sent in batches. 36 | Within a batch, the signatures of all channel announcements can be non-interactively half aggregated since this does not require the communication with the nodes. 37 | Each channel announcement signature is thus reduced to a half-aggregated signature which is half the size of the original signature. 38 | 39 | As a result, starting from four signatures (256 bytes) which make up about 60% of a channel announcement today are aggregated into one half signature (32 bytes for a large batch). 40 | 41 | Of course, variations of above recipe are possible. 42 | For example, if one wants to avoid full aggregation for simplicity's sake, the four signatures in an announcement can just be half aggregated to reduce them to the size of 2.5 signatures. 43 | 44 | ## Integration Into The Bitcoin Protocol 45 | 46 | Since the verification algorithm for half and fully aggregated signatures differs from BIP 340 Schnorr Signature verification, nodes can not simply start to produce and verify aggregated signatures. 47 | This would result in a chain split. 48 | 49 | Taproot & Tapscript provide multiple upgrade paths: 50 | - **Redefine `OP_SUCCESS` to `OP_CHECKAGGSIG`:** 51 | As pointed out in [this post to the bitcoin-dev mailing list](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-March/015838.html), an `OP_CHECKAGGSIG` appearing within a script that includes `OP_SUCCESS` can result in a chain split. 52 | That's because `OP_CHECKAGGSIG` does not actually verify the signature, but puts the public key in some datastructure against which the aggregate signature is only verified in the end - after having encountered all `OP_CHECKAGGSIG`. 53 | While one node sees `OP_SUCCESS OP_CHECKSIGADD`, a node with another upgrade - supposedly a softfork - may see `OP_DOPE OP_CHECKSIGADD`. 54 | Since they disagree how to verify the aggregate signature, they will disagree on the verification result which results in a chainsplit. 55 | Hence, `OP_CHECKAGGSIG` can't be used in a scripting system with `OP_SUCCESS`. 56 | The same argument holds for the attempt to add aggregate signatures via Tapscript's key version. 57 | - **Define new leaf version to replace tapscript:** If the new scripting system has `OP_SUCCESS` then this does not solve the problem. 58 | - **Define new SegWit version:** 59 | It is possible to define a new SegWit version that is a copy of Taproot & Tapscript with the exception that all keyspend signatures are allowed to be aggregated. 60 | On the other hand, if the goal is that aggregation is _only_ possible for keyspends, then defining a new SegWit version is necessary (since Taproot keyspends expects a BIP-340 signature and not some aggregated signature). 61 | However, keyspends can not be aggregated across SegWit versions: 62 | nodes that do not recognize the newest SegWit version are not able to pick up the public keys and messages that the aggregate signature is supposed to be verified with. 63 | 64 | Assume that a new SegWit version is defined to deploy aggregate signatures by copying Taproot and Tapscript and allowing only keyspends to be aggregated. 65 | This would be limiting. 66 | For example, a spending policy `(pk(A) and pk(B)) or (pk(A) and older(N))` would usually be instantiated in Taproot by aggregating keys A and B to create a keypath spend and appending a script path for `(pk(A) and older(N))`. 67 | It wouldn't be possible to aggregate the signature if the second spending path is used. 68 | 69 | This [bitcoin-dev post](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-July/016249.html) shows that this limitation is indeed unnecessary by introducing Generalized Taproot, a.k.a. g'root (see also [this post](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-October/016461.html) for a summary). 70 | Essentially, instead of requiring that each leaf of the taproot merkle tree is a script, in g'root leafs can consist of both a public key and a script. 71 | In order to use such a spending path, a signature for the public key must be provided, as well as the inputs to satisfy the script. 72 | This means that the public key is moved out of the scripting system, leaving it unencumbered by `OP_SUCCESS` and other potentially dangerous Tapscript components. 73 | Hence, signatures for these public keys _can_ be aggregated. 74 | 75 | Consider the example policy `(pk(A) and pk(B)) or (pk(A) and older(N))` from above. 76 | In g'root the root key `keyagg((pk(A), pk(B)))` commits via taproot tweaking to a spending condition consisting of public key `pk(A)` and script `older(N)`. 77 | In order to spend with the latter path, the script must be satisfied and an _aggregated_ signature for `pk(A)` must exist. 78 | 79 | The [Entroot](https://gist.github.com/sipa/ca1502f8465d0d5032d9dd2465f32603) proposal is a slightly improved version of g'root that integrates Graftroot. 80 | One of the main appeals is that Entroot is "remarkably elegant" because the validation rules of Entroot are rather simple for the capabilities it enables. 81 | 82 | 83 | ### Cross-input Aggregation Savings 84 | 85 | See [savings.org](savings.org). 86 | 87 | ### Half Aggregation And Mempool Caching 88 | 89 | As mentioned [on bitcoin-dev](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014308.html) nodes accepting a transaction with a half aggregate signature `(s, R_1, ..., R_n)` to their mempool would not throw it away or aggregate it with other signatures. 90 | Instead, they keep the signature and when a block with block-wide aggregate signature `(s', R'_1, ..., R'_n')` arrives they can subtract `s` from `s'` and remove `R_1, ..., R_n`, from the block-wide aggregate signature before verifying it. 91 | As a result, the nodes skip what they have already verified. 92 | 93 | ### Half Aggregation And Reorgs 94 | 95 | Assume there is a transaction `X` with half aggregate signature `S0 = (s0, R_1, ..., R_n)`. 96 | The transaction is contained in chain `C1` and therefore there exists a block with a signature `S1` that half aggregates all signatures in the block. 97 | Since `s0` is aggregated into `S1`, it is not retrievable from the block. 98 | 99 | Now there happens to be a reorganization from chain `C1` to chain `C2`. 100 | There are the following two cases where half aggregation affects the reorganization. 101 | 102 | 1. Transaction `X` is contained in both chain `C1` and `C2`. 103 | Let `S2` be the block-wide half aggregate signature of the block in `C2` that conatains `X`. 104 | In general `S1 != S2`, so the whole half-aggregate signature `S2` must be verified, including the contribution of `X` despite having it verified already. 105 | If `s0` was kept, it could be subtracted from `S2`. 106 | This is in contrast to ordinary signatures, which do not have to be re-verified in a reorg. 107 | 2. Transaction `X` is contained in `C1` but not in `C2`. 108 | Because we can't recover `s0`, we can't broadcast transaction `X`, nor can we build a block that includes it. 109 | Hence, we can't meaningfully put `X` back into the mempool. 110 | 111 | Both cases would indicate that it is beneficial to keep `s0` even though the transaction is included in the best chain. 112 | Only when the transaction is buried so deep that reorgs can be ruled out, the value `s0` can be discarded. 113 | 114 | Another solution for case 2. is to have the participants of the transaction (such as sender and receiver) rebroadcast the transaction. 115 | But this may have privacy issues. 116 | 117 | ### Half Aggregation And Adaptor Signatures 118 | 119 | Half aggregation prevents using adaptor signatures ([stackexchange](https://bitcoin.stackexchange.com/questions/107196/why-does-blockwide-signature-aggregation-prevent-adaptor-signatures)). 120 | However, a new SegWit version as outlined in section [Integration Into The Bitcoin Protocol](#integration-into-the-bitcoin-protocol) would keep signatures inside Tapscript unaggregatable. 121 | Hence, protocols using adaptor signatures can be instantiated by having adaptor signatures only appear inside Tapscript. 122 | 123 | This should not be any less efficient in g'root if the output can be spend directly with a script, i.e., without showing a merkle proof. 124 | However, since this is not a normal keypath spend and explicitly unaggregatable, such a spend will stick out from other transactions. 125 | It is an open question if this actually affects protocols built on adaptor signatures. 126 | In other words, can such protocols can be instantiated with a Tapscript spending path for the adaptor signature but without having to use actually use that path - at least in the cooperative case? 127 | See [half-agg-and-adaptor-sigs.md](half-agg-and-adaptor-sigs.md) for more discussion on this subject. 128 | -------------------------------------------------------------------------------- /hacspec-halfagg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hacspec-halfagg" 3 | version = "0.1.0" 4 | authors = ["Jonas Nick "] 5 | edition = "2018" 6 | description = "A specification for half-aggregation" 7 | 8 | [lib] 9 | path = "src/halfagg.rs" 10 | 11 | [dependencies] 12 | # TODO: pin version 13 | hacspec-lib = { git = "https://github.com/jonasnick/hacspec/", branch = "bip-340" } 14 | hacspec-bip-340 = { git = "https://github.com/jonasnick/hacspec/", branch = "bip-340" } 15 | 16 | 17 | [dev-dependencies] 18 | quickcheck = "1" 19 | quickcheck_macros = "1" 20 | serde_json = "1.0" 21 | serde = {version = "1.0", features = ["derive"]} 22 | hacspec-dev = { git = "https://github.com/jonasnick/hacspec/", branch = "bip-340" } -------------------------------------------------------------------------------- /hacspec-halfagg/src/halfagg.rs: -------------------------------------------------------------------------------- 1 | //! WARNING: This specification is EXPERIMENTAL and has _not_ received adequate 2 | //! security review. 3 | 4 | use hacspec_bip_340::*; 5 | use hacspec_lib::*; 6 | 7 | #[derive(Debug, PartialEq)] 8 | pub enum Error { 9 | InvalidPublicKey(usize), 10 | InvalidSignature, 11 | AggSigTooBig, 12 | MalformedSignature, 13 | } 14 | 15 | pub type AggSig = ByteSeq; 16 | 17 | public_bytes!(TaggedHashHalfAggPrefix, 18); 18 | // string "HalfAgg/randomizer" 19 | const HALFAGG_RANDOMIZER: TaggedHashHalfAggPrefix = TaggedHashHalfAggPrefix([ 20 | 0x48u8, 0x61u8, 0x6cu8, 0x66u8, 0x41u8, 0x67u8, 0x67u8, 0x2fu8, 0x72u8, 0x61u8, 0x6eu8, 0x64u8, 21 | 0x6fu8, 0x6du8, 0x69u8, 0x7au8, 0x65u8, 0x72u8, 22 | ]); 23 | 24 | pub fn hash_halfagg(input: &Seq<(PublicKey, Message, Bytes32)>) -> Bytes32 { 25 | let mut c = ByteSeq::new(0); 26 | for i in 0..input.len() { 27 | let (pk, msg, rx) = input[i]; 28 | c = c.concat(&rx).concat(&pk).concat(&msg); 29 | } 30 | tagged_hash(&PublicByteSeq::from_seq(&HALFAGG_RANDOMIZER), &c) 31 | } 32 | 33 | pub fn randomizer(pmr: &Seq<(PublicKey, Message, Bytes32)>, index: usize) -> Scalar { 34 | if index == 0 { 35 | Scalar::ONE() 36 | } else { 37 | // TODO: The following line hashes i elements and therefore leads to 38 | // quadratic runtime. Instead, we should cache the intermediate result 39 | // and only hash the new element. 40 | scalar_from_bytes(hash_halfagg( 41 | &Seq::<(PublicKey, Message, Bytes32)>::from_slice(pmr, 0, index + 1), 42 | )) 43 | } 44 | } 45 | 46 | pub type AggregateResult = Result; 47 | pub fn aggregate(pms: &Seq<(PublicKey, Message, Signature)>) -> AggregateResult { 48 | let aggsig = AggSig::new(32); 49 | inc_aggregate(&aggsig, &Seq::<(PublicKey, Message)>::new(0), pms) 50 | } 51 | 52 | pub fn inc_aggregate( 53 | aggsig: &AggSig, 54 | pm_aggd: &Seq<(PublicKey, Message)>, 55 | pms_to_agg: &Seq<(PublicKey, Message, Signature)>, 56 | ) -> AggregateResult { 57 | let (sum, overflow) = pm_aggd.len().overflowing_add(pms_to_agg.len()); 58 | if overflow || sum > 0xffff { 59 | AggregateResult::Err(Error::AggSigTooBig)?; 60 | } 61 | if aggsig.len() != 32 * (pm_aggd.len() + 1) { 62 | AggregateResult::Err(Error::MalformedSignature)?; 63 | } 64 | let v = aggsig.len() / 32 - 1; 65 | let u = pms_to_agg.len(); 66 | let mut pmr = Seq::<(PublicKey, Message, Bytes32)>::new(v + u); 67 | for i in 0..v { 68 | let (pk, msg) = pm_aggd[i]; 69 | pmr[i] = (pk, msg, Bytes32::from_slice(aggsig, 32 * i, 32)); 70 | } 71 | let mut s = Scalar::from_byte_seq_be(&aggsig.slice(32 * v, 32)); 72 | 73 | for i in v..v + u { 74 | let (pk, msg, sig) = pms_to_agg[i - v]; 75 | pmr[i] = (pk, msg, Bytes32::from_slice(&sig, 0, 32)); 76 | let z = randomizer(&pmr, i); 77 | s = s + z * Scalar::from_byte_seq_be(&Bytes32::from_slice(&sig, 32, 32)); 78 | } 79 | let mut ret = Seq::::new(0); 80 | for i in 0..pmr.len() { 81 | let (_, _, rx) = pmr[i]; 82 | ret = ret.concat(&rx) 83 | } 84 | ret = ret.concat(&bytes_from_scalar(s)); 85 | AggregateResult::Ok(ret) 86 | } 87 | 88 | fn point_multi_mul(b: Scalar, terms: &Seq<(Scalar, AffinePoint)>) -> Point { 89 | let mut acc = point_mul_base(b); 90 | for i in 0..terms.len() { 91 | let (s, p) = terms[i]; 92 | acc = point_add(acc, point_mul(s, Point::Affine(p))); 93 | } 94 | acc 95 | } 96 | 97 | pub type VerifyResult = Result<(), Error>; 98 | pub fn verify_aggregate(aggsig: &AggSig, pm_aggd: &Seq<(PublicKey, Message)>) -> VerifyResult { 99 | if pm_aggd.len() > 0xffff { 100 | VerifyResult::Err(Error::AggSigTooBig)?; 101 | } 102 | if aggsig.len() != 32 * (pm_aggd.len() + 1) { 103 | VerifyResult::Err(Error::InvalidSignature)?; 104 | } 105 | let u = pm_aggd.len(); 106 | let mut terms = Seq::<(Scalar, AffinePoint)>::new(2 * u); 107 | let mut pmr = Seq::<(PublicKey, Message, Bytes32)>::new(u); 108 | for i in 0..u { 109 | let (pk, msg) = pm_aggd[i]; 110 | let px = fieldelem_from_bytes(pk).ok_or(Error::InvalidPublicKey(i))?; 111 | let p_res = lift_x(px); 112 | if p_res.is_err() { 113 | VerifyResult::Err(Error::InvalidPublicKey(i))?; 114 | } 115 | let p = p_res.unwrap(); 116 | let rx = Bytes32::from_slice(aggsig, 32 * i, 32); 117 | let rx_f = fieldelem_from_bytes(rx).ok_or(Error::InvalidSignature)?; 118 | let r_res = lift_x(rx_f); 119 | if r_res.is_err() { 120 | VerifyResult::Err(Error::InvalidSignature)?; 121 | } 122 | let r = r_res.unwrap(); 123 | let e = scalar_from_bytes(hash_challenge(rx, pk, msg)); 124 | pmr[i] = (pk, msg, rx); 125 | let z = randomizer(&pmr, i); 126 | terms[2 * i] = (z, r); 127 | terms[2 * i + 1] = (z * e, p); 128 | } 129 | let s = scalar_from_bytes_strict(Bytes32::from_seq(&aggsig.slice(32 * u, 32))) 130 | .ok_or(Error::InvalidSignature)?; 131 | match point_multi_mul(Scalar::ZERO() - s, &terms) { 132 | Point::Affine(_) => VerifyResult::Err(Error::InvalidSignature), 133 | Point::AtInfinity => VerifyResult::Ok(()), 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /hacspec-halfagg/tests/tests.rs: -------------------------------------------------------------------------------- 1 | use hacspec_bip_340::*; 2 | use hacspec_dev::rand::*; 3 | use hacspec_halfagg::*; 4 | use hacspec_lib::*; 5 | 6 | fn strip_sigs(pms_triple: &Seq<(PublicKey, Message, Signature)>) -> Seq<(PublicKey, Message)> { 7 | let pm_tuple = Seq::<(PublicKey, Message)>::from_vec( 8 | pms_triple 9 | .native_slice() 10 | .to_vec() 11 | .iter() 12 | .map(|&(x, y, _)| (x, y)) 13 | .collect::>(), 14 | ); 15 | pm_tuple 16 | } 17 | 18 | #[allow(dead_code)] 19 | fn test_verify_vectors_gen() -> Vec<(Seq<(PublicKey, Message)>, AggSig)> { 20 | let skm = vec![ 21 | ( 22 | SecretKey::from_public_array([1; 32]), 23 | Message::from_public_array([2; 32]), 24 | AuxRand::from_public_array([3; 32]), 25 | ), 26 | ( 27 | SecretKey::from_public_array([4; 32]), 28 | Message::from_public_array([5; 32]), 29 | AuxRand::from_public_array([6; 32]), 30 | ), 31 | ]; 32 | let vectors_input = vec![vec![], vec![skm[0]], vec![skm[0], skm[1]]]; 33 | 34 | let mut vectors = vec![]; 35 | for v_in in vectors_input { 36 | let mut pms = Seq::<(PublicKey, Message, Signature)>::new(0); 37 | for skm in v_in { 38 | let sk = skm.0; 39 | let pk = pubkey_gen(sk).unwrap(); 40 | let sig = sign(skm.1, sk, skm.2).unwrap(); 41 | pms = pms.push(&(pk, skm.1, sig)); 42 | } 43 | let aggsig = aggregate(&pms).unwrap(); 44 | vectors.push((strip_sigs(&pms), aggsig)); 45 | } 46 | vectors 47 | } 48 | 49 | #[allow(dead_code)] 50 | fn test_verify_vectors_print(vectors: &Vec<(Seq<(PublicKey, Message)>, AggSig)>) { 51 | println!("let vectors_raw = vec!["); 52 | for v in vectors { 53 | let s: String = 54 | v.0.iter() 55 | .map(|(pk, m)| format!("(\"{}\", \"{}\"),", pk.to_hex(), m.to_hex())) 56 | .collect(); 57 | println!("(vec![{}], \"{}\"),", s, v.1.to_hex()); 58 | } 59 | println!("];"); 60 | } 61 | 62 | fn test_verify_vectors_process( 63 | vectors: &Vec<(Vec<(&str, &str)>, &str)>, 64 | ) -> Vec<(Seq<(PublicKey, Message)>, AggSig)> { 65 | let mut processed_vectors = vec![]; 66 | for v in vectors { 67 | let pm = Seq::from_vec( 68 | v.0.iter() 69 | .map(|(pk, m)| (PublicKey::from_hex(&pk), Message::from_hex(&m))) 70 | .collect(), 71 | ); 72 | let aggsig = AggSig::from_hex(v.1); 73 | processed_vectors.push((pm, aggsig)); 74 | } 75 | processed_vectors 76 | } 77 | 78 | #[test] 79 | fn test_verify_vectors() { 80 | #[rustfmt::skip] 81 | let vectors_raw = vec![ 82 | (vec![], "0000000000000000000000000000000000000000000000000000000000000000"), 83 | (vec![("1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", "0202020202020202020202020202020202020202020202020202020202020202"),], "b070aafcea439a4f6f1bbfc2eb66d29d24b0cab74d6b745c3cfb009cc8fe4aa80e066c34819936549ff49b6fd4d41edfc401a367b87ddd59fee38177961c225f"), 84 | (vec![("1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", "0202020202020202020202020202020202020202020202020202020202020202"),("462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b", "0505050505050505050505050505050505050505050505050505050505050505"),], "b070aafcea439a4f6f1bbfc2eb66d29d24b0cab74d6b745c3cfb009cc8fe4aa8a3afbdb45a6a34bf7c8c00f1b6d7e7d375b54540f13716c87b62e51e2f4f22ffbf8913ec53226a34892d60252a7052614ca79ae939986828d81d2311957371ad"), 85 | ]; 86 | let vectors = test_verify_vectors_process(&vectors_raw); 87 | // Uncomment to generate and print test vectors: 88 | // let vectors_expected = test_verify_vectors_gen(); 89 | // test_verify_vectors_print(&vectors_expected); 90 | for i in 0..vectors.len() { 91 | let aggsig = &vectors[i].1; 92 | let pm = &vectors[i].0; 93 | assert!(verify_aggregate(aggsig, pm).is_ok()) 94 | } 95 | } 96 | 97 | #[test] 98 | fn test_aggregate_verify() { 99 | let mut pms_triples = Seq::<(PublicKey, Message, Signature)>::new(0); 100 | let mut aggsigs = Seq::new(0); 101 | for i in 0..3usize { 102 | let sk = [i as u8 + 1; 32]; 103 | let sk = SecretKey::from_public_array(sk); 104 | let msg = [i as u8 + 2; 32]; 105 | let msg = Message::from_public_array(msg); 106 | let aux_rand = [i as u8 + 3; 32]; 107 | let aux_rand = AuxRand::from_public_array(aux_rand); 108 | let sig = sign(msg, sk, aux_rand).unwrap(); 109 | pms_triples = pms_triples.push(&(pubkey_gen(sk).unwrap(), msg, sig)); 110 | let aggsig = aggregate(&pms_triples).unwrap(); 111 | aggsigs = aggsigs.push(&aggsig); 112 | let pm_tuples = strip_sigs(&pms_triples); 113 | assert!(verify_aggregate(&aggsig, &pm_tuples).is_ok()); 114 | for j in 0..i { 115 | // Incrementally aggregate aggsig[j] (which has j+1) signatures, and 116 | // the remaining i - j pms_triples (pms_triples.len() = i + 1 = (j + 117 | // 1) + (i - j)). 118 | let aggsig = inc_aggregate( 119 | &aggsigs[j], 120 | &Seq::from_slice(&pm_tuples, 0, j + 1), 121 | &Seq::from_slice(&pms_triples, j + 1, i - j), 122 | ) 123 | .unwrap(); 124 | assert!(verify_aggregate(&aggsig, &pm_tuples).is_ok()); 125 | } 126 | } 127 | } 128 | 129 | /// Constructs two invalid signatures whose aggregate signature is valid 130 | #[test] 131 | fn test_aggregate_verify_strange() { 132 | let mut pms_triples = Seq::<(PublicKey, Message, Signature)>::new(0); 133 | for i in 0..2 { 134 | let sk = [i as u8 + 1; 32]; 135 | let sk = SecretKey::from_public_array(sk); 136 | let msg = [i as u8 + 2; 32]; 137 | let msg = Message::from_public_array(msg); 138 | let aux_rand = [i as u8 + 3; 32]; 139 | let aux_rand = AuxRand::from_public_array(aux_rand); 140 | let sig = sign(msg, sk, aux_rand).unwrap(); 141 | pms_triples = pms_triples.push(&(pubkey_gen(sk).unwrap(), msg, sig)); 142 | } 143 | let aggsig = aggregate(&pms_triples).unwrap(); 144 | let pm_tuples = strip_sigs(&pms_triples); 145 | assert!(verify_aggregate(&aggsig, &pm_tuples).is_ok()); 146 | 147 | // Compute z values like in IncAggegrate 148 | let mut pmr = Seq::<(PublicKey, Message, Bytes32)>::new(0); 149 | let mut z = Seq::new(0); 150 | for i in 0..2 { 151 | let (pk, msg, sig) = pms_triples[i]; 152 | pmr = pmr.push(&(pk, msg, Bytes32::from_slice(&sig, 0, 32))); 153 | z = z.push(&randomizer(&pmr, i)); 154 | } 155 | 156 | // Shift signatures appropriately 157 | let sagg = scalar_from_bytes(Bytes32::from_seq(&aggsig.slice(32 * 2, 32))); 158 | let s1: [u8; 32] = random_bytes(); 159 | let s1 = scalar_from_bytes(Bytes32::from_public_array(s1)); 160 | // Division is ordinary integer division, so use inv() explicitly 161 | let s0 = (sagg - z[1] * s1) * (z[0] as Scalar).inv(); 162 | 163 | let (pk0, msg0, sig0): (PublicKey, Message, Signature) = pms_triples[0]; 164 | let (pk1, msg1, sig1): (PublicKey, Message, Signature) = pms_triples[1]; 165 | let sig0_invalid = sig0.update(32, &bytes_from_scalar(s0)); 166 | let sig1_invalid = sig1.update(32, &bytes_from_scalar(s1)); 167 | assert!(!verify(msg0, pk0, sig0_invalid).is_ok()); 168 | assert!(!verify(msg1, pk1, sig1_invalid).is_ok()); 169 | 170 | let mut pms_strange = Seq::<(PublicKey, Message, Signature)>::new(0); 171 | pms_strange = pms_strange.push(&(pk0, msg0, sig0_invalid)); 172 | pms_strange = pms_strange.push(&(pk1, msg1, sig1_invalid)); 173 | let aggsig_strange = aggregate(&pms_strange).unwrap(); 174 | let pm_strange = strip_sigs(&pms_strange); 175 | assert!(verify_aggregate(&aggsig_strange, &pm_strange).is_ok()); 176 | } 177 | 178 | #[test] 179 | fn test_edge_cases() { 180 | let empty_pm = Seq::<(PublicKey, Message)>::new(0); 181 | let empty_pms = Seq::<(PublicKey, Message, Signature)>::new(0); 182 | let aggsig = aggregate(&empty_pms).unwrap(); 183 | let inc_aggsig = inc_aggregate(&aggsig, &empty_pm, &empty_pms).unwrap(); 184 | assert!(verify_aggregate(&aggsig, &empty_pm).is_ok()); 185 | assert!(verify_aggregate(&inc_aggsig, &empty_pm).is_ok()); 186 | 187 | let aggsig = AggSig::new(32); 188 | let inc_aggsig = inc_aggregate(&aggsig, &empty_pm, &empty_pms).unwrap(); 189 | assert!(verify_aggregate(&aggsig, &empty_pm).is_ok()); 190 | assert!(verify_aggregate(&inc_aggsig, &empty_pm).is_ok()); 191 | 192 | let aggsig = AggSig::new(0); 193 | assert!( 194 | inc_aggregate(&aggsig, &empty_pm, &empty_pms).unwrap_err() 195 | == hacspec_halfagg::Error::MalformedSignature 196 | ); 197 | assert!( 198 | verify_aggregate(&aggsig, &empty_pm).unwrap_err() 199 | == hacspec_halfagg::Error::InvalidSignature 200 | ); 201 | 202 | let big_pms = Seq::<(PublicKey, Message, Signature)>::new(0xffff + 1); 203 | assert!(aggregate(&big_pms).unwrap_err() == hacspec_halfagg::Error::AggSigTooBig); 204 | let aggsig = AggSig::new(32); 205 | let big_pm = Seq::<(PublicKey, Message)>::new(0xffff + 1); 206 | assert!( 207 | inc_aggregate(&aggsig, &big_pm, &empty_pms).unwrap_err() 208 | == hacspec_halfagg::Error::AggSigTooBig 209 | ); 210 | assert!( 211 | inc_aggregate(&aggsig, &empty_pm, &big_pms).unwrap_err() 212 | == hacspec_halfagg::Error::AggSigTooBig 213 | ); 214 | assert!( 215 | verify_aggregate(&aggsig, &big_pm).unwrap_err() == hacspec_halfagg::Error::AggSigTooBig 216 | ); 217 | } 218 | -------------------------------------------------------------------------------- /half-agg-and-adaptor-sigs.md: -------------------------------------------------------------------------------- 1 | # Does cross-input half-aggregation affect adaptor signature protocols? 2 | 3 | We assume that half-aggregation is only allowed to be applied to key spends, not to script spends. 4 | 5 | # In the cooperative case, half-aggregation can add a half-roundtrip to the communication cost 6 | 7 | To demonstrate this claim, let's look at an adaptor signature protocol, first without half-aggregation and then with half aggregation. 8 | 9 | ## Without half-aggregation 10 | 11 | **Scenario**: Alice creates a coin that can be spent by either 12 | 1. Alice and Bob (key spend) 13 | 2. Alice after time T (script spend) 14 | 15 | Alice wants to learn a secret if Bob spends the coin. 16 | 17 | - Bob sends Alice an adaptor signature for path 1 over a transaction that sends all coins to Bob. 18 | - Alice replies with a signature for path 1. 19 | - Bob spends via path 1. Alice extracts the secret from the adaptor sig and Bob's real sig and is happy. 20 | 21 | ## With half-aggregation 22 | 23 | **Scenario**: Alice creates a coin that can be spent by either 24 | 1. Alice and Bob (key spend) 25 | 2. Alice after time T (script spend) 26 | 3. Alice and Bob (script spend) 27 | 28 | Alice wants to learn a secret if Bob spends the coin. 29 | 30 | - Bob sends Alice an adaptor signature for path 3 over a transaction that sends all coins to Bob. 31 | Due to half-aggregation, path 1 can not be used for this: the signature could get randomized which would prevent extracting the secret. 32 | - Alice replies with a signature for path 3. 33 | - Bob could spend via path 3, but that would be more expensive than spending via path 1. 34 | - Therefore, Bob sends Alice the secret directly 35 | - ... and if Alice cooperates, she replies with a signature for path 1. 36 | 37 | Thus, with half aggregation, the cooperative case requires more communication. 38 | 39 | Note that there are adaptor signature protocols that are not affected by half aggregation. 40 | In some sense, there are two types of adaptor signature interactions, one where Alice learns a secret from a transaction and one where Bob can create a transaction after learning a secret (for example, by making a lightning payment). 41 | We have seen the former above and provide an example for the latter below to show that it is unaffected by half aggregation. 42 | The standard scriptless script coinswap requires both types of adaptor signatures. 43 | 44 | ### Unaffected adaptor sig protocol 45 | 46 | **Scenario**: Alice creates a coin that can be spent by either 47 | 1. Alice and Bob (key spend) 48 | 2. Alice after time T (script spend) 49 | 50 | Bob wants to be able to spend the coin if he learns a secret. 51 | 52 | Alice sends Bob an adaptor signature for path 1 over a transaction that sends all coins to Bob. 53 | Using the secret adaptor that Bob later learns, Bob can extract the signature. 54 | Once Bob learns the adaptor secret, for example, by making a Lightning payment, he can immediately spend the funding output via path 1. 55 | -------------------------------------------------------------------------------- /half-aggregation.mediawiki: -------------------------------------------------------------------------------- 1 |
  2 |   Title: Half-Aggregation of BIP 340 signatures
  3 |   Status: EXPERIMENTAL
  4 | 
5 | 6 | == Introduction == 7 | 8 | === Abstract === 9 | 10 | This document describes ''half-aggregation'' of BIP 340 signatures. 11 | Half-aggregation is a non-interactive process for aggregating a collection of signatures into a single aggregate signature. 12 | The size of the resulting aggregate signature is approximately half of the combined size of the original signatures. 13 | 14 | === Copyright === 15 | 16 | This document is licensed under the 3-clause BSD license. 17 | 18 | === Motivation === 19 | 20 | Half-aggregation is applicable if there is a verifier that needs to verify multiple signatures. 21 | Instead of sending individual signatures to the verifier, the signatures can be compressed into a single aggregate signature and sent to the verifier. 22 | If the verifier can successfully verify the aggregate signature, the verifier can be sure that the individual signatures would have passed verification. 23 | 24 | The purpose of half-aggregation is to reduce the size of the data that is sent to the verifier. 25 | While ''n'' BIP 340 signatures are ''64*n'' bytes, a half-aggregate of the same signatures is ''32*n + 32'' bytes. 26 | The process of half-aggregation is straightforward: it is a pure function of the input signatures, public keys, and messages. 27 | It is non-interactive and does ''not'' require cooperation from other parties, including signers or verifiers. 28 | 29 | There are a variety of scenarios where half-aggregation of BIP-340 signatures is useful. 30 | To keep this section brief and avoid getting outdated quickly, we focus on listing example applications and defer the detailed discussion of the application-specific trade-offs to other places. 31 | 32 | One example is the Lightning Network routing gossip protocol, which [https://github.com/lightning/bolts/blob/2e8f2095a36afb9de38da0f3f0051c7dc16dfc36/07-routing-gossip.md as of this writing] involves messages that contain ECDSA signatures. 33 | If the signature scheme was changed to BIP 340, half-aggregation could reduce the total amount of gossiped data. 34 | Instead of sending individual gossip messages, nodes could assemble a batch of messages and half-aggregate the signatures of the individual messages into a single signature for the batch. 35 | 36 | Another application of half-aggregation is within the Bitcoin consensus protocol. 37 | In particular, it has been discussed in the context of the [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2018-February/015700.html Graftroot proposal]. 38 | Half-aggregation would allow Graftroot spending to be as efficient as best-case Taproot spends by aggregating the signature of the ''surrogate script'' and signatures that satisfy this script. 39 | Moreover, half-aggregation improves the efficiency of proposed Bitcoin script opcodes that verify multiple signatures, such as [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2022-February/019926.html OP_EVICT]. 40 | We can also imagine adding a Bitcoin script opcode that verifies a half-aggregate signature, allowing for more efficient non-interactive multi and threshold signatures. 41 | 42 | Half-aggregation can also be applied to the signatures in the inputs of Bitcoin transactions, a process known as cross-input signature aggregation (CISA). 43 | This [https://github.com/ElementsProject/cross-input-aggregation/blob/master/savings.org reduces the size of an average transaction] by 20.6% and the weight by 7.6%. 44 | A known downside of using half aggregation is that some uses of adaptor signature protocols [https://github.com/ElementsProject/cross-input-aggregation#half-aggregation-and-adaptor-signatures may be incompatible]. 45 | Usually, CISA is proposed with interactive ''full'' signature aggregation instead of non-interactive half-aggregation because creating a valid transaction already requires cooperation, and full signature aggregation is more efficient. 46 | However, the difference in complexity between half-aggregation and full aggregation is so significant that basing a CISA on half-aggregation is a legitimate approach. 47 | 48 | The most invasive application to Bitcoin's consensus would be block-wide signature aggregation. 49 | It refers to a process where block producers aggregate as many transaction signatures as possible. 50 | In the best case, a full block would only have a single half-aggregate signature. 51 | While this is attractive from the efficiency perspective, block-wide aggregation requires more research (and, in particular, special attention to handling 52 | [https://github.com/ElementsProject/cross-input-aggregation#half-aggregation-and-reorgs reorgs]). 53 | 54 | === Design === 55 | 56 | The idea for half-aggregation of Schnorr signatures was brought up in the context of block-wide signature aggregation [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014272.html by Tadge Dryja on the Bitcoin mailing list] in 2017. 57 | The scheme had a security flaw that was [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014306.html noticed] and [https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014308.html fixed] shortly after by Russell O'Connor and Andrew Poelstra. 58 | In 2021 [https://eprint.iacr.org/2021/350 Chalkias, Garillot, Kondi and Nikolaenko] published a security proof in the random oracle model (ROM) that reduces the security of half-aggregation to the security of Schnorr signatures. 59 | [https://eprint.iacr.org/2022/222.pdf Chen and Zhao] were able to produce a tight proof in the ROM and algebraic group model in the following year. 60 | Moreover, they came up with an elegant approach to incremental aggregation that is used in this document. 61 | 62 | * Incremental aggregation allows non-interactively aggregating additional BIP 340 signatures into an existing half-aggregate signature. 63 | * A half-aggregate signature of ''u'' BIP 340 input signatures is serialized as the ''(u+1)⋅32''-byte array ''r1 || ... || ru || bytes(s)'' where ''ri'' is a 32-byte array from input signature ''i'' and ''s'' is a scalar aggregate (see below for details). 64 | * This document does ''not'' specify the aggregation of multiple aggregate signatures (yet). It is possible, but requires changing the encoding of an aggregate signature. Since it is not possible to undo the aggregation of the s-values, when verifying of such an aggregate signature the randomizers need to be the same as when verifying the individual aggregate signature. Therefore, the aggregate signature needs to encode a tree that reveals how the individual signatures were aggregated and how the resulting aggregate signatures were reaggregated. 65 | * The first randomizer ''z0'' is fixed to the constant ''1'', which speeds up verification because ''z0⋅R0 = R0''. This optimization has been suggested and proven secure by [https://eprint.iacr.org/2022/222.pdf Chen and Zhao]. 66 | * The maximum number of signatures that can be aggregated is ''216 - 1''. Having a maximum value is supposed to prevent integer overflows. This specific value was a conservative choice and may be raised in the future (TODO). 67 | 68 | == Description == 69 | 70 | === Specification === 71 | 72 | The specification is written in [https://github.com/hacspec/hacspec hacspec], a language for formal specifications and a subset of rust. 73 | It can be found in the [[hacspec-halfagg/src/halfagg.rs|hacspec-halfagg directory]]. 74 | Note that the specification depends the hacspec library and a [https://github.com/hacspec/hacspec/pull/244 hacspec implementation of BIP 340]. 75 | 76 | === Test Vectors === 77 | 78 | Preliminary test vectors are provided in [[hacspec-halfagg/tests/tests.rs|tests.rs]]. 79 | The specification can be executed with the test vectors by running cargo test in the [[hacspec-halfagg|hacspec-halfagg directory]] (cargo is the [https://doc.rust-lang.org/stable/cargo/ rust package manager]). 80 | 81 | === Pseudocode === 82 | 83 | The following pseudocode is ''not'' a specification but is only intended to augment the actual hacspec [[#specification|specification]]. 84 | 85 | ==== Notation ==== 86 | 87 | The following conventions are used, with constants as defined for [https://www.secg.org/sec2-v2.pdf secp256k1]. We note that adapting this specification to other elliptic curves is not straightforward and can result in an insecure schemeAmong other pitfalls, using the specification with a curve whose order is not close to the size of the range of the nonce derivation function is insecure.. 88 | * Lowercase variables represent integers or byte arrays. 89 | ** The constant ''p'' refers to the field size, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F''. 90 | ** The constant ''n'' refers to the curve order, ''0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141''. 91 | * Uppercase variables refer to points on the curve with equation ''y2 = x3 + 7'' over the integers modulo ''p''. 92 | ** ''is_infinite(P)'' returns whether or not ''P'' is the point at infinity. 93 | ** ''x(P)'' and ''y(P)'' are integers in the range ''0..p-1'' and refer to the X and Y coordinates of a point ''P'' (assuming it is not infinity). 94 | ** The constant ''G'' refers to the base point, for which ''x(G) = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'' and ''y(G) = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8''. 95 | ** Addition of points refers to the usual [https://en.wikipedia.org/wiki/Elliptic_curve#The_group_law elliptic curve group operation]. 96 | ** [https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication Multiplication (⋅) of an integer and a point] refers to the repeated application of the group operation. 97 | * Functions and operations: 98 | ** ''||'' refers to byte array concatenation. 99 | ** The function ''x[i:j]'', where ''x'' is a byte array and ''i, j ≥ 0'', returns a ''(j - i)''-byte array with a copy of the ''i''-th byte (inclusive) to the ''j''-th byte (exclusive) of ''x''. 100 | ** The function ''bytes(x)'', where ''x'' is an integer, returns the 32-byte encoding of ''x'', most significant byte first. 101 | ** The function ''bytes(P)'', where ''P'' is a point, returns ''bytes(x(P))''. 102 | ** The function ''len(x)'' where ''x'' is a byte array returns the length of the array. 103 | ** The function ''int(x)'', where ''x'' is a 32-byte array, returns the 256-bit unsigned integer whose most significant byte first encoding is ''x''. 104 | ** The function ''has_even_y(P)'', where ''P'' is a point for which ''not is_infinite(P)'', returns ''y(P) mod 2 = 0''. 105 | ** The function ''lift_x(x)'', where ''x'' is a 256-bit unsigned integer, returns the point ''P'' for which ''x(P) = x'' 106 | Given a candidate X coordinate ''x'' in the range ''0..p-1'', there exist either exactly two or exactly zero valid Y coordinates. If no valid Y coordinate exists, then ''x'' is not a valid X coordinate either, i.e., no point ''P'' exists for which ''x(P) = x''. The valid Y coordinates for a given candidate ''x'' are the square roots of ''c = x3 + 7 mod p'' and they can be computed as ''y = ±c(p+1)/4 mod p'' (see [https://en.wikipedia.org/wiki/Quadratic_residue#Prime_or_prime_power_modulus Quadratic residue]) if they exist, which can be checked by squaring and comparing with ''c''. and ''has_even_y(P)'', or fails if ''x'' is greater than ''p-1'' or no such point exists. The function ''lift_x(x)'' is equivalent to the following pseudocode: 107 | *** Fail if ''x ≥ p''. 108 | *** Let ''c = x3 + 7 mod p''. 109 | *** Let ''y = c(p+1)/4 mod p''. 110 | *** Fail if ''c ≠ y2 mod p''. 111 | *** Return the unique point ''P'' such that ''x(P) = x'' and ''y(P) = y'' if ''y mod 2 = 0'' or ''y(P) = p-y'' otherwise. 112 | ** The function ''hashtag(x)'' where ''tag'' is a UTF-8 encoded tag name and ''x'' is a byte array returns the 32-byte hash ''SHA256(SHA256(tag) || SHA256(tag) || x)''. 113 | * Other: 114 | ** Tuples are written by listing the elements within parentheses and separated by commas. For example, ''(2, 3, 1)'' is a tuple. 115 | 116 | ==== Aggregate ==== 117 | 118 | ''Aggregate'' takes an array of public key, message and signature triples and returns an aggregate signature. 119 | If every triple ''(p, m, s)'' is valid (i.e., ''Verify(p, m, s)'' as defined in BIP 340 returns true), then the returned aggregate signature and the array of ''(p, m)'' tuples passes ''VerifyAggregate''. 120 | (However, the inverse does not hold: given suitable valid triples, it is possible to construct an input array to ''Aggregate'' which contains invalid triples, but for which ''VerifyAggregate'' will accept the aggregate signature returned by ''Aggregate''. If this is undesired, input triples should be verified individually before passing them to ''Aggregate''.) 121 | 122 | Input: 123 | * ''pms0..u-1'': an array of ''u'' triples, where the first element of each triple is a 32-byte public key, the second element is a 32-byte message and the third element is a 64-byte BIP 340 signature 124 | 125 | '''''Aggregate(pms0..u-1)''''': 126 | * Let ''aggsig = bytes(0)'' 127 | * Let ''pm_aggd'' be an empty array 128 | * Return ''IncAggregate(aggsig, pm_aggd, pms0..u-1)''; fail if that fails. 129 | 130 | ==== IncAggregate ==== 131 | 132 | ''IncAggregate'' takes an aggregate signature, an array of public key and message tuples corresponding to the aggregate signature, and an additional array of public key, message and signature triples. 133 | It aggregates the additional array of triples into the existing aggregate signature and returns the resulting new aggregate signature. 134 | In other words, if ''VerifyAggregate(aggsig, pm_aggd)'' passes and every triple ''(p, m, s)'' in ''pms_to_agg'' is valid (i.e., ''Verify(p, m, s)'' as defined in BIP 340 returns true), then the returned aggregate signature along with the array of ''(p, m)'' tuples of ''pm_aggd'' and ''pms_to_agg'' passes ''VerifyAggregate''. 135 | (However, the inverse does not hold: given a suitable valid aggregate signature and suitable valid triples, it is possible to construct inputs to ''IncAggregate'' which contain an invalid aggregate signature or invalid triples, but for which ''VerifyAggregate'' will accept the aggregate signature returned by ''IncAggregate''. If this is undesired, the input triples and the input aggregate signature should be verified individually before passing them to ''IncAggregate''.) 136 | 137 | Input: 138 | * ''aggsig'' : a byte array 139 | * ''pm_aggd0..v-1'': an array of ''v'' tuples, where the first element of each tuple is a 32-byte public key and the second element is a 32-byte message 140 | * ''pms_to_agg0..u-1'': an array of ''u'' triples, where the first element of each tuple is a 32-byte public key, the second element is a 32-byte message and the third element is a 64-byte BIP 340 signature 141 | 142 | '''''IncAggregate(aggsig, pm_aggd0..v-1, pms_to_agg0..u-1)''''': 143 | * Fail if ''v + u ≥ 216'' 144 | * Fail if ''len(aggsig) ≠ 32 * (v + 1)'' 145 | * For ''i = 0 .. v-1'': 146 | ** Let ''(pki, mi) = pm_aggdi'' 147 | ** Let ''ri = aggsig[i⋅32:(i+1)⋅32]'' 148 | * For ''i = v .. v+u-1'': 149 | ** Let ''(pki, mi, sigi) = pms_to_aggi-v'' 150 | ** Let ''ri = sigi[0:32]'' 151 | ** Let ''si = int(sigi[32:64])'' 152 | ** If ''i = 0'': 153 | *** Let ''zi = 1'' 154 | ** Else: 155 | *** Let ''zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n'' 156 | * Let ''s = int(aggsig[(v⋅32:(v+1)⋅32]) + zv⋅sv + ... + zv+u-1⋅sv+u-1 mod n'' 157 | * Return ''r0 || ... || rv+u-1 || bytes(s)'' 158 | 159 | ==== VerifyAggregate ==== 160 | 161 | ''VerifyAggregate'' verifies a given aggregate signature against an array of public key and message tuples. 162 | 163 | Input: 164 | * ''aggsig'' : a byte array 165 | * ''pm_aggd0..u-1'': an array of ''u'' tuples, where the first element of each tuple is a 32-byte public key and the second element is a 32-byte message 166 | 167 | '''''VerifyAggregate(aggsig, pm_aggd0..u-1)''''': 168 | The algorithm ''VerifyAggregate(aggsig, pm_aggd0..u-1)'' is defined as: 169 | * Fail if ''u ≥ 216'' 170 | * Fail if ''len(aggsig) ≠ 32 * (u + 1)'' 171 | * For ''i = 0 .. u-1'': 172 | ** Let ''(pki, mi) = pm_aggdi'' 173 | ** Let ''Pi = lift_x(int(pki))''; fail if that fails 174 | ** Let ''ri = aggsig[i⋅32:(i+1)⋅32]'' 175 | ** Let ''Ri = lift_x(int(ri))''; fail if that fails 176 | ** Let ''ei = int(hashBIP0340/challenge(bytes(ri) || pki || mi)) mod n'' 177 | ** If ''i = 0'': 178 | *** Let ''zi = 1'' 179 | ** Else: 180 | *** Let ''zi = int(hashHalfAgg/randomizer(r0 || pk0 || m0 || ... || ri || pki || mi)) mod n'' 181 | * Let ''s = int(aggsig[u⋅32:(u+1)⋅32]); fail if ''s ≥ n'' 182 | * Fail if ''s⋅G ≠ z0⋅(R0 + e0⋅P0) + ... + zu-1⋅(Ru-1 + eu-1⋅Pu-1)'' 183 | * Return success iff no failure occurred before reaching this point. 184 | 185 | The verification algorithm is similar to [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#batch-verification BIP340 Batch Verification]. As in BIP340, using an [https://bitcoin.stackexchange.com/a/80702/109853 efficient algorithm for computing the sum of multiple EC multiplications] can significantly speed up verification. 186 | -------------------------------------------------------------------------------- /savings.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Cross-input-aggregation savings 2 | 3 | * Savings in an average taproot transaction 4 | 5 | #+BEGIN_SRC python :session :results value :exports both 6 | # Transaction with segwit v1 spends and outputs. 7 | # Outputs size in bytes and weight units. 8 | def tx_size(n_inputs, n_outputs): 9 | assert(n_inputs <= 252 and n_outputs <= 252) 10 | return list(map(lambda mult: ( 11 | mult*( 12 | 4 # version 13 | + 1 # number of inputs 14 | + n_inputs * ( 15 | 32 + 4 # prevout 16 | + 1 # script len 17 | + 4) # sequence 18 | + 1 # number of outputs 19 | + n_outputs * ( 20 | 8 # amount 21 | + 1 # script len 22 | + 34) # script 23 | + 4) # locktime 24 | + 2 # witness flag & marker 25 | + n_inputs * ( 26 | 1 # witness stack items 27 | + 1 # witness item len 28 | + 64)), # BIP-340 sig 29 | [1, 4])) 30 | 31 | 32 | # 365 day moving average according to 33 | # - https://transactionfee.info/charts/inputs-per-transaction/?avg=365&start=2025-02-26&end=2025-02-27 and 34 | # - https://transactionfee.info/charts/outputs-per-transaction/?avg=365&start=2025-02-26&end=2025-02-27 35 | # retrieved 2025-02-28. 36 | n_inputs = 2.26 37 | n_outputs = 2.69 38 | 39 | 40 | size = tx_size(n_inputs, n_outputs) 41 | half_agged_tx = map(lambda s: s - (n_inputs - 1)*32, size) 42 | half_agged_block = map(lambda s: s - n_inputs*32, size) 43 | full_agged_tx = map(lambda s: s - (n_inputs-1)*64, size) 44 | full_agged_tx_half_agged_block = map(lambda s: s - (n_inputs-1)*64 - 32, size) 45 | max_agged = map(lambda s: s - n_inputs*64, size) 46 | 47 | def savings(name, agged_sizes): 48 | return [name] + ["%.1f%%" % ((1 - a/b)*100) for (a,b) in zip(agged_sizes, size)] 49 | 50 | [ 51 | [ "", "bytes", "weight units" ], 52 | None, 53 | savings("half aggregation across tx", half_agged_tx), 54 | savings("half aggregation across block", half_agged_block), 55 | savings("full aggregation across tx", full_agged_tx), 56 | savings("full aggregation across tx & half aggregation across block", full_agged_tx_half_agged_block), 57 | savings("max (like infinite large fully aggregated coinjoin)", max_agged) 58 | ] 59 | #+end_src 60 | 61 | #+RESULTS: 62 | | | bytes | weight units | 63 | |------------------------------------------------------------+-------+--------------| 64 | | half aggregation across tx | 10.9% | 3.9% | 65 | | half aggregation across block | 19.6% | 7.1% | 66 | | full aggregation across tx | 21.8% | 7.9% | 67 | | full aggregation across tx & half aggregation across block | 30.5% | 11.0% | 68 | | max (like infinite large fully aggregated coinjoin) | 39.1% | 14.1% | 69 | 70 | 71 | * Graftroot 72 | Spending a taproot-like output via graftroot requires a 64-byte signature of the script that the signers delegate control to. 73 | In comparison, opening a taproot commitment only requires revealing the 32-byte "internal" pubkey (if the committed script is at depth zero). 74 | With half- or full-aggregation of signatures outside script, the graftroot signature can be aggregated just like key-spend signatures. 75 | As a result, graftroot spends would be equal to or more efficient that taproot script spends. 76 | -------------------------------------------------------------------------------- /slides/2021-Q2-halfagg-impl.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Intro to Signature Half Aggregation 2 | 3 | Goal: implement half aggregation in libsecp256k1-zkp 4 | ([[https://github.com/ElementsProject/secp256k1-zkp]]) 5 | 6 | * Standard signatures 7 | - KeyGen(sk) -> pk 8 | - Sign(sk, m) -> sig 9 | - Verify(pk, m, sig) -> {true, false} 10 | 11 | * Aggregate signatures 12 | - AggVerify((pk_1, m_1), ..., (pk_n, m_n), sig) -> {true, false} 13 | - Trivial solution: 14 | sig = (sig_1, ..., sig_n) 15 | - Goal Nr 2: sig should be short 16 | - Note the different messages != multisignatures, MuSig, etc. 17 | 18 | * Schnorr Signature Half Aggregation 19 | - Aggregate(sig_1, ..., sig_n) -> sig 20 | - AggVerify((pk_1, m_1), ..., (pk_n, m_n), sig) -> {true, false} 21 | 22 | 23 | 1. |sig| ≈ 1/2 (|sig_1| + ... + |sig_n|) 24 | 2. aggregation is non-interactive 25 | 26 | [[https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014272.html][proposed on Bitcoin mailing list ~2017]], recent academic paper ([[https://eprint.iacr.org/2021/350][Chalkias et al.]]) 27 | 28 | * Applications 29 | - block-wide signature aggregation (but it has downsides related to adaptor sigs) 30 | - L2 gossip protocols 31 | 32 | * Aside: "Full" Schnorr Sig Aggregation 33 | 1. |sig| = |sig_1| 34 | 2. aggregation _is_ interactive 35 | 36 | Tx-wide aggregation 37 | can be combined with half aggregation 38 | * Schnorr signature verification (BIP 340) 39 | #+BEGIN_SRC 40 | Verify(pk, m, sig): 41 | P = lift_x(int(pk)); fail if that fails 42 | r = int(sig[0:32]); fail if r ≥ p 43 | s = int(sig[32:64]); fail if s ≥ N 44 | R = lift_x(int(r)); fail if that fails 45 | 46 | e = int(hash_{BIP0340/challenge}(bytes(r) || pk || m)) mod N 47 | Fail if s⋅G ≠ R + e⋅P 48 | #+END_SRC 49 | 50 | * Schnorr Signature Half Aggregation 51 | "Concatenate the r-values of the given signatures, and just +sum up the s-values+ 52 | sum up the s-values after multiplying them with unpredictable values" 53 | 54 | - Aggregate((pk_1, m_1, sig_1), ..., (pk_n, m_n, sig_n)): 55 | #+BEGIN_SRC 56 | For i = 1 .. n: 57 | r_i = sig_i[0:32] 58 | s_i = int(sig_i[32:64]) 59 | For i = 1 .. n: 60 | z_i = int(hash_{HalfAggregation}(r_1 || pk_1 || m_1 || ... || r_n || pk_n || m_n || i)) mod N 61 | s = z_1⋅s_1 + ... + z_n⋅s_n 62 | Return sig = r_1 || ... || r_n || bytes(s) 63 | #+END_SRC 64 | 65 | - AggregateVerify((pk_1, m_1), ..., (pk_n, m_n)), sig): 66 | #+BEGIN_SRC 67 | For i = 1 .. n: 68 | P_i = lift_x(int(pk_i)); fail if that fails 69 | r_i = sig[(i-1)⋅32:i⋅32]; fail if r ≥ p 70 | R_i = lift_x(int(r_i)); fail if that fails 71 | e_i = int(hash_{BIP0340/challenge}(bytes(r_i) || pk_i || m_i)) mod N 72 | For i = 1 .. n: 73 | z_i = int(hash_{HalfAggregation}(r_1 || pk_1 || m_1 || ... || r_n || pk_n || m_n || i)) mod N 74 | s = int(sig[n⋅32:(n+1)⋅32]) mod N 75 | Fail if s⋅G ≠ z_1⋅(R_1 + e_1⋅P_1) + ... + z_n⋅(R_n + e_n⋅P_n) 76 | #+END_SRC 77 | 78 | - Correctness? 79 | - Example: Given two sigs (r_1, s_1), (r_2, s_2) 80 | - valid Schnorr signature implies s_i⋅G = lift_x(r_i) + e_i⋅P_i 81 | - Aggregate sig = (r_1, r_2, z_1⋅s_1 + z_2⋅s_2) 82 | - And it holds that 83 | - (z_1⋅s_1 + z_2⋅s_2)⋅G = z_1⋅(lift_x(r_1) + e_1⋅P_1) + z_2⋅(lift_x(r_1) + e_2⋅P_2) 84 | - Hence, AggregateVerify succeeds 85 | 86 | * Let's go! 87 | 1. Implement Aggregate and AggregateVerify interface 88 | 2. Write Test 89 | - Correctness: Create Schnorr signatures, Aggregate them, AggregateVerify should always succeed 90 | 3. Implement Aggregate and AggregateVerify 91 | 4. Bonus: Write Test 92 | - "Unforgeability": Create Schnorr signatures, Aggregate them, 93 | any random bit flipped in the input of AggregateVerify will make it fail 94 | 5. Bonus: separate module? API tests? multiexp? z_1 = 1 optimization? streaming api? 95 | --------------------------------------------------------------------------------