diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java index 04ed1897c58..307b7de1f23 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/attestation/ValidatableAttestation.java @@ -13,6 +13,8 @@ package tech.pegasys.teku.spec.datastructures.attestation; +import static com.google.common.base.Preconditions.checkState; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; @@ -24,6 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.constants.Domain; @@ -31,11 +34,12 @@ import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.spec.datastructures.operations.IndexedAttestation; import tech.pegasys.teku.spec.datastructures.operations.SignedAggregateAndProof; +import tech.pegasys.teku.spec.datastructures.operations.versions.electra.AttestationElectraSchema; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; public class ValidatableAttestation { private final Spec spec; - private final Attestation attestation; + private volatile Attestation attestation; private final Optional maybeAggregate; private final Supplier hashTreeRoot; private final AtomicBoolean gossiped = new AtomicBoolean(false); @@ -48,6 +52,54 @@ public class ValidatableAttestation { private volatile Optional indexedAttestation = Optional.empty(); private volatile Optional committeeShufflingSeed = Optional.empty(); private volatile Optional committeesSize = Optional.empty(); + private volatile Optional singleAttestation = Optional.empty(); + + public void convertFromSingleAttestation(final SszBitlist singleAttestationAggregationBits) { + final Attestation localAttestation = attestation; + checkState(localAttestation.isSingleAttestation()); + singleAttestation = Optional.of(localAttestation.toSingleAttestationRequired()); + + final AttestationElectraSchema attestationElectraSchema = + spec.atSlot(localAttestation.getData().getSlot()) + .getSchemaDefinitions() + .getAttestationSchema() + .toVersionElectra() + .orElseThrow(); + + attestation = + attestationElectraSchema.create( + singleAttestationAggregationBits, + localAttestation.getData(), + localAttestation.getAggregateSignature(), + attestationElectraSchema + .getCommitteeBitsSchema() + .orElseThrow() + .ofBits(localAttestation.getFirstCommitteeIndex().intValue())); + } + + public void createSingleAttestation(final UInt64 validatorIndex) { + if (attestation.getCommitteeBits().isEmpty() || singleAttestation.isPresent()) { + return; + } + spec.atSlot(attestation.getData().getSlot()) + .getSchemaDefinitions() + .toVersionElectra() + .ifPresent( + schemaDefinitionsElectra -> { + checkState(attestation.getCommitteeBitsRequired().getBitCount() == 1); + + singleAttestation = + Optional.of( + schemaDefinitionsElectra + .getSingleAttestationSchema() + .toSingleAttestationSchemaRequired() + .create( + attestation.getFirstCommitteeIndex(), + validatorIndex, + attestation.getData(), + attestation.getAggregateSignature())); + }); + } public static ValidatableAttestation from(final Spec spec, final Attestation attestation) { return new ValidatableAttestation( @@ -220,6 +272,10 @@ public Attestation getAttestation() { return attestation; } + public Attestation getSingleAttestation() { + return singleAttestation.orElse(attestation); + } + public SignedAggregateAndProof getSignedAggregateAndProof() { return maybeAggregate.orElseThrow( () -> new UnsupportedOperationException("ValidatableAttestation is not an aggregate.")); @@ -273,6 +329,7 @@ public String toString() { .add("committeeShufflingSeed", committeeShufflingSeed) .add("committeesSize", committeesSize) .add("receivedSubnetId", receivedSubnetId) + .add("singleAttestation", singleAttestation) .toString(); } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/Attestation.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/Attestation.java index 57f0ff3f234..06cef12df1d 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/Attestation.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/Attestation.java @@ -13,6 +13,7 @@ package tech.pegasys.teku.spec.datastructures.operations; +import com.google.common.collect.Sets; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -34,9 +35,13 @@ public interface Attestation extends SszContainer { @Override AttestationSchema getSchema(); - UInt64 getEarliestSlotForForkChoiceProcessing(final Spec spec); + default UInt64 getEarliestSlotForForkChoiceProcessing(final Spec spec) { + return getData().getEarliestSlotForForkChoice(spec); + } - Collection getDependentBlockRoots(); + default Collection getDependentBlockRoots() { + return Sets.newHashSet(getData().getTarget().getRoot(), getData().getBeaconBlockRoot()); + } AttestationData getData(); @@ -65,4 +70,16 @@ default List getCommitteeIndicesRequired() { } boolean requiresCommitteeBits(); + + default boolean isSingleAttestation() { + return false; + } + + default SingleAttestation toSingleAttestationRequired() { + throw new UnsupportedOperationException("Not a SingleAttestation"); + } + + default UInt64 getValidatorIndexRequired() { + throw new UnsupportedOperationException("Not a SingleAttestation"); + } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/AttestationSchema.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/AttestationSchema.java index 3b0b0f880ec..f3b45e20c0f 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/AttestationSchema.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/AttestationSchema.java @@ -41,6 +41,11 @@ default SszBitlist createEmptyAggregationBits() { return bitsSchema.ofBits(Math.toIntExact(bitsSchema.getMaxLength())); } + default SszBitlist createAggregationBitsOf(final int size, final int... indices) { + final SszBitlistSchema bitsSchema = getAggregationBitsSchema(); + return bitsSchema.ofBits(size, indices); + } + default Optional createEmptyCommitteeBits() { return getCommitteeBitsSchema().map(SszBitvectorSchema::ofBits); } @@ -54,6 +59,10 @@ default Optional toVersionElectra() { return Optional.empty(); } + default SingleAttestationSchema toSingleAttestationSchemaRequired() { + throw new UnsupportedOperationException("Not a SingleAttestationSchema"); + } + SszBitlistSchema getAggregationBitsSchema(); Optional> getCommitteeBitsSchema(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/SingleAttestation.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/SingleAttestation.java new file mode 100644 index 00000000000..5aaaa1c73ac --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/SingleAttestation.java @@ -0,0 +1,93 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.spec.datastructures.operations; + +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; +import tech.pegasys.teku.infrastructure.ssz.containers.Container4; +import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.type.SszSignature; + +public class SingleAttestation + extends Container4 + implements Attestation { + public SingleAttestation(final SingleAttestationSchema type, final TreeNode backingNode) { + super(type, backingNode); + } + + public SingleAttestation( + final SingleAttestationSchema schema, + final UInt64 committeeIndex, + final UInt64 validatorIndex, + final AttestationData data, + final BLSSignature signature) { + super( + schema, + SszUInt64.of(committeeIndex), + SszUInt64.of(validatorIndex), + data, + new SszSignature(signature)); + } + + @Override + public SingleAttestationSchema getSchema() { + return (SingleAttestationSchema) super.getSchema(); + } + + @Override + public AttestationData getData() { + return getField2(); + } + + @Override + public SszBitlist getAggregationBits() { + throw new UnsupportedOperationException("Not supported in SingleAttestation"); + } + + @Override + public UInt64 getFirstCommitteeIndex() { + return getField0().get(); + } + + @Override + public BLSSignature getAggregateSignature() { + return getField3().getSignature(); + } + + public BLSSignature getSignature() { + return getField3().getSignature(); + } + + @Override + public boolean requiresCommitteeBits() { + return false; + } + + @Override + public boolean isSingleAttestation() { + return true; + } + + @Override + public SingleAttestation toSingleAttestationRequired() { + return this; + } + + @Override + public UInt64 getValidatorIndexRequired() { + return getField1().get(); + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/SingleAttestationSchema.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/SingleAttestationSchema.java new file mode 100644 index 00000000000..0993ec88e7f --- /dev/null +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/SingleAttestationSchema.java @@ -0,0 +1,84 @@ +/* + * Copyright Consensys Software Inc., 2024 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package tech.pegasys.teku.spec.datastructures.operations; + +import java.util.Optional; +import java.util.function.Supplier; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; +import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; +import tech.pegasys.teku.infrastructure.ssz.containers.ContainerSchema4; +import tech.pegasys.teku.infrastructure.ssz.primitive.SszUInt64; +import tech.pegasys.teku.infrastructure.ssz.schema.SszPrimitiveSchemas; +import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszBitlistSchema; +import tech.pegasys.teku.infrastructure.ssz.schema.collections.SszBitvectorSchema; +import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; +import tech.pegasys.teku.infrastructure.unsigned.UInt64; +import tech.pegasys.teku.spec.datastructures.type.SszSignature; +import tech.pegasys.teku.spec.datastructures.type.SszSignatureSchema; + +public class SingleAttestationSchema + extends ContainerSchema4 + implements AttestationSchema { + public SingleAttestationSchema() { + super( + "SingleAttestation", + namedSchema("committee_index", SszPrimitiveSchemas.UINT64_SCHEMA), + namedSchema("attester_index", SszPrimitiveSchemas.UINT64_SCHEMA), + namedSchema("data", AttestationData.SSZ_SCHEMA), + namedSchema("signature", SszSignatureSchema.INSTANCE)); + } + + @Override + public SingleAttestation createFromBackingNode(final TreeNode node) { + return new SingleAttestation(this, node); + } + + public SingleAttestation create( + final UInt64 committeeIndex, + final UInt64 attesterIndex, + final AttestationData data, + final BLSSignature signature) { + return new SingleAttestation(this, committeeIndex, attesterIndex, data, signature); + } + + @Override + public Attestation create( + final SszBitlist aggregationBits, + final AttestationData data, + final BLSSignature signature, + final Supplier committeeBits) { + throw new UnsupportedOperationException("Not supported in SingleAttestation"); + } + + @Override + public SszBitlistSchema getAggregationBitsSchema() { + throw new UnsupportedOperationException("Not supported in SingleAttestation"); + } + + @Override + public Optional> getCommitteeBitsSchema() { + return Optional.empty(); + } + + @Override + public SingleAttestationSchema toSingleAttestationSchemaRequired() { + return this; + } + + @Override + public boolean requiresCommitteeBits() { + return false; + } +} diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/electra/AttestationElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/electra/AttestationElectra.java index 1842c46e897..cebb76f5cb3 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/electra/AttestationElectra.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/electra/AttestationElectra.java @@ -13,18 +13,14 @@ package tech.pegasys.teku.spec.datastructures.operations.versions.electra; -import com.google.common.collect.Sets; -import java.util.Collection; import java.util.List; import java.util.Optional; -import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitvector; import tech.pegasys.teku.infrastructure.ssz.containers.Container4; import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; import tech.pegasys.teku.infrastructure.unsigned.UInt64; -import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.operations.Attestation; import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.spec.datastructures.type.SszSignature; @@ -51,16 +47,6 @@ public AttestationElectraSchema getSchema() { return (AttestationElectraSchema) super.getSchema(); } - @Override - public UInt64 getEarliestSlotForForkChoiceProcessing(final Spec spec) { - return getData().getEarliestSlotForForkChoice(spec); - } - - @Override - public Collection getDependentBlockRoots() { - return Sets.newHashSet(getData().getTarget().getRoot(), getData().getBeaconBlockRoot()); - } - @Override public SszBitlist getAggregationBits() { return getField0(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/phase0/AttestationPhase0.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/phase0/AttestationPhase0.java index dd343f37361..e7d464a43aa 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/phase0/AttestationPhase0.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/datastructures/operations/versions/phase0/AttestationPhase0.java @@ -13,15 +13,11 @@ package tech.pegasys.teku.spec.datastructures.operations.versions.phase0; -import com.google.common.collect.Sets; -import java.util.Collection; -import org.apache.tuweni.bytes.Bytes32; import tech.pegasys.teku.bls.BLSSignature; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; import tech.pegasys.teku.infrastructure.ssz.containers.Container3; import tech.pegasys.teku.infrastructure.ssz.tree.TreeNode; import tech.pegasys.teku.infrastructure.unsigned.UInt64; -import tech.pegasys.teku.spec.Spec; import tech.pegasys.teku.spec.datastructures.operations.Attestation; import tech.pegasys.teku.spec.datastructures.operations.AttestationData; import tech.pegasys.teku.spec.datastructures.type.SszSignature; @@ -47,16 +43,6 @@ public AttestationPhase0Schema getSchema() { return (AttestationPhase0Schema) super.getSchema(); } - @Override - public UInt64 getEarliestSlotForForkChoiceProcessing(final Spec spec) { - return getData().getEarliestSlotForForkChoice(spec); - } - - @Override - public Collection getDependentBlockRoots() { - return Sets.newHashSet(getData().getTarget().getRoot(), getData().getBeaconBlockRoot()); - } - @Override public SszBitlist getAggregationBits() { return getField0(); diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/util/AttestationUtilElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/util/AttestationUtilElectra.java index 943536e7bf6..59c9de9c9e2 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/util/AttestationUtilElectra.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/logic/versions/electra/util/AttestationUtilElectra.java @@ -13,23 +13,45 @@ package tech.pegasys.teku.spec.logic.versions.electra.util; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static tech.pegasys.teku.infrastructure.async.SafeFuture.completedFuture; + import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import java.util.List; +import java.util.Optional; import java.util.stream.IntStream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import tech.pegasys.teku.bls.BLSPublicKey; +import tech.pegasys.teku.bls.BLSSignature; +import tech.pegasys.teku.infrastructure.async.SafeFuture; import tech.pegasys.teku.infrastructure.ssz.collections.SszBitlist; import tech.pegasys.teku.infrastructure.unsigned.UInt64; import tech.pegasys.teku.spec.config.SpecConfig; +import tech.pegasys.teku.spec.constants.Domain; +import tech.pegasys.teku.spec.datastructures.attestation.ValidatableAttestation; import tech.pegasys.teku.spec.datastructures.blocks.BeaconBlockSummary; import tech.pegasys.teku.spec.datastructures.operations.Attestation; import tech.pegasys.teku.spec.datastructures.operations.AttestationData; +import tech.pegasys.teku.spec.datastructures.operations.IndexedAttestation; +import tech.pegasys.teku.spec.datastructures.operations.IndexedAttestationSchema; +import tech.pegasys.teku.spec.datastructures.operations.SingleAttestation; +import tech.pegasys.teku.spec.datastructures.state.Fork; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; +import tech.pegasys.teku.spec.datastructures.util.AttestationProcessingResult; import tech.pegasys.teku.spec.logic.common.helpers.BeaconStateAccessors; import tech.pegasys.teku.spec.logic.common.helpers.MiscHelpers; +import tech.pegasys.teku.spec.logic.common.util.AsyncBLSSignatureVerifier; import tech.pegasys.teku.spec.logic.versions.deneb.util.AttestationUtilDeneb; import tech.pegasys.teku.spec.schemas.SchemaDefinitions; public class AttestationUtilElectra extends AttestationUtilDeneb { + private static final Logger LOG = LogManager.getLogger(); + public AttestationUtilElectra( final SpecConfig specConfig, final SchemaDefinitions schemaDefinitions, @@ -93,4 +115,166 @@ public AttestationData getGenericAttestationData( final UInt64 committeeIndex) { return super.getGenericAttestationData(slot, state, block, UInt64.ZERO); } + + @Override + public IndexedAttestation getIndexedAttestation( + final BeaconState state, final Attestation attestation) { + if (attestation.isSingleAttestation()) { + final IndexedAttestationSchema indexedAttestationSchema = + schemaDefinitions.getIndexedAttestationSchema(); + + return indexedAttestationSchema.create( + indexedAttestationSchema + .getAttestingIndicesSchema() + .of(attestation.getValidatorIndexRequired()), + attestation.getData(), + attestation.getAggregateSignature()); + } + return super.getIndexedAttestation(state, attestation); + } + + public IndexedAttestation getIndexedAttestationFromSingleAttestation( + final SingleAttestation attestation) { + final IndexedAttestationSchema indexedAttestationSchema = + schemaDefinitions.getIndexedAttestationSchema(); + + return indexedAttestationSchema.create( + indexedAttestationSchema + .getAttestingIndicesSchema() + .of(attestation.getValidatorIndexRequired()), + attestation.getData(), + attestation.getSignature()); + } + + @Override + public SafeFuture isValidIndexedAttestationAsync( + final Fork fork, + final BeaconState state, + final ValidatableAttestation attestation, + final AsyncBLSSignatureVerifier blsSignatureVerifier) { + + if (attestation.isProducedLocally() && !attestation.isAggregate()) { + // prepare single attestation so that we will have the right version when we gossip it + attestation.createSingleAttestation( + getValidatorIndexFromAttestation(state, attestation.getAttestation())); + } + + if (!attestation.getAttestation().isSingleAttestation()) { + return super.isValidIndexedAttestationAsync(fork, state, attestation, blsSignatureVerifier); + } + + // single attestation flow + + // 1. verify signature first + // 2. verify call getSingleAttestationAggregationBits which also validates the validatorIndex + // and the committee against the state + // 3. convert attestation inside ValidatableAttestation to AttestationElectra + // 4. set the indexed attestation into ValidatableAttestation + // 5. set the attestation as valid indexed attestation + + return validateSingleAttestationSignature( + fork, + state, + attestation.getAttestation().toSingleAttestationRequired(), + blsSignatureVerifier) + .thenApply( + result -> { + if (result.isSuccessful()) { + final SszBitlist singleAttestationAggregationBits = + getSingleAttestationAggregationBits(state, attestation.getAttestation()); + attestation.convertFromSingleAttestation(singleAttestationAggregationBits); + + final IndexedAttestation indexedAttestation = + getIndexedAttestationFromSingleAttestation( + attestation.getSingleAttestation().toSingleAttestationRequired()); + attestation.saveCommitteeShufflingSeedAndCommitteesSize(state); + attestation.setIndexedAttestation(indexedAttestation); + attestation.setValidIndexedAttestation(); + } + return result; + }) + .exceptionallyCompose( + err -> { + if (err.getCause() instanceof IllegalArgumentException) { + LOG.debug("on_attestation: Attestation is not valid: ", err); + return SafeFuture.completedFuture( + AttestationProcessingResult.invalid(err.getMessage())); + } else { + return SafeFuture.failedFuture(err); + } + }); + } + + private SafeFuture validateSingleAttestationSignature( + final Fork fork, + final BeaconState state, + final SingleAttestation singleAttestation, + final AsyncBLSSignatureVerifier signatureVerifier) { + final Optional pubkey = + beaconStateAccessors.getValidatorPubKey( + state, singleAttestation.getValidatorIndexRequired()); + + if (pubkey.isEmpty()) { + return completedFuture( + AttestationProcessingResult.invalid("Attesting index include non-existent validator")); + } + + final BLSSignature signature = singleAttestation.getSignature(); + final Bytes32 domain = + beaconStateAccessors.getDomain( + Domain.BEACON_ATTESTER, + singleAttestation.getData().getTarget().getEpoch(), + fork, + state.getGenesisValidatorsRoot()); + final Bytes signingRoot = miscHelpers.computeSigningRoot(singleAttestation.getData(), domain); + + return signatureVerifier + .verify(pubkey.get(), signingRoot, signature) + .thenApply( + isValidSignature -> { + if (isValidSignature) { + return AttestationProcessingResult.SUCCESSFUL; + } else { + return AttestationProcessingResult.invalid("Signature is invalid"); + } + }); + } + + private SszBitlist getSingleAttestationAggregationBits( + final BeaconState state, final Attestation attestation) { + checkArgument(attestation.isSingleAttestation(), "Expecting single attestation"); + + final IntList committee = + beaconStateAccessors.getBeaconCommittee( + state, attestation.getData().getSlot(), attestation.getFirstCommitteeIndex()); + + int validatorIndex = attestation.getValidatorIndexRequired().intValue(); + + int validatorCommitteeBit = committee.indexOf(validatorIndex); + + checkArgument( + validatorCommitteeBit >= 0, + "Validator index %s is not part of the committee %s", + validatorIndex, + attestation.getFirstCommitteeIndex()); + + return schemaDefinitions + .toVersionElectra() + .orElseThrow() + .getAttestationSchema() + .createAggregationBitsOf(committee.size(), validatorCommitteeBit); + } + + private UInt64 getValidatorIndexFromAttestation( + final BeaconState state, final Attestation attestation) { + final IntList committee = + beaconStateAccessors.getBeaconCommittee( + state, attestation.getData().getSlot(), attestation.getFirstCommitteeIndex()); + final IntList validatorIndices = attestation.getAggregationBits().getAllSetBits(); + checkState( + validatorIndices.size() == 1, + "Expected a single validator to be attesting but found %s", + validatorIndices.size()); + return UInt64.valueOf(committee.getInt(validatorIndices.getInt(0))); + } } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsElectra.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsElectra.java index 46ef4ecd760..1cf7f87adf9 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsElectra.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/SchemaDefinitionsElectra.java @@ -20,6 +20,7 @@ import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.PENDING_CONSOLIDATIONS_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.PENDING_DEPOSITS_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.PENDING_PARTIAL_WITHDRAWALS_SCHEMA; +import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SINGLE_ATTESTATION_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.WITHDRAWAL_REQUEST_SCHEMA; import java.util.Optional; @@ -31,6 +32,9 @@ import tech.pegasys.teku.spec.datastructures.execution.versions.electra.DepositRequestSchema; import tech.pegasys.teku.spec.datastructures.execution.versions.electra.ExecutionRequestsSchema; import tech.pegasys.teku.spec.datastructures.execution.versions.electra.WithdrawalRequestSchema; +import tech.pegasys.teku.spec.datastructures.operations.AttestationSchema; +import tech.pegasys.teku.spec.datastructures.operations.SingleAttestation; +import tech.pegasys.teku.spec.datastructures.operations.SingleAttestationSchema; import tech.pegasys.teku.spec.datastructures.state.versions.electra.PendingConsolidation; import tech.pegasys.teku.spec.datastructures.state.versions.electra.PendingConsolidation.PendingConsolidationSchema; import tech.pegasys.teku.spec.datastructures.state.versions.electra.PendingDeposit; @@ -53,6 +57,8 @@ public class SchemaDefinitionsElectra extends SchemaDefinitionsDeneb { private final PendingPartialWithdrawalSchema pendingPartialWithdrawalSchema; private final PendingConsolidationSchema pendingConsolidationSchema; + private final SingleAttestationSchema singleAttestationSchema; + public SchemaDefinitionsElectra(final SchemaRegistry schemaRegistry) { super(schemaRegistry); this.executionRequestsSchema = schemaRegistry.get(EXECUTION_REQUESTS_SCHEMA); @@ -60,6 +66,8 @@ public SchemaDefinitionsElectra(final SchemaRegistry schemaRegistry) { this.pendingPartialWithdrawalsSchema = schemaRegistry.get(PENDING_PARTIAL_WITHDRAWALS_SCHEMA); this.pendingConsolidationsSchema = schemaRegistry.get(PENDING_CONSOLIDATIONS_SCHEMA); + this.singleAttestationSchema = schemaRegistry.get(SINGLE_ATTESTATION_SCHEMA); + this.depositRequestSchema = schemaRegistry.get(DEPOSIT_REQUEST_SCHEMA); this.withdrawalRequestSchema = schemaRegistry.get(WITHDRAWAL_REQUEST_SCHEMA); this.consolidationRequestSchema = schemaRegistry.get(CONSOLIDATION_REQUEST_SCHEMA); @@ -126,6 +134,10 @@ public PendingConsolidation.PendingConsolidationSchema getPendingConsolidationSc return pendingPartialWithdrawalsSchema; } + public AttestationSchema getSingleAttestationSchema() { + return singleAttestationSchema; + } + public SszListSchema getPendingConsolidationsSchema() { return pendingConsolidationsSchema; } diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java index 0f459fb0d4b..16ac090a73a 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaRegistryBuilder.java @@ -58,6 +58,7 @@ import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SIGNED_BLOCK_CONTENTS_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SIGNED_BLS_TO_EXECUTION_CHANGE_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SIGNED_BUILDER_BID_SCHEMA; +import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SINGLE_ATTESTATION_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.SYNCNETS_ENR_FIELD_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.WITHDRAWAL_REQUEST_SCHEMA; import static tech.pegasys.teku.spec.schemas.registry.SchemaTypes.WITHDRAWAL_SCHEMA; @@ -117,6 +118,7 @@ import tech.pegasys.teku.spec.datastructures.operations.IndexedAttestationSchema; import tech.pegasys.teku.spec.datastructures.operations.SignedAggregateAndProof.SignedAggregateAndProofSchema; import tech.pegasys.teku.spec.datastructures.operations.SignedBlsToExecutionChangeSchema; +import tech.pegasys.teku.spec.datastructures.operations.SingleAttestationSchema; import tech.pegasys.teku.spec.datastructures.operations.versions.electra.AttestationElectraSchema; import tech.pegasys.teku.spec.datastructures.operations.versions.phase0.AttestationPhase0Schema; import tech.pegasys.teku.spec.datastructures.state.HistoricalBatch.HistoricalBatchSchema; @@ -188,7 +190,14 @@ public static SchemaRegistryBuilder create() { .addProvider(createDepositRequestSchemaProvider()) .addProvider(createWithdrawalRequestSchemaProvider()) .addProvider(createConsolidationRequestSchemaProvider()) - .addProvider(createExecutionRequestsSchemaProvider()); + .addProvider(createExecutionRequestsSchemaProvider()) + .addProvider(createSingleAttestationSchemaProvider()); + } + + private static SchemaProvider createSingleAttestationSchemaProvider() { + return providerBuilder(SINGLE_ATTESTATION_SCHEMA) + .withCreator(ELECTRA, (registry, specConfig, schemaName) -> new SingleAttestationSchema()) + .build(); } private static SchemaProvider createDepositRequestSchemaProvider() { diff --git a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java index 4a8356d373a..1c6b3756672 100644 --- a/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java +++ b/ethereum/spec/src/main/java/tech/pegasys/teku/spec/schemas/registry/SchemaTypes.java @@ -59,6 +59,7 @@ import tech.pegasys.teku.spec.datastructures.operations.IndexedAttestationSchema; import tech.pegasys.teku.spec.datastructures.operations.SignedAggregateAndProof.SignedAggregateAndProofSchema; import tech.pegasys.teku.spec.datastructures.operations.SignedBlsToExecutionChangeSchema; +import tech.pegasys.teku.spec.datastructures.operations.SingleAttestationSchema; import tech.pegasys.teku.spec.datastructures.state.HistoricalBatch.HistoricalBatchSchema; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconState; import tech.pegasys.teku.spec.datastructures.state.beaconstate.BeaconStateSchema; @@ -169,6 +170,8 @@ public class SchemaTypes { create("WITHDRAWAL_REQUEST_SCHEMA"); public static final SchemaId CONSOLIDATION_REQUEST_SCHEMA = create("CONSOLIDATION_REQUEST_SCHEMA"); + public static final SchemaId SINGLE_ATTESTATION_SCHEMA = + create("SINGLE_ATTESTATION_SCHEMA"); private SchemaTypes() { // Prevent instantiation diff --git a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationValidator.java b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationValidator.java index f652f597585..041d1f9cb07 100644 --- a/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationValidator.java +++ b/ethereum/statetransition/src/main/java/tech/pegasys/teku/statetransition/validation/AttestationValidator.java @@ -72,6 +72,11 @@ public SafeFuture validate( } private InternalValidationResult singleAttestationChecks(final Attestation attestation) { + // if it is a SingleAttestation type we are guaranteed to be a valid single attestation + if (attestation.isSingleAttestation()) { + return InternalValidationResult.ACCEPT; + } + // The attestation is unaggregated -- that is, it has exactly one participating validator // (len([bit for bit in attestation.aggregation_bits if bit == 0b1]) == 1). final int bitCount = attestation.getAggregationBits().getBitCount(); @@ -167,15 +172,17 @@ SafeFuture singleOrAggregateAttestationChecks attestation.getFirstCommitteeIndex(), receivedOnSubnetId.getAsInt())); } - // [REJECT] The number of aggregation bits matches the committee size - final IntList committee = - spec.getBeaconCommittee( - state, data.getSlot(), attestation.getFirstCommitteeIndex()); - if (committee.size() != attestation.getAggregationBits().size()) { - return completedFuture( - InternalValidationResultWithState.reject( - "Aggregation bit size %s is greater than committee size %s", - attestation.getAggregationBits().size(), committee.size())); + if (!attestation.isSingleAttestation()) { + // [REJECT] The number of aggregation bits matches the committee size + final IntList committee = + spec.getBeaconCommittee( + state, data.getSlot(), attestation.getFirstCommitteeIndex()); + if (committee.size() != attestation.getAggregationBits().size()) { + return completedFuture( + InternalValidationResultWithState.reject( + "Aggregation bit size %s is greater than committee size %s", + attestation.getAggregationBits().size(), committee.size())); + } } return spec.isValidIndexedAttestation( diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/AttestationGossipManager.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/AttestationGossipManager.java index 3e9dfa6ba60..33007b1fdc8 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/AttestationGossipManager.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/AttestationGossipManager.java @@ -53,7 +53,8 @@ public void onNewAttestation(final ValidatableAttestation validatableAttestation if (validatableAttestation.isAggregate() || !validatableAttestation.markGossiped()) { return; } - final Attestation attestation = validatableAttestation.getAttestation(); + final Attestation attestation = validatableAttestation.getSingleAttestation(); + subnetSubscriptions .gossip(attestation) .finish( diff --git a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/AttestationSubnetSubscriptions.java b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/AttestationSubnetSubscriptions.java index de1231d38c0..356a0e3036f 100644 --- a/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/AttestationSubnetSubscriptions.java +++ b/networking/eth2/src/main/java/tech/pegasys/teku/networking/eth2/gossip/subnets/AttestationSubnetSubscriptions.java @@ -30,6 +30,8 @@ import tech.pegasys.teku.spec.datastructures.operations.Attestation; import tech.pegasys.teku.spec.datastructures.operations.AttestationSchema; import tech.pegasys.teku.spec.datastructures.state.ForkInfo; +import tech.pegasys.teku.spec.schemas.SchemaDefinitions; +import tech.pegasys.teku.spec.schemas.SchemaDefinitionsElectra; import tech.pegasys.teku.statetransition.util.DebugDataDumper; import tech.pegasys.teku.storage.client.RecentChainData; @@ -58,8 +60,14 @@ public AttestationSubnetSubscriptions( this.recentChainData = recentChainData; this.processor = processor; this.forkInfo = forkInfo; + final SchemaDefinitions schemaDefinitions = + spec.atEpoch(forkInfo.getFork().getEpoch()).getSchemaDefinitions(); attestationSchema = - spec.atEpoch(forkInfo.getFork().getEpoch()).getSchemaDefinitions().getAttestationSchema(); + schemaDefinitions + .toVersionElectra() + .>map( + SchemaDefinitionsElectra::getSingleAttestationSchema) + .orElse(schemaDefinitions.getAttestationSchema()); this.debugDataDumper = debugDataDumper; }